Patch Detail
get:
Show a patch.
patch:
Update a patch.
put:
Update a patch.
GET /api/1.1/patches/2222111/?format=api
{ "id": 2222111, "url": "http://patchwork.ozlabs.org/api/1.1/patches/2222111/?format=api", "web_url": "http://patchwork.ozlabs.org/project/qemu-devel/patch/20260410-qemu-vnc-v2-67-231416f76dc3@redhat.com/", "project": { "id": 14, "url": "http://patchwork.ozlabs.org/api/1.1/projects/14/?format=api", "name": "QEMU Development", "link_name": "qemu-devel", "list_id": "qemu-devel.nongnu.org", "list_email": "qemu-devel@nongnu.org", "web_url": "", "scm_url": "", "webscm_url": "" }, "msgid": "<20260410-qemu-vnc-v2-67-231416f76dc3@redhat.com>", "date": "2026-04-10T19:19:29", "name": "[v2,67/67] tools/qemu-vnc: add standalone VNC server over D-Bus", "commit_ref": null, "pull_url": null, "state": "new", "archived": false, "hash": "844f5aaa7b4e5956e6e50077b842e4c4c24d95e4", "submitter": { "id": 66774, "url": "http://patchwork.ozlabs.org/api/1.1/people/66774/?format=api", "name": "Marc-André Lureau", "email": "marcandre.lureau@redhat.com" }, "delegate": null, "mbox": "http://patchwork.ozlabs.org/project/qemu-devel/patch/20260410-qemu-vnc-v2-67-231416f76dc3@redhat.com/mbox/", "series": [ { "id": 499494, "url": "http://patchwork.ozlabs.org/api/1.1/series/499494/?format=api", "web_url": "http://patchwork.ozlabs.org/project/qemu-devel/list/?series=499494", "date": "2026-04-10T19:18:23", "name": "ui: add standalone VNC server over D-Bus", "version": 2, "mbox": "http://patchwork.ozlabs.org/series/499494/mbox/" } ], "comments": "http://patchwork.ozlabs.org/api/patches/2222111/comments/", "check": "pending", "checks": "http://patchwork.ozlabs.org/api/patches/2222111/checks/", "tags": {}, "headers": { "Return-Path": "<qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org>", "X-Original-To": "incoming@patchwork.ozlabs.org", "Delivered-To": "patchwork-incoming@legolas.ozlabs.org", "Authentication-Results": [ "legolas.ozlabs.org;\n\tdkim=pass (1024-bit key;\n unprotected) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256\n header.s=mimecast20190719 header.b=XbU+og8C;\n\tdkim-atps=neutral", "legolas.ozlabs.org;\n spf=pass (sender SPF authorized) smtp.mailfrom=nongnu.org\n (client-ip=209.51.188.17; helo=lists.gnu.org;\n envelope-from=qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org;\n receiver=patchwork.ozlabs.org)" ], "Received": [ "from lists.gnu.org (lists1p.gnu.org [209.51.188.17])\n\t(using TLSv1.2 with cipher ECDHE-ECDSA-AES256-GCM-SHA384 (256/256 bits))\n\t(No client certificate requested)\n\tby legolas.ozlabs.org (Postfix) with ESMTPS id 4fsmxr6LMzz1yGb\n\tfor <incoming@patchwork.ozlabs.org>; Sat, 11 Apr 2026 05:26:24 +1000 (AEST)", "from localhost ([::1] helo=lists1p.gnu.org)\n\tby lists.gnu.org with esmtp (Exim 4.90_1)\n\t(envelope-from <qemu-devel-bounces@nongnu.org>)\n\tid 1wBHUD-0000Vb-PQ; Fri, 10 Apr 2026 15:25:49 -0400", "from eggs.gnu.org ([2001:470:142:3::10])\n by lists1p.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)\n (Exim 4.90_1) (envelope-from <marcandre.lureau@redhat.com>)\n id 1wBHUB-0000Kl-GE\n for qemu-devel@nongnu.org; Fri, 10 Apr 2026 15:25:47 -0400", "from us-smtp-delivery-124.mimecast.com ([170.10.129.124])\n by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)\n (Exim 4.90_1) (envelope-from <marcandre.lureau@redhat.com>)\n id 1wBHU6-0003II-Gc\n for qemu-devel@nongnu.org; Fri, 10 Apr 2026 15:25:47 -0400", "from mx-prod-mc-01.mail-002.prod.us-west-2.aws.redhat.com\n (ec2-54-186-198-63.us-west-2.compute.amazonaws.com [54.186.198.63]) by\n relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3,\n cipher=TLS_AES_256_GCM_SHA384) id us-mta-635-cwiUQQBAMZOHgNdVN4igTw-1; Fri,\n 10 Apr 2026 15:25:39 -0400", "from mx-prod-int-01.mail-002.prod.us-west-2.aws.redhat.com\n (mx-prod-int-01.mail-002.prod.us-west-2.aws.redhat.com [10.30.177.4])\n (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)\n key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest\n SHA256)\n (No client certificate requested)\n by mx-prod-mc-01.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTPS\n id AA8CB195609D\n for <qemu-devel@nongnu.org>; Fri, 10 Apr 2026 19:25:38 +0000 (UTC)", "from localhost (unknown [10.44.22.4])\n by mx-prod-int-01.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTP\n id 0D78C3000C16; Fri, 10 Apr 2026 19:25:34 +0000 (UTC)" ], "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com;\n s=mimecast20190719; t=1775849141;\n h=from:from:reply-to:subject:subject:date:date:message-id:message-id:\n to:to:cc:cc:mime-version:mime-version:content-type:content-type:\n content-transfer-encoding:content-transfer-encoding:\n in-reply-to:in-reply-to:references:references;\n bh=DsgbszMduye2UC15r6dJelAckDT/FxP81KIaolU5B+E=;\n b=XbU+og8CAQ6rMt83w7jMD5GErygt4MmuewtB70Ti+3viEp+mZ5HJbt9yctl+aagAHhTYpC\n mrhqWme1NDj8uINYCp6g7vOVJjn5KbrV+2VYlvvMNkflz5H4M7d4e21WLS/iIsRKITBDrc\n cWngW4M35OwQbR9nUZO1tEVRunkfqeQ=", "X-MC-Unique": "cwiUQQBAMZOHgNdVN4igTw-1", "X-Mimecast-MFC-AGG-ID": "cwiUQQBAMZOHgNdVN4igTw_1775849138", "From": "=?utf-8?q?Marc-Andr=C3=A9_Lureau?= <marcandre.lureau@redhat.com>", "Date": "Fri, 10 Apr 2026 23:19:29 +0400", "Subject": "[PATCH v2 67/67] tools/qemu-vnc: add standalone VNC server over D-Bus", "MIME-Version": "1.0", "Content-Type": "text/plain; charset=\"utf-8\"", "Content-Transfer-Encoding": "8bit", "Message-Id": "<20260410-qemu-vnc-v2-67-231416f76dc3@redhat.com>", "References": "<20260410-qemu-vnc-v2-0-231416f76dc3@redhat.com>", "In-Reply-To": "<20260410-qemu-vnc-v2-0-231416f76dc3@redhat.com>", "To": "qemu-devel@nongnu.org", "Cc": "=?utf-8?q?Marc-Andr=C3=A9_Lureau?= <marcandre.lureau@redhat.com>", "X-Developer-Signature": "v=1; a=openpgp-sha256; l=155773;\n i=marcandre.lureau@redhat.com; h=from:subject:message-id;\n bh=3zGQNq4A7DRtUjW9gFIJnsuMyEQldufLTpgjuyB/zdY=;\n b=owEBbQKS/ZANAwAKAdro4Ql1lpzlAcsmYgBp2U0XBEuiYqCmGLCAdF76DJ7NnAhACJxlHW0FZ\n 2lNIcoFXlqJAjMEAAEKAB0WIQSHqb2TP4fGBtJ29i3a6OEJdZac5QUCadlNFwAKCRDa6OEJdZac\n 5UQiEAChu5K+uyka5AFlqz4XQ+gLAVUSQaUUq42CFKmPkqmTgiYbIxOR4yrU7NMogWOJPcF+XFd\n IhUflrF5KoiHXi+9BU3jlKxC8Pd2Rbmdwvup8Gva9BGoil15wC3K6rHWtdlyt7v56iOrwU9CoGH\n jelGeeTJn/DXjGTD9soewswLTsLlWR1Vq6Afqe2mQJ9BRPvUOCM+V9cB0nRBgG+JuhK4S4VnOHE\n Ylru/Jp9Xxw+TyR+dnBb5mJOsPOVxCBfRwS++9V91P1BVQUWC8HhmBJJTkZzjv11lryBlm0sAVZ\n PglpUWSTkSeoVvt92nQqaT89qoMx8iRko6Ls2zSpyVPgfU1YJkMGkDG+UfHMmQvIzNKIBe5rOXg\n ysrFpxjDY1s+q58cwHtDwkitumQ5sGr7arvk3hVta60hb2i5OQYBXK5vKiQUd51HydlIBlbENPm\n OLFNeECIxrKRh1KpVp5aP9BiqZtLvKMjKoj6HrRPmec7MeS/P0//jxvG7NvYd3diQgEMdsjlXKD\n f8T65LE3yT40IHx9akieMBHVMjcw01Dzg4Z54eOBmQDVe4VDZ1YWjefH4r0AWRckdtRKYqauDyi\n CFb4rYWtWOY3YM486T99yQSDFmcLkI7XZDDCOvBQrnEvN23vDoqO2gEnd+cEpFg1Z23vsT/uyxJ\n ZwyLFQTz7UVMiBA==", "X-Developer-Key": "i=marcandre.lureau@redhat.com; a=openpgp;\n fpr=87A9BD933F87C606D276F62DDAE8E10975969CE5", "X-Scanned-By": "MIMEDefang 3.4.1 on 10.30.177.4", "Received-SPF": "pass client-ip=170.10.129.124;\n envelope-from=marcandre.lureau@redhat.com;\n helo=us-smtp-delivery-124.mimecast.com", "X-Spam_score_int": "7", "X-Spam_score": "0.7", "X-Spam_bar": "/", "X-Spam_report": "(0.7 / 5.0 requ) BAYES_00=-1.9, DKIMWL_WL_HIGH=-0.54,\n DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1,\n RCVD_IN_DNSWL_NONE=-0.0001, RCVD_IN_MSPIKE_H4=0.001, RCVD_IN_MSPIKE_WL=0.001,\n RCVD_IN_SBL_CSS=3.335, RCVD_IN_VALIDITY_RPBL_BLOCKED=0.001,\n RCVD_IN_VALIDITY_SAFE_BLOCKED=0.001, SPF_HELO_PASS=-0.001,\n SPF_PASS=-0.001 autolearn=no autolearn_force=no", "X-Spam_action": "no action", "X-BeenThere": "qemu-devel@nongnu.org", "X-Mailman-Version": "2.1.29", "Precedence": "list", "List-Id": "qemu development <qemu-devel.nongnu.org>", "List-Unsubscribe": "<https://lists.nongnu.org/mailman/options/qemu-devel>,\n <mailto:qemu-devel-request@nongnu.org?subject=unsubscribe>", "List-Archive": "<https://lists.nongnu.org/archive/html/qemu-devel>", "List-Post": "<mailto:qemu-devel@nongnu.org>", "List-Help": "<mailto:qemu-devel-request@nongnu.org?subject=help>", "List-Subscribe": "<https://lists.nongnu.org/mailman/listinfo/qemu-devel>,\n <mailto:qemu-devel-request@nongnu.org?subject=subscribe>", "Errors-To": "qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org", "Sender": "qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org" }, "content": "Add a standalone VNC server binary that connects to a running QEMU\ninstance via the D-Bus display interface (org.qemu.Display1, via the bus\nor directly p2p). This allows serving a VNC display without compiling\nVNC support directly into the QEMU system emulator, and enables running\nthe VNC server as a separate process with independent lifecycle and\nprivilege domain.\n\nBuilt only when both VNC and D-Bus display support are enabled.\nIf we wanted to have qemu -vnc disabled, and qemu-vnc built, we would\nneed to split CONFIG_VNC. This is left as a future exercise.\n\nCurrent omissions include some QEMU VNC runtime features (better handled via\nrestart), legacy options, and Windows support.\n\nSigned-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>\n---\n MAINTAINERS | 5 +\n docs/conf.py | 3 +\n docs/interop/dbus-display.rst | 2 +\n docs/interop/dbus-vnc.rst | 26 +\n docs/interop/index.rst | 1 +\n docs/meson.build | 1 +\n docs/tools/index.rst | 1 +\n docs/tools/qemu-vnc.rst | 222 +++++++\n meson.build | 17 +\n tools/qemu-vnc/qemu-vnc.h | 46 ++\n tools/qemu-vnc/trace.h | 4 +\n tests/qtest/dbus-vnc-test.c | 1342 +++++++++++++++++++++++++++++++++++++++++\n tools/qemu-vnc/audio.c | 307 ++++++++++\n tools/qemu-vnc/chardev.c | 127 ++++\n tools/qemu-vnc/clipboard.c | 378 ++++++++++++\n tools/qemu-vnc/console.c | 168 ++++++\n tools/qemu-vnc/dbus.c | 439 ++++++++++++++\n tools/qemu-vnc/display.c | 456 ++++++++++++++\n tools/qemu-vnc/input.c | 239 ++++++++\n tools/qemu-vnc/qemu-vnc.c | 491 +++++++++++++++\n tools/qemu-vnc/stubs.c | 62 ++\n tools/qemu-vnc/utils.c | 59 ++\n meson_options.txt | 2 +\n scripts/meson-buildoptions.sh | 3 +\n tests/dbus-daemon.sh | 14 +-\n tests/qtest/meson.build | 13 +\n tools/qemu-vnc/meson.build | 26 +\n tools/qemu-vnc/qemu-vnc1.xml | 174 ++++++\n tools/qemu-vnc/trace-events | 20 +\n 29 files changed, 4645 insertions(+), 3 deletions(-)", "diff": "diff --git a/MAINTAINERS b/MAINTAINERS\nindex ad215eced84..9cddf898c77 100644\n--- a/MAINTAINERS\n+++ b/MAINTAINERS\n@@ -2825,6 +2825,11 @@ F: docs/interop/vhost-user-gpu.rst\n F: contrib/vhost-user-gpu\n F: hw/display/vhost-user-*\n \n+qemu-vnc:\n+M: Marc-André Lureau <marcandre.lureau@redhat.com>\n+S: Maintained\n+F: tools/qemu-vnc\n+\n Cirrus VGA\n M: Gerd Hoffmann <kraxel@redhat.com>\n S: Odd Fixes\ndiff --git a/docs/conf.py b/docs/conf.py\nindex f835904ba1e..7e35d2158d3 100644\n--- a/docs/conf.py\n+++ b/docs/conf.py\n@@ -333,6 +333,9 @@\n ('tools/qemu-trace-stap', 'qemu-trace-stap',\n 'QEMU SystemTap trace tool',\n [], 1),\n+ ('tools/qemu-vnc', 'qemu-vnc',\n+ 'QEMU standalone VNC server',\n+ [], 1),\n ]\n man_make_section_directory = False\n \ndiff --git a/docs/interop/dbus-display.rst b/docs/interop/dbus-display.rst\nindex 8c6e8e0f5a8..87648e91dc0 100644\n--- a/docs/interop/dbus-display.rst\n+++ b/docs/interop/dbus-display.rst\n@@ -1,3 +1,5 @@\n+.. _dbus-display:\n+\n D-Bus display\n =============\n \ndiff --git a/docs/interop/dbus-vnc.rst b/docs/interop/dbus-vnc.rst\nnew file mode 100644\nindex 00000000000..d2b77978f63\n--- /dev/null\n+++ b/docs/interop/dbus-vnc.rst\n@@ -0,0 +1,26 @@\n+D-Bus VNC\n+=========\n+\n+The ``qemu-vnc`` standalone VNC server exposes a D-Bus interface for management\n+and monitoring of VNC connections.\n+\n+The service is available on the bus under the well-known name ``org.qemu.vnc``.\n+Objects are exported under ``/org/qemu/Vnc1/``.\n+\n+.. contents::\n+ :local:\n+ :depth: 1\n+\n+.. only:: sphinx4\n+\n+ .. dbus-doc:: tools/qemu-vnc/qemu-vnc1.xml\n+\n+.. only:: not sphinx4\n+\n+ .. warning::\n+ Sphinx 4 is required to build D-Bus documentation.\n+\n+ This is the content of ``tools/qemu-vnc/qemu-vnc1.xml``:\n+\n+ .. literalinclude:: ../../tools/qemu-vnc/qemu-vnc1.xml\n+ :language: xml\ndiff --git a/docs/interop/index.rst b/docs/interop/index.rst\nindex d830c5c4104..2cf3a8c9aa3 100644\n--- a/docs/interop/index.rst\n+++ b/docs/interop/index.rst\n@@ -13,6 +13,7 @@ are useful for making QEMU interoperate with other software.\n dbus\n dbus-vmstate\n dbus-display\n+ dbus-vnc\n live-block-operations\n nbd\n parallels\ndiff --git a/docs/meson.build b/docs/meson.build\nindex 7e54b01e6a0..c3e9fb05846 100644\n--- a/docs/meson.build\n+++ b/docs/meson.build\n@@ -54,6 +54,7 @@ if build_docs\n 'qemu-pr-helper.8': (have_tools ? 'man8' : ''),\n 'qemu-storage-daemon.1': (have_tools ? 'man1' : ''),\n 'qemu-trace-stap.1': (stap.found() ? 'man1' : ''),\n+ 'qemu-vnc.1': (have_qemu_vnc ? 'man1' : ''),\n 'qemu.1': 'man1',\n 'qemu-block-drivers.7': 'man7',\n 'qemu-cpu-models.7': 'man7'\ndiff --git a/docs/tools/index.rst b/docs/tools/index.rst\nindex 1e88ae48cdc..868c3c4d9d8 100644\n--- a/docs/tools/index.rst\n+++ b/docs/tools/index.rst\n@@ -16,3 +16,4 @@ command line utilities and other standalone programs.\n qemu-pr-helper\n qemu-trace-stap\n qemu-vmsr-helper\n+ qemu-vnc\ndiff --git a/docs/tools/qemu-vnc.rst b/docs/tools/qemu-vnc.rst\nnew file mode 100644\nindex 00000000000..a4de4a7d4f8\n--- /dev/null\n+++ b/docs/tools/qemu-vnc.rst\n@@ -0,0 +1,222 @@\n+.. _qemu-vnc:\n+\n+==========================\n+QEMU standalone VNC server\n+==========================\n+\n+Synopsis\n+--------\n+\n+**qemu-vnc** [*OPTION*]...\n+\n+Description\n+-----------\n+\n+``qemu-vnc`` is a standalone VNC server that connects to a running QEMU instance\n+via the D-Bus display interface (:ref:`dbus-display`). It serves the guest\n+display, input, audio, clipboard, and serial console chardevs over the VNC\n+protocol, allowing VNC clients to interact with the virtual machine without QEMU\n+itself binding a VNC socket.\n+\n+Options\n+-------\n+\n+.. program:: qemu-vnc\n+\n+.. option:: -h, --help\n+\n+ Display help and exit.\n+\n+.. option:: -V, --version\n+\n+ Print version information and exit.\n+\n+.. option:: -a ADDRESS, --dbus-address=ADDRESS\n+\n+ D-Bus address to connect to. When not specified, ``qemu-vnc`` connects to the\n+ session bus.\n+\n+.. option:: -p FD, --dbus-p2p-fd=FD\n+\n+ File descriptor of an inherited Unix socket for a peer-to-peer D-Bus\n+ connection to QEMU. This is mutually exclusive with ``--dbus-address`` and\n+ ``--bus-name``.\n+\n+.. option:: -n NAME, --bus-name=NAME\n+\n+ D-Bus bus name of the QEMU instance to connect to. The default is\n+ ``org.qemu``. When a custom ``--dbus-address`` is given without a bus name,\n+ peer-to-peer D-Bus is used.\n+\n+.. option:: --password\n+\n+ Require VNC password authentication from connecting clients. The password is\n+ set at runtime via the D-Bus ``SetPassword`` method (see\n+ :doc:`/interop/dbus-vnc`). Clients will not be able to connect until a\n+ password has been set.\n+\n+ This option is ignored when a systemd credential password is present, since\n+ password authentication is already enabled via ``password-secret`` in that\n+ case.\n+\n+.. option:: -l ADDR, --vnc-addr=ADDR\n+\n+ VNC listen address in the same format as the QEMU ``-vnc`` option (default\n+ ``localhost:0``, i.e. TCP port 5900).\n+\n+.. option:: -w ADDR, --websocket=ADDR\n+\n+ Enable WebSocket transport on the given address. *ADDR* can be a port number\n+ or an *address:port* pair.\n+\n+.. option:: -O OBJDEF, --object=OBJDEF\n+\n+ Create a QEMU user-creatable object. *OBJDEF* uses the same key=value syntax\n+ as the QEMU ``-object`` option. This option may be given multiple times. It is\n+ needed, for example, to create authorization objects referenced by\n+ ``--tls-authz``.\n+\n+.. option:: -t DIR, --tls-creds=DIR\n+\n+ Directory containing TLS x509 credentials (``ca-cert.pem``,\n+ ``server-cert.pem``, ``server-key.pem``). When specified, the VNC server\n+ requires TLS from connecting clients.\n+\n+.. option:: --tls-authz=ID\n+\n+ ID of a ``QAuthZ`` object previously created with ``--object`` for TLS client\n+ certificate authorization. When specified, the TLS credentials are created\n+ with ``verify-peer=yes`` so connecting clients must present a valid\n+ certificate. After the TLS handshake, the client certificate Distinguished\n+ Name is checked against the authorization object. This option requires\n+ ``--tls-creds``.\n+\n+.. option:: --sasl\n+\n+ Require that the client use SASL to authenticate with the VNC server. The\n+ exact choice of authentication method used is controlled from the system /\n+ user's SASL configuration file for the 'qemu' service. This is typically found\n+ in ``/etc/sasl2/qemu.conf``. If running QEMU as an unprivileged user, an\n+ environment variable ``SASL_CONF_PATH`` can be used to make it search\n+ alternate locations for the service config. While some SASL auth methods can\n+ also provide data encryption (eg GSSAPI), it is recommended that SASL always\n+ be combined with the 'tls' and 'x509' settings to enable use of SSL and server\n+ certificates. This ensures a data encryption preventing compromise of\n+ authentication credentials. See the :ref:`VNC security` section in the System\n+ Emulation Users Guide for details on using SASL authentication.\n+\n+.. option:: --sasl-authz=ID\n+\n+ ID of a ``QAuthZ`` object previously created with ``--object`` for SASL\n+ username authorization. After successful SASL authentication, the\n+ authenticated username is checked against the authorization object. If the\n+ check fails, the client is disconnected. This option requires ``--sasl``.\n+\n+.. option:: -s POLICY, --share=POLICY\n+\n+ Set display sharing policy. *POLICY* is one of ``allow-exclusive``,\n+ ``force-shared``, or ``ignore``.\n+\n+ ``allow-exclusive`` allows clients to ask for exclusive access. As suggested\n+ by the RFB spec this is implemented by dropping other connections. Connecting\n+ multiple clients in parallel requires all clients asking for a shared session\n+ (vncviewer: -shared switch). This is the default.\n+\n+ ``force-shared`` disables exclusive client access. Useful for shared desktop\n+ sessions, where you don't want someone forgetting to specify -shared\n+ disconnect everybody else.\n+\n+ ``ignore`` completely ignores the shared flag and allows everybody to connect\n+ unconditionally. Doesn't conform to the RFB spec but is traditional QEMU\n+ behavior.\n+\n+.. option:: -C NAME, --vt-chardev=NAME\n+\n+ Chardev type name to expose as a VNC text console. This option may be given\n+ multiple times to expose several chardevs. When not specified, the defaults\n+ ``org.qemu.console.serial.0`` and ``org.qemu.monitor.hmp.0`` are used.\n+\n+.. option:: -N, --no-vt\n+\n+ Do not expose any chardevs as text consoles. This overrides the default\n+ chardev list and any ``--vt-chardev`` options.\n+\n+.. option:: -k LAYOUT, --keyboard-layout=LAYOUT\n+\n+ Keyboard layout (e.g. ``en-us``). Passed through to the VNC server for\n+ key-code translation.\n+\n+.. option:: --lossy\n+\n+ Enable lossy compression methods (gradient, JPEG, ...). If this option is set,\n+ VNC client may receive lossy framebuffer updates depending on its encoding\n+ settings. Enabling this option can save a lot of bandwidth at the expense of\n+ quality.\n+\n+.. option:: --non-adaptive\n+\n+ Disable adaptive encodings. Adaptive encodings are enabled by default. An\n+ adaptive encoding will try to detect frequently updated screen regions, and\n+ send updates in these regions using a lossy encoding (like JPEG). This can be\n+ really helpful to save bandwidth when playing videos. Disabling adaptive\n+ encodings restores the original static behavior of encodings like Tight.\n+\n+.. option:: -T, --trace [[enable=]PATTERN][,events=FILE][,file=FILE]\n+\n+ .. include:: ../qemu-option-trace.rst.inc\n+\n+Examples\n+--------\n+\n+Start QEMU with the D-Bus display backend::\n+\n+ qemu-system-x86_64 -display dbus ...\n+\n+Then attach ``qemu-vnc``::\n+\n+ qemu-vnc\n+\n+A VNC client can now connect to ``localhost:5900``.\n+\n+To listen on a different port with TLS::\n+\n+ qemu-vnc --vnc-addr localhost:1 --tls-creds /etc/pki/qemu-vnc\n+\n+To require TLS with client certificate authorization::\n+\n+ qemu-vnc --object authz-list-file,id=auth0,filename=/etc/qemu/vnc.acl,refresh=on \\\n+ --tls-creds /etc/pki/qemu-vnc --tls-authz auth0\n+\n+To enable SASL authentication with TLS::\n+\n+ qemu-vnc --tls-creds /etc/pki/qemu-vnc --sasl\n+\n+VNC password authentication\n+----------------------------\n+\n+There are two ways to enable VNC password authentication:\n+\n+1. ``--password`` flag -- start ``qemu-vnc`` with ``--password`` and\n+ then set the password at runtime using the D-Bus ``SetPassword``\n+ method. Clients will be rejected until a password is set.\n+\n+2. systemd credentials -- if the ``CREDENTIALS_DIRECTORY``\n+ environment variable is set (see :manpage:`systemd.exec(5)`) and\n+ contains a file named ``vnc-password``, the VNC server will use\n+ that file's contents as the password automatically. The\n+ ``--password`` flag is not needed in this case.\n+\n+D-Bus interface\n+---------------\n+\n+``qemu-vnc`` exposes a D-Bus interface for management and monitoring of\n+VNC connections. See :doc:`/interop/dbus-vnc` for the full interface\n+reference.\n+\n+See also\n+--------\n+\n+:manpage:`qemu(1)`,\n+:doc:`/interop/dbus-display`,\n+:doc:`/interop/dbus-vnc`,\n+`The RFB Protocol <https://github.com/rfbproto/rfbproto>`_\ndiff --git a/meson.build b/meson.build\nindex ab3e97eb9f4..78aa3d490ad 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -2341,6 +2341,17 @@ dbus_display = get_option('dbus_display') \\\n error_message: gdbus_codegen_error.format('-display dbus')) \\\n .allowed()\n \n+have_qemu_vnc = get_option('qemu_vnc') \\\n+ .require(have_tools,\n+ error_message: 'qemu-vnc requires tools support') \\\n+ .require(dbus_display,\n+ error_message: 'qemu-vnc requires dbus-display support') \\\n+ .require(vnc.found(),\n+ error_message: 'qemu-vnc requires vnc support') \\\n+ .require(host_os != 'windows',\n+ error_message: 'qemu-vnc is not currently supported on Windows') \\\n+ .allowed()\n+\n have_virtfs = get_option('virtfs') \\\n .require(host_os == 'linux' or host_os == 'darwin' or host_os == 'freebsd',\n error_message: 'virtio-9p (virtfs) requires Linux or macOS or FreeBSD') \\\n@@ -3595,6 +3606,7 @@ trace_events_subdirs = [\n 'monitor',\n 'util',\n 'gdbstub',\n+ 'tools/qemu-vnc',\n ]\n if have_linux_user\n trace_events_subdirs += [ 'linux-user' ]\n@@ -4563,6 +4575,10 @@ if have_tools\n subdir('contrib/ivshmem-client')\n subdir('contrib/ivshmem-server')\n endif\n+\n+ if have_qemu_vnc\n+ subdir('tools/qemu-vnc')\n+ endif\n endif\n \n if stap.found()\n@@ -4898,6 +4914,7 @@ if vnc.found()\n summary_info += {'VNC SASL support': sasl}\n summary_info += {'VNC JPEG support': jpeg}\n endif\n+summary_info += {'VNC D-Bus server (qemu-vnc)': have_qemu_vnc}\n summary_info += {'spice protocol support': spice_protocol}\n if spice_protocol.found()\n summary_info += {' spice server support': spice}\ndiff --git a/tools/qemu-vnc/qemu-vnc.h b/tools/qemu-vnc/qemu-vnc.h\nnew file mode 100644\nindex 00000000000..420d5f66d42\n--- /dev/null\n+++ b/tools/qemu-vnc/qemu-vnc.h\n@@ -0,0 +1,46 @@\n+/*\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+#ifndef CONTRIB_QEMU_VNC_H\n+#define CONTRIB_QEMU_VNC_H\n+\n+#include \"qemu/osdep.h\"\n+\n+#include <gio/gunixfdlist.h>\n+#include \"qemu/dbus.h\"\n+#include \"ui/console.h\"\n+#include \"ui/dbus-display1.h\"\n+\n+#define TEXT_COLS 80\n+#define TEXT_ROWS 24\n+#define TEXT_FONT_WIDTH 8\n+#define TEXT_FONT_HEIGHT 16\n+\n+\n+QemuTextConsole *qemu_vnc_text_console_new(const char *name,\n+ int fd, bool echo);\n+\n+void input_setup(QemuDBusDisplay1Keyboard *kbd,\n+ QemuDBusDisplay1Mouse *mouse);\n+bool console_setup(GDBusConnection *bus, const char *bus_name,\n+ const char *console_path);\n+QemuDBusDisplay1Keyboard *console_get_keyboard(QemuConsole *con);\n+QemuDBusDisplay1Mouse *console_get_mouse(QemuConsole *con);\n+\n+void audio_setup(GDBusObjectManager *manager);\n+void clipboard_setup(GDBusObjectManager *manager, GDBusConnection *bus);\n+void chardev_setup(const char * const *chardev_names,\n+ GDBusObjectManager *manager);\n+\n+GThread *p2p_dbus_thread_new(int fd);\n+\n+void vnc_dbus_setup(GDBusConnection *bus);\n+void vnc_dbus_cleanup(void);\n+void vnc_dbus_client_connected(const char *host, const char *service,\n+ const char *family, bool websocket);\n+void vnc_dbus_client_initialized(const char *host, const char *service,\n+ const char *x509_dname,\n+ const char *sasl_username);\n+void vnc_dbus_client_disconnected(const char *host, const char *service);\n+\n+#endif /* CONTRIB_QEMU_VNC_H */\ndiff --git a/tools/qemu-vnc/trace.h b/tools/qemu-vnc/trace.h\nnew file mode 100644\nindex 00000000000..5fb7b432359\n--- /dev/null\n+++ b/tools/qemu-vnc/trace.h\n@@ -0,0 +1,4 @@\n+/*\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+#include \"trace/trace-tools_qemu_vnc.h\"\ndiff --git a/tests/qtest/dbus-vnc-test.c b/tests/qtest/dbus-vnc-test.c\nnew file mode 100644\nindex 00000000000..cbd6def9d51\n--- /dev/null\n+++ b/tests/qtest/dbus-vnc-test.c\n@@ -0,0 +1,1342 @@\n+/*\n+ * D-Bus VNC server (qemu-vnc) end-to-end test\n+ *\n+ * Copyright (c) 2026 Red Hat, Inc.\n+ *\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+#include <gio/gio.h>\n+#include <gvnc.h>\n+#include <sys/un.h>\n+#include \"qemu/sockets.h\"\n+#include \"libqtest.h\"\n+#include \"qemu-vnc1.h\"\n+#ifdef CONFIG_TASN1\n+#include \"tests/unit/crypto-tls-x509-helpers.h\"\n+#endif\n+\n+#define VNC_TEST_TIMEOUT_MS 10000\n+\n+typedef struct DbusTest {\n+ QTestState *qts;\n+ GSubprocess *vnc_subprocess;\n+ GTestDBus *bus;\n+ GDBusConnection *bus_conn;\n+ GMainLoop *loop;\n+ char *vnc_sock_path;\n+ char *tmp_dir;\n+ char *bus_addr;\n+} DbusTest;\n+\n+typedef struct LifecycleData {\n+ DbusTest *dt;\n+ QemuVnc1Server *server_proxy;\n+ VncConnection *conn;\n+ char *client_path;\n+ gboolean got_connected;\n+ gboolean got_initialized;\n+ gboolean got_disconnected;\n+} LifecycleData;\n+\n+static QemuVnc1Server *\n+create_server_proxy(GDBusConnection *bus_conn, GError **errp)\n+{\n+ return qemu_vnc1_server_proxy_new_sync(\n+ bus_conn,\n+ G_DBUS_PROXY_FLAGS_NONE,\n+ \"org.qemu.vnc\",\n+ \"/org/qemu/Vnc1/Server\",\n+ NULL, errp);\n+}\n+\n+static void\n+on_vnc_error(VncConnection *self, const char *msg)\n+{\n+ g_error(\"vnc-error: %s\", msg);\n+}\n+\n+static void\n+on_vnc_auth_failure(VncConnection *self, const char *msg)\n+{\n+ g_error(\"vnc-auth-failure: %s\", msg);\n+}\n+\n+static void\n+on_vnc_initialized(VncConnection *self, GMainLoop *loop)\n+{\n+ const char *name = vnc_connection_get_name(self);\n+\n+ g_assert_cmpstr(name, ==, \"QEMU (dbus-vnc-test)\");\n+ g_main_loop_quit(loop);\n+}\n+\n+static gboolean\n+timeout_cb(gpointer data)\n+{\n+ g_error(\"test timed out\");\n+ return G_SOURCE_REMOVE;\n+}\n+\n+static int\n+connect_unix_socket(const char *path)\n+{\n+ int fd;\n+ struct sockaddr_un addr = { .sun_family = AF_UNIX };\n+\n+ fd = socket(AF_UNIX, SOCK_STREAM, 0);\n+ g_assert(fd >= 0);\n+\n+ snprintf(addr.sun_path, sizeof(addr.sun_path), \"%s\", path);\n+\n+ if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {\n+ close(fd);\n+ return -1;\n+ }\n+ return fd;\n+}\n+\n+static int\n+wait_for_vnc_socket(const char *path, int timeout_ms)\n+{\n+ int elapsed = 0;\n+ const int interval = 50;\n+\n+ while (elapsed < timeout_ms) {\n+ int fd = connect_unix_socket(path);\n+\n+ if (fd >= 0) {\n+ return fd;\n+ }\n+\n+ g_usleep(interval * 1000);\n+ elapsed += interval;\n+ }\n+ return -1;\n+}\n+\n+static GSubprocess *\n+spawn_qemu_vnc(int dbus_fd, const char *sock_path)\n+{\n+ const char *binary;\n+ g_autoptr(GError) err = NULL;\n+ g_autoptr(GSubprocessLauncher) launcher = NULL;\n+ GSubprocess *proc;\n+ g_autofree char *fd_str = NULL;\n+ g_autofree char *vnc_addr = NULL;\n+\n+ binary = g_getenv(\"QTEST_QEMU_VNC_BINARY\");\n+ g_assert(binary != NULL);\n+\n+ fd_str = g_strdup_printf(\"%d\", dbus_fd);\n+ vnc_addr = g_strdup_printf(\"unix:%s\", sock_path);\n+\n+ launcher = g_subprocess_launcher_new(G_SUBPROCESS_FLAGS_NONE);\n+ g_subprocess_launcher_take_fd(launcher, dbus_fd, dbus_fd);\n+\n+ proc = g_subprocess_launcher_spawn(launcher, &err,\n+ binary,\n+ \"--dbus-p2p-fd\", fd_str,\n+ \"--vnc-addr\", vnc_addr,\n+ NULL);\n+ g_assert_no_error(err);\n+ g_assert(proc != NULL);\n+\n+ return proc;\n+}\n+\n+static GSubprocess *\n+spawn_qemu_vnc_bus_full(const char *dbus_addr, const char *sock_path,\n+ const char *const *extra_args)\n+{\n+ const char *binary;\n+ g_autoptr(GError) err = NULL;\n+ g_autoptr(GSubprocessLauncher) launcher = NULL;\n+ g_autoptr(GPtrArray) argv = NULL;\n+ GSubprocess *proc;\n+ g_autofree char *vnc_addr = NULL;\n+\n+ binary = g_getenv(\"QTEST_QEMU_VNC_BINARY\");\n+ g_assert(binary != NULL);\n+\n+ vnc_addr = g_strdup_printf(\"unix:%s\", sock_path);\n+\n+ argv = g_ptr_array_new();\n+ g_ptr_array_add(argv, (gpointer)binary);\n+ g_ptr_array_add(argv, (gpointer)\"--dbus-address\");\n+ g_ptr_array_add(argv, (gpointer)dbus_addr);\n+ g_ptr_array_add(argv, (gpointer)\"--bus-name\");\n+ g_ptr_array_add(argv, (gpointer)\"org.qemu\");\n+ g_ptr_array_add(argv, (gpointer)\"--vnc-addr\");\n+ g_ptr_array_add(argv, (gpointer)vnc_addr);\n+\n+ if (extra_args) {\n+ for (int i = 0; extra_args[i]; i++) {\n+ g_ptr_array_add(argv, (gpointer)extra_args[i]);\n+ }\n+ }\n+\n+ g_ptr_array_add(argv, NULL);\n+\n+ launcher = g_subprocess_launcher_new(G_SUBPROCESS_FLAGS_NONE);\n+ proc = g_subprocess_launcher_spawnv(launcher, (const char *const *)argv->pdata, &err);\n+ g_assert_no_error(err);\n+ g_assert(proc != NULL);\n+\n+ return proc;\n+}\n+\n+\n+static void\n+name_appeared_cb(GDBusConnection *connection,\n+ const gchar *name,\n+ const gchar *name_owner,\n+ gpointer user_data)\n+{\n+ gboolean *appeared = user_data;\n+ *appeared = TRUE;\n+}\n+\n+static bool\n+setup_dbus_test_full(DbusTest *dt, const char *const *vnc_extra_args)\n+{\n+ g_autoptr(GError) err = NULL;\n+ g_auto(GStrv) addr_parts = NULL;\n+ g_autofree char *qemu_args = NULL;\n+\n+ if (!g_getenv(\"QTEST_QEMU_VNC_BINARY\")) {\n+ g_test_skip(\"QTEST_QEMU_VNC_BINARY not set\");\n+ return false;\n+ }\n+\n+ dt->bus = g_test_dbus_new(G_TEST_DBUS_NONE);\n+ g_test_dbus_up(dt->bus);\n+\n+ /* remove ,guid=foo part */\n+ addr_parts = g_strsplit(g_test_dbus_get_bus_address(dt->bus), \",\", 2);\n+ dt->bus_addr = g_strdup(addr_parts[0]);\n+\n+ dt->bus_conn = g_dbus_connection_new_for_address_sync(\n+ g_test_dbus_get_bus_address(dt->bus),\n+ G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT |\n+ G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION,\n+ NULL, NULL, &err);\n+ g_assert_no_error(err);\n+\n+ qemu_args = g_strdup_printf(\"-display dbus,addr=%s \"\n+ \"-name dbus-vnc-test\", dt->bus_addr);\n+ dt->qts = qtest_init(qemu_args);\n+\n+ dt->tmp_dir = g_dir_make_tmp(\"dbus-vnc-test-XXXXXX\", NULL);\n+ g_assert(dt->tmp_dir != NULL);\n+ dt->vnc_sock_path = g_build_filename(dt->tmp_dir, \"vnc.sock\", NULL);\n+ dt->vnc_subprocess = spawn_qemu_vnc_bus_full(dt->bus_addr,\n+ dt->vnc_sock_path,\n+ vnc_extra_args);\n+\n+ /*\n+ * Wait for the org.qemu.vnc bus name to appear, which indicates\n+ * qemu-vnc has fully initialized (connected to QEMU, set up the\n+ * display, exported its D-Bus interfaces, and opened the VNC\n+ * socket).\n+ */\n+ {\n+ guint watch_id, timeout_id;\n+ gboolean appeared = FALSE;\n+\n+ watch_id = g_bus_watch_name_on_connection(\n+ dt->bus_conn, \"org.qemu.vnc\",\n+ G_BUS_NAME_WATCHER_FLAGS_NONE,\n+ name_appeared_cb, NULL, &appeared, NULL);\n+ timeout_id = g_timeout_add_seconds(10, timeout_cb, NULL);\n+\n+ while (!appeared &&\n+ g_main_context_iteration(NULL, TRUE)) {\n+ }\n+\n+ g_bus_unwatch_name(watch_id);\n+ g_source_remove(timeout_id);\n+\n+ if (!appeared) {\n+ g_test_fail();\n+ g_test_message(\"Timed out waiting for org.qemu.vnc bus name\");\n+ return false;\n+ }\n+ }\n+\n+ return true;\n+}\n+\n+static bool\n+setup_dbus_test(DbusTest *dt)\n+{\n+ return setup_dbus_test_full(dt, NULL);\n+}\n+\n+static void\n+cleanup_dbus_test(DbusTest *dt)\n+{\n+ if (dt->bus_conn) {\n+ g_dbus_connection_close_sync(dt->bus_conn, NULL, NULL);\n+ g_object_unref(dt->bus_conn);\n+ }\n+ if (dt->vnc_subprocess) {\n+ g_subprocess_force_exit(dt->vnc_subprocess);\n+ g_subprocess_wait(dt->vnc_subprocess, NULL, NULL);\n+ g_object_unref(dt->vnc_subprocess);\n+ }\n+ if (dt->vnc_sock_path) {\n+ unlink(dt->vnc_sock_path);\n+ g_free(dt->vnc_sock_path);\n+ }\n+ if (dt->tmp_dir) {\n+ rmdir(dt->tmp_dir);\n+ g_free(dt->tmp_dir);\n+ }\n+ if (dt->qts) {\n+ qtest_quit(dt->qts);\n+ }\n+ if (dt->bus) {\n+ g_test_dbus_down(dt->bus);\n+ g_object_unref(dt->bus);\n+ }\n+ g_free(dt->bus_addr);\n+}\n+\n+static void\n+test_dbus_vnc_basic(void)\n+{\n+ DbusTest dt = { 0 };\n+ VncConnection *conn = NULL;\n+ GMainLoop *loop = NULL;\n+ int pair[2];\n+ int vnc_fd;\n+ guint timeout_id;\n+\n+ if (!g_getenv(\"QTEST_QEMU_VNC_BINARY\")) {\n+ g_test_skip(\"QTEST_QEMU_VNC_BINARY not set\");\n+ return;\n+ }\n+\n+ dt.qts = qtest_init(\"-display dbus,p2p=yes -name dbus-vnc-test\");\n+\n+ g_assert_cmpint(qemu_socketpair(AF_UNIX, SOCK_STREAM, 0, pair), ==, 0);\n+ qtest_qmp_add_client(dt.qts, \"@dbus-display\", pair[1]);\n+ close(pair[1]);\n+\n+ dt.tmp_dir = g_dir_make_tmp(\"dbus-vnc-test-XXXXXX\", NULL);\n+ g_assert(dt.tmp_dir != NULL);\n+ dt.vnc_sock_path = g_build_filename(dt.tmp_dir, \"vnc.sock\", NULL);\n+\n+ dt.vnc_subprocess = spawn_qemu_vnc(pair[0], dt.vnc_sock_path);\n+\n+ vnc_fd = wait_for_vnc_socket(dt.vnc_sock_path, VNC_TEST_TIMEOUT_MS);\n+ g_assert(vnc_fd >= 0);\n+\n+ loop = g_main_loop_new(NULL, FALSE);\n+\n+ conn = vnc_connection_new();\n+ g_signal_connect(conn, \"vnc-error\",\n+ G_CALLBACK(on_vnc_error), NULL);\n+ g_signal_connect(conn, \"vnc-auth-failure\",\n+ G_CALLBACK(on_vnc_auth_failure), NULL);\n+ g_signal_connect(conn, \"vnc-initialized\",\n+ G_CALLBACK(on_vnc_initialized), loop);\n+ vnc_connection_set_auth_type(conn, VNC_CONNECTION_AUTH_NONE);\n+ vnc_connection_open_fd(conn, vnc_fd);\n+\n+ timeout_id = g_timeout_add_seconds(10, timeout_cb, NULL);\n+ g_main_loop_run(loop);\n+ g_source_remove(timeout_id);\n+\n+ if (conn) {\n+ vnc_connection_shutdown(conn);\n+ g_object_unref(conn);\n+ }\n+ g_clear_pointer(&loop, g_main_loop_unref);\n+ cleanup_dbus_test(&dt);\n+}\n+\n+static void\n+test_dbus_vnc_server_props(void)\n+{\n+ DbusTest dt = { 0 };\n+ QemuVnc1Server *proxy = NULL;\n+ g_autoptr(GError) err = NULL;\n+ const gchar *const *clients;\n+ GVariant *listeners;\n+\n+ if (!setup_dbus_test(&dt)) {\n+ goto cleanup;\n+ }\n+\n+ proxy = create_server_proxy(dt.bus_conn, &err);\n+ g_assert_no_error(err);\n+ g_assert_nonnull(proxy);\n+\n+ g_assert_cmpstr(qemu_vnc1_server_get_name(proxy), ==,\n+ \"dbus-vnc-test\");\n+ g_assert_cmpstr(qemu_vnc1_server_get_auth(proxy), ==,\n+ \"none\");\n+ g_assert_cmpstr(qemu_vnc1_server_get_vencrypt_sub_auth(proxy), ==,\n+ \"\");\n+\n+ clients = qemu_vnc1_server_get_clients(proxy);\n+ g_assert_nonnull(clients);\n+ g_assert_cmpint(g_strv_length((gchar **)clients), ==, 0);\n+\n+ listeners = qemu_vnc1_server_get_listeners(proxy);\n+ g_assert_nonnull(listeners);\n+ g_assert_cmpint(g_variant_n_children(listeners), >, 0);\n+\n+cleanup:\n+ g_clear_object(&proxy);\n+ cleanup_dbus_test(&dt);\n+}\n+\n+static void\n+on_client_connected(QemuVnc1Server *proxy,\n+ const gchar *client_path,\n+ LifecycleData *data)\n+{\n+ data->got_connected = TRUE;\n+ data->client_path = g_strdup(client_path);\n+}\n+\n+static void\n+on_client_initialized(QemuVnc1Server *proxy,\n+ const gchar *client_path,\n+ LifecycleData *data)\n+{\n+ data->got_initialized = TRUE;\n+ g_main_loop_quit(data->dt->loop);\n+}\n+\n+static void\n+on_client_disconnected(QemuVnc1Server *proxy,\n+ const gchar *client_path,\n+ LifecycleData *data)\n+{\n+ data->got_disconnected = TRUE;\n+ g_main_loop_quit(data->dt->loop);\n+}\n+\n+static void\n+test_dbus_vnc_client_lifecycle(void)\n+{\n+ DbusTest dt = { 0 };\n+ QemuVnc1Server *server_proxy = NULL;\n+ QemuVnc1Client *client_proxy = NULL;\n+ g_autoptr(GError) err = NULL;\n+ LifecycleData ldata = { 0 };\n+ int vnc_fd;\n+ guint timeout_id;\n+\n+ if (!setup_dbus_test(&dt)) {\n+ goto cleanup;\n+ }\n+\n+ server_proxy = create_server_proxy(dt.bus_conn, &err);\n+ g_assert_no_error(err);\n+\n+ ldata.dt = &dt;\n+ ldata.server_proxy = server_proxy;\n+\n+ g_signal_connect(server_proxy, \"client-connected\",\n+ G_CALLBACK(on_client_connected), &ldata);\n+ g_signal_connect(server_proxy, \"client-initialized\",\n+ G_CALLBACK(on_client_initialized), &ldata);\n+ g_signal_connect(server_proxy, \"client-disconnected\",\n+ G_CALLBACK(on_client_disconnected), &ldata);\n+\n+ vnc_fd = wait_for_vnc_socket(dt.vnc_sock_path, VNC_TEST_TIMEOUT_MS);\n+ g_assert(vnc_fd >= 0);\n+\n+ ldata.conn = vnc_connection_new();\n+ g_signal_connect(ldata.conn, \"vnc-error\",\n+ G_CALLBACK(on_vnc_error), NULL);\n+ g_signal_connect(ldata.conn, \"vnc-auth-failure\",\n+ G_CALLBACK(on_vnc_auth_failure), NULL);\n+ vnc_connection_set_auth_type(ldata.conn, VNC_CONNECTION_AUTH_NONE);\n+ vnc_connection_open_fd(ldata.conn, vnc_fd);\n+\n+ /* wait for ClientInitialized */\n+ dt.loop = g_main_loop_new(NULL, FALSE);\n+ timeout_id = g_timeout_add_seconds(10, timeout_cb, NULL);\n+ g_main_loop_run(dt.loop);\n+ g_source_remove(timeout_id);\n+\n+ g_assert_true(ldata.got_connected);\n+ g_assert_true(ldata.got_initialized);\n+ g_assert_nonnull(ldata.client_path);\n+\n+ /* Check client properties while still connected */\n+ client_proxy = qemu_vnc1_client_proxy_new_sync(\n+ dt.bus_conn,\n+ G_DBUS_PROXY_FLAGS_NONE,\n+ \"org.qemu.vnc\",\n+ ldata.client_path,\n+ NULL, &err);\n+ g_assert_no_error(err);\n+\n+ g_assert_cmpstr(qemu_vnc1_client_get_family(client_proxy), ==,\n+ \"unix\");\n+ g_assert_false(qemu_vnc1_client_get_web_socket(client_proxy));\n+ g_assert_cmpstr(qemu_vnc1_client_get_x509_dname(client_proxy), ==,\n+ \"\");\n+ g_assert_cmpstr(qemu_vnc1_client_get_sasl_username(client_proxy),\n+ ==, \"\");\n+\n+ /* disconnect and wait for ClientDisconnected */\n+ vnc_connection_shutdown(ldata.conn);\n+ timeout_id = g_timeout_add_seconds(10, timeout_cb, NULL);\n+ g_main_loop_run(dt.loop);\n+ g_source_remove(timeout_id);\n+\n+ g_assert_true(ldata.got_disconnected);\n+\n+ g_object_unref(ldata.conn);\n+ g_main_loop_unref(dt.loop);\n+ dt.loop = NULL;\n+ g_free(ldata.client_path);\n+\n+cleanup:\n+ g_clear_object(&server_proxy);\n+ g_clear_object(&client_proxy);\n+ cleanup_dbus_test(&dt);\n+}\n+\n+static void\n+test_dbus_vnc_no_password(void)\n+{\n+ DbusTest dt = { 0 };\n+ QemuVnc1Server *proxy = NULL;\n+ g_autoptr(GError) err = NULL;\n+ gboolean ret;\n+\n+ if (!setup_dbus_test(&dt)) {\n+ goto cleanup;\n+ }\n+\n+ proxy = create_server_proxy(dt.bus_conn, &err);\n+ g_assert_no_error(err);\n+\n+ /*\n+ * With default auth=none, SetPassword should return an error\n+ * because VNC password authentication is not enabled.\n+ */\n+ ret = qemu_vnc1_server_call_set_password_sync(\n+ proxy, \"secret\",\n+ G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err);\n+ g_assert_false(ret);\n+ g_assert_error(err, G_DBUS_ERROR, G_DBUS_ERROR_FAILED);\n+ g_clear_error(&err);\n+\n+ ret = qemu_vnc1_server_call_expire_password_sync(\n+ proxy, \"never\",\n+ G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err);\n+ g_assert_no_error(err);\n+ g_assert_true(ret);\n+\n+ ret = qemu_vnc1_server_call_expire_password_sync(\n+ proxy, \"+3600\",\n+ G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err);\n+ g_assert_no_error(err);\n+ g_assert_true(ret);\n+\n+cleanup:\n+ g_clear_object(&proxy);\n+ cleanup_dbus_test(&dt);\n+}\n+\n+typedef struct PasswordData {\n+ DbusTest *dt;\n+ VncConnection *conn;\n+ const char *password;\n+ gboolean auth_succeeded;\n+ gboolean auth_failed;\n+} PasswordData;\n+\n+G_GNUC_BEGIN_IGNORE_DEPRECATIONS\n+static void\n+on_pw_vnc_auth_credential(VncConnection *conn, GValueArray *creds,\n+ PasswordData *data)\n+{\n+ for (guint i = 0; i < creds->n_values; i++) {\n+ int type = g_value_get_enum(g_value_array_get_nth(creds, i));\n+\n+ if (type == VNC_CONNECTION_CREDENTIAL_PASSWORD) {\n+ vnc_connection_set_credential(conn, type, data->password);\n+ }\n+ }\n+}\n+G_GNUC_END_IGNORE_DEPRECATIONS\n+\n+static void\n+on_pw_vnc_initialized(VncConnection *conn, PasswordData *data)\n+{\n+ data->auth_succeeded = TRUE;\n+ g_main_loop_quit(data->dt->loop);\n+}\n+\n+static void\n+on_pw_vnc_auth_failure(VncConnection *conn, const char *msg,\n+ PasswordData *data)\n+{\n+ data->auth_failed = TRUE;\n+ g_main_loop_quit(data->dt->loop);\n+}\n+\n+static void\n+on_pw_vnc_error(VncConnection *conn, const char *msg,\n+ PasswordData *data)\n+{\n+ data->auth_failed = TRUE;\n+ g_main_loop_quit(data->dt->loop);\n+}\n+\n+static void\n+test_dbus_vnc_password_auth(void)\n+{\n+ DbusTest dt = { 0 };\n+ QemuVnc1Server *proxy = NULL;\n+ g_autoptr(GError) err = NULL;\n+ PasswordData pdata = { 0 };\n+ const char *extra_args[] = { \"--password\", NULL };\n+ int vnc_fd;\n+ guint timeout_id;\n+ gboolean ret;\n+\n+ if (!setup_dbus_test_full(&dt, extra_args)) {\n+ goto cleanup;\n+ }\n+\n+ proxy = create_server_proxy(dt.bus_conn, &err);\n+ g_assert_no_error(err);\n+\n+ g_assert_cmpstr(qemu_vnc1_server_get_auth(proxy), ==, \"vnc\");\n+\n+ ret = qemu_vnc1_server_call_set_password_sync(\n+ proxy, \"testpass123\",\n+ G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err);\n+ g_assert_no_error(err);\n+ g_assert_true(ret);\n+\n+ vnc_fd = wait_for_vnc_socket(dt.vnc_sock_path, VNC_TEST_TIMEOUT_MS);\n+ g_assert(vnc_fd >= 0);\n+\n+ pdata.dt = &dt;\n+ pdata.password = \"testpass123\";\n+ pdata.conn = vnc_connection_new();\n+\n+ g_signal_connect(pdata.conn, \"vnc-error\",\n+ G_CALLBACK(on_pw_vnc_error), &pdata);\n+ g_signal_connect(pdata.conn, \"vnc-auth-failure\",\n+ G_CALLBACK(on_pw_vnc_auth_failure), &pdata);\n+ g_signal_connect(pdata.conn, \"vnc-auth-credential\",\n+ G_CALLBACK(on_pw_vnc_auth_credential), &pdata);\n+ g_signal_connect(pdata.conn, \"vnc-initialized\",\n+ G_CALLBACK(on_pw_vnc_initialized), &pdata);\n+ vnc_connection_set_auth_type(pdata.conn, VNC_CONNECTION_AUTH_VNC);\n+ vnc_connection_open_fd(pdata.conn, vnc_fd);\n+\n+ dt.loop = g_main_loop_new(NULL, FALSE);\n+ timeout_id = g_timeout_add_seconds(10, timeout_cb, NULL);\n+ g_main_loop_run(dt.loop);\n+ g_source_remove(timeout_id);\n+\n+ g_assert_true(pdata.auth_succeeded);\n+ g_assert_false(pdata.auth_failed);\n+\n+ vnc_connection_shutdown(pdata.conn);\n+ g_object_unref(pdata.conn);\n+ g_main_loop_unref(dt.loop);\n+ dt.loop = NULL;\n+\n+cleanup:\n+ g_clear_object(&proxy);\n+ cleanup_dbus_test(&dt);\n+}\n+\n+static void\n+test_dbus_vnc_sasl_authz_no_sasl(void)\n+{\n+ const char *binary;\n+ g_autoptr(GError) err = NULL;\n+ g_autoptr(GSubprocess) proc = NULL;\n+ gboolean ok;\n+\n+ binary = g_getenv(\"QTEST_QEMU_VNC_BINARY\");\n+ if (!binary) {\n+ g_test_skip(\"QTEST_QEMU_VNC_BINARY not set\");\n+ return;\n+ }\n+\n+ proc = g_subprocess_new(G_SUBPROCESS_FLAGS_STDERR_SILENCE,\n+ &err,\n+ binary,\n+ \"--sasl-authz\", \"authz0\",\n+ NULL);\n+ g_assert_no_error(err);\n+ g_assert_nonnull(proc);\n+\n+ ok = g_subprocess_wait(proc, NULL, &err);\n+ g_assert_no_error(err);\n+ g_assert_true(ok);\n+ g_assert_false(g_subprocess_get_successful(proc));\n+}\n+\n+#ifdef CONFIG_VNC_SASL\n+static void\n+test_dbus_vnc_sasl_server_props(void)\n+{\n+ DbusTest dt = { 0 };\n+ QemuVnc1Server *proxy = NULL;\n+ g_autoptr(GError) err = NULL;\n+ const char *extra_args[] = { \"--sasl\", NULL };\n+\n+ if (!setup_dbus_test_full(&dt, extra_args)) {\n+ goto cleanup;\n+ }\n+\n+ proxy = create_server_proxy(dt.bus_conn, &err);\n+ g_assert_no_error(err);\n+ g_assert_nonnull(proxy);\n+\n+ g_assert_cmpstr(qemu_vnc1_server_get_auth(proxy), ==, \"sasl\");\n+\n+cleanup:\n+ g_clear_object(&proxy);\n+ cleanup_dbus_test(&dt);\n+}\n+\n+#define SASL_TEST_USER \"testuser\"\n+#define SASL_TEST_PASS \"testpass123\"\n+\n+typedef struct SaslAuthData {\n+ DbusTest *dt;\n+ const char *username;\n+ const char *password;\n+ gboolean auth_succeeded;\n+ gboolean auth_failed;\n+} SaslAuthData;\n+\n+typedef struct SaslTestData {\n+ DbusTest dt;\n+ SaslAuthData sdata;\n+ char *sasl_dir;\n+ char *db_path;\n+} SaslTestData;\n+\n+G_GNUC_BEGIN_IGNORE_DEPRECATIONS\n+static void\n+on_sasl_vnc_auth_credential(VncConnection *conn, GValueArray *creds,\n+ SaslAuthData *data)\n+{\n+ for (guint i = 0; i < creds->n_values; i++) {\n+ int type = g_value_get_enum(g_value_array_get_nth(creds, i));\n+\n+ switch (type) {\n+ case VNC_CONNECTION_CREDENTIAL_USERNAME:\n+ vnc_connection_set_credential(conn, type, data->username);\n+ break;\n+ case VNC_CONNECTION_CREDENTIAL_PASSWORD:\n+ vnc_connection_set_credential(conn, type, data->password);\n+ break;\n+ }\n+ }\n+}\n+G_GNUC_END_IGNORE_DEPRECATIONS\n+\n+static void\n+on_sasl_vnc_initialized(VncConnection *conn, SaslAuthData *data)\n+{\n+ data->auth_succeeded = TRUE;\n+ g_main_loop_quit(data->dt->loop);\n+}\n+\n+static void\n+on_sasl_vnc_auth_failure(VncConnection *conn, const char *msg,\n+ SaslAuthData *data)\n+{\n+ data->auth_failed = TRUE;\n+ g_main_loop_quit(data->dt->loop);\n+}\n+\n+static void\n+on_sasl_vnc_error(VncConnection *conn, const char *msg,\n+ SaslAuthData *data)\n+{\n+ data->auth_failed = TRUE;\n+ g_main_loop_quit(data->dt->loop);\n+}\n+\n+/*\n+ * Create a SASL configuration directory with a qemu.conf and a\n+ * sasldb2 user database. Returns the path to the sasldb file,\n+ * or NULL if saslpasswd2 is not available.\n+ */\n+static char *\n+create_sasl_config(const char *dir, const char *username,\n+ const char *password)\n+{\n+ g_autofree char *conf_path = g_strdup_printf(\"%s/qemu.conf\", dir);\n+ g_autofree char *db_path = g_strdup_printf(\"%s/sasldb2\", dir);\n+ g_autoptr(GError) err = NULL;\n+ g_autoptr(GSubprocess) proc = NULL;\n+ g_autofree char *conf = NULL;\n+ GOutputStream *stdin_stream;\n+ gboolean ok;\n+\n+ /* use PLAIN, and local auxprop sasldb plugin */\n+ conf = g_strdup_printf(\n+ \"mech_list: plain\\n\"\n+ \"pwcheck_method: auxprop\\n\"\n+ \"auxprop_plugin: sasldb\\n\"\n+ \"sasldb_path: %s\\n\", db_path);\n+ g_assert_true(g_file_set_contents(conf_path, conf, -1, NULL));\n+\n+ proc = g_subprocess_new(\n+ G_SUBPROCESS_FLAGS_STDIN_PIPE |\n+ G_SUBPROCESS_FLAGS_STDOUT_SILENCE |\n+ G_SUBPROCESS_FLAGS_STDERR_SILENCE,\n+ &err,\n+ \"saslpasswd2\", \"-f\", db_path, \"-a\", \"qemu\", \"-p\", \"-c\",\n+ username, NULL);\n+ if (!proc) {\n+ return NULL;\n+ }\n+\n+ stdin_stream = g_subprocess_get_stdin_pipe(proc);\n+ g_output_stream_write_all(stdin_stream, password,\n+ strlen(password), NULL, NULL, NULL);\n+ g_output_stream_close(stdin_stream, NULL, NULL);\n+\n+ ok = g_subprocess_wait_check(proc, NULL, &err);\n+ if (!ok) {\n+ return NULL;\n+ }\n+\n+ return g_strdup(db_path);\n+}\n+\n+static void\n+cleanup_sasl_config(const char *dir, const char *db_path)\n+{\n+ g_autofree char *conf = g_strdup_printf(\"%s/qemu.conf\", dir);\n+\n+ unlink(conf);\n+ if (db_path) {\n+ unlink(db_path);\n+ }\n+ rmdir(dir);\n+}\n+\n+/*\n+ * Set up SASL environment: create temp config dir, sasldb, and\n+ * start qemu-vnc with the given extra_args. Returns FALSE if the\n+ * test should be skipped.\n+ */\n+static gboolean\n+setup_sasl_test(SaslTestData *st, const char **extra_args)\n+{\n+ if (!g_getenv(\"QTEST_QEMU_VNC_BINARY\")) {\n+ g_test_skip(\"QTEST_QEMU_VNC_BINARY not set\");\n+ return FALSE;\n+ }\n+\n+ st->sasl_dir = g_dir_make_tmp(\"dbus-vnc-sasl-XXXXXX\", NULL);\n+ g_assert_nonnull(st->sasl_dir);\n+\n+ st->db_path = create_sasl_config(st->sasl_dir, SASL_TEST_USER,\n+ SASL_TEST_PASS);\n+ if (!st->db_path) {\n+ g_test_skip(\"saslpasswd2 not available or failed\");\n+ cleanup_sasl_config(st->sasl_dir, NULL);\n+ return FALSE;\n+ }\n+\n+ g_setenv(\"SASL_CONF_PATH\", st->sasl_dir, TRUE);\n+\n+ if (!setup_dbus_test_full(&st->dt, extra_args)) {\n+ return FALSE;\n+ }\n+\n+ return TRUE;\n+}\n+\n+/*\n+ * Connect to the VNC server using SASL and run the main loop\n+ * until authentication completes or times out.\n+ */\n+static void\n+run_sasl_auth(SaslTestData *st, const char *username,\n+ const char *password)\n+{\n+ VncConnection *conn;\n+ guint timeout_id;\n+ int vnc_fd;\n+\n+ st->sdata.dt = &st->dt;\n+ st->sdata.username = username;\n+ st->sdata.password = password;\n+\n+ vnc_fd = wait_for_vnc_socket(st->dt.vnc_sock_path, VNC_TEST_TIMEOUT_MS);\n+ g_assert(vnc_fd >= 0);\n+\n+ conn = vnc_connection_new();\n+ g_signal_connect(conn, \"vnc-error\",\n+ G_CALLBACK(on_sasl_vnc_error), &st->sdata);\n+ g_signal_connect(conn, \"vnc-auth-failure\",\n+ G_CALLBACK(on_sasl_vnc_auth_failure), &st->sdata);\n+ g_signal_connect(conn, \"vnc-auth-credential\",\n+ G_CALLBACK(on_sasl_vnc_auth_credential),\n+ &st->sdata);\n+ g_signal_connect(conn, \"vnc-initialized\",\n+ G_CALLBACK(on_sasl_vnc_initialized), &st->sdata);\n+ vnc_connection_set_auth_type(conn, VNC_CONNECTION_AUTH_SASL);\n+ vnc_connection_open_fd(conn, vnc_fd);\n+\n+ st->dt.loop = g_main_loop_new(NULL, FALSE);\n+ timeout_id = g_timeout_add_seconds(10, timeout_cb, NULL);\n+ g_main_loop_run(st->dt.loop);\n+ g_source_remove(timeout_id);\n+\n+ g_signal_handlers_disconnect_by_data(conn, &st->sdata);\n+ vnc_connection_shutdown(conn);\n+ g_object_unref(conn);\n+ g_main_loop_unref(st->dt.loop);\n+ st->dt.loop = NULL;\n+}\n+\n+static void\n+cleanup_sasl_test(SaslTestData *st)\n+{\n+ cleanup_dbus_test(&st->dt);\n+ g_unsetenv(\"SASL_CONF_PATH\");\n+ cleanup_sasl_config(st->sasl_dir, st->db_path);\n+ g_free(st->sasl_dir);\n+ g_free(st->db_path);\n+}\n+\n+static void\n+test_dbus_vnc_sasl_auth(void)\n+{\n+ SaslTestData st = { 0 };\n+ const char *extra_args[] = { \"--sasl\", NULL };\n+\n+ if (!setup_sasl_test(&st, extra_args)) {\n+ return;\n+ }\n+\n+ run_sasl_auth(&st, SASL_TEST_USER, SASL_TEST_PASS);\n+\n+ g_assert_true(st.sdata.auth_succeeded);\n+ g_assert_false(st.sdata.auth_failed);\n+\n+ cleanup_sasl_test(&st);\n+}\n+\n+static void\n+test_dbus_vnc_sasl_auth_bad_password(void)\n+{\n+ SaslTestData st = { 0 };\n+ const char *extra_args[] = { \"--sasl\", NULL };\n+\n+ if (!setup_sasl_test(&st, extra_args)) {\n+ return;\n+ }\n+\n+ run_sasl_auth(&st, SASL_TEST_USER, \"wrongpassword\");\n+\n+ g_assert_false(st.sdata.auth_succeeded);\n+ g_assert_true(st.sdata.auth_failed);\n+\n+ cleanup_sasl_test(&st);\n+}\n+\n+static void\n+test_dbus_vnc_sasl_authz_denied(void)\n+{\n+ SaslTestData st = { 0 };\n+ const char *extra_args[] = {\n+ \"--sasl\",\n+ \"--object\",\n+ \"authz-simple,id=authz0,identity=otheruser\",\n+ \"--sasl-authz\", \"authz0\",\n+ NULL\n+ };\n+\n+ if (!setup_sasl_test(&st, extra_args)) {\n+ return;\n+ }\n+\n+ run_sasl_auth(&st, SASL_TEST_USER, SASL_TEST_PASS);\n+\n+ g_assert_false(st.sdata.auth_succeeded);\n+ g_assert_true(st.sdata.auth_failed);\n+\n+ cleanup_sasl_test(&st);\n+}\n+#endif /* CONFIG_VNC_SASL */\n+\n+static void\n+test_dbus_vnc_tls_authz_no_creds(void)\n+{\n+ const char *binary;\n+ g_autoptr(GError) err = NULL;\n+ g_autoptr(GSubprocess) proc = NULL;\n+ gboolean ok;\n+\n+ binary = g_getenv(\"QTEST_QEMU_VNC_BINARY\");\n+ if (!binary) {\n+ g_test_skip(\"QTEST_QEMU_VNC_BINARY not set\");\n+ return;\n+ }\n+\n+ proc = g_subprocess_new(G_SUBPROCESS_FLAGS_STDERR_SILENCE,\n+ &err,\n+ binary,\n+ \"--tls-authz\", \"authz0\",\n+ NULL);\n+ g_assert_no_error(err);\n+ g_assert_nonnull(proc);\n+\n+ ok = g_subprocess_wait(proc, NULL, &err);\n+ g_assert_no_error(err);\n+ g_assert_true(ok);\n+ g_assert_false(g_subprocess_get_successful(proc));\n+}\n+\n+#ifdef CONFIG_TASN1\n+#define CLIENT_CERT_CN \"qemu-vnc-test\"\n+\n+static char *\n+create_tls_certs(const char *dir)\n+{\n+ char *keyfile = g_strdup_printf(\"%s/key.pem\", dir);\n+ char *cacert = g_strdup_printf(\"%s/ca-cert.pem\", dir);\n+ char *servercert = g_strdup_printf(\"%s/server-cert.pem\", dir);\n+ char *serverkey = g_strdup_printf(\"%s/server-key.pem\", dir);\n+ char *clientcert = g_strdup_printf(\"%s/client-cert.pem\", dir);\n+\n+ test_tls_init(keyfile);\n+ g_assert(link(keyfile, serverkey) == 0);\n+\n+ TLS_ROOT_REQ_SIMPLE(cacertreq, cacert);\n+ TLS_CERT_REQ_SIMPLE_SERVER(servercertreq, cacertreq,\n+ servercert, \"localhost\", NULL);\n+ TLS_CERT_REQ_SIMPLE_CLIENT(clientcertreq, cacertreq,\n+ CLIENT_CERT_CN, clientcert);\n+\n+ test_tls_deinit_cert(&clientcertreq);\n+ test_tls_deinit_cert(&servercertreq);\n+ test_tls_deinit_cert(&cacertreq);\n+\n+ g_free(cacert);\n+ g_free(servercert);\n+ g_free(serverkey);\n+ g_free(clientcert);\n+ return keyfile;\n+}\n+\n+static void\n+cleanup_tls_certs(const char *dir, const char *keyfile)\n+{\n+ g_autofree char *cacert = g_strdup_printf(\"%s/ca-cert.pem\", dir);\n+ g_autofree char *servercert = g_strdup_printf(\"%s/server-cert.pem\", dir);\n+ g_autofree char *serverkey = g_strdup_printf(\"%s/server-key.pem\", dir);\n+ g_autofree char *clientcert = g_strdup_printf(\"%s/client-cert.pem\", dir);\n+\n+ unlink(cacert);\n+ unlink(servercert);\n+ unlink(serverkey);\n+ unlink(clientcert);\n+ unlink(keyfile);\n+ test_tls_cleanup(keyfile);\n+ rmdir(dir);\n+}\n+\n+/*\n+ * Do a minimal VNC/VeNCrypt negotiation on @fd up to the point where\n+ * the TLS handshake should begin, then perform a GnuTLS handshake\n+ * using the given credentials.\n+ */\n+static bool\n+try_raw_tls_connect(int fd, gnutls_certificate_credentials_t cred)\n+{\n+ char buf[13];\n+ uint8_t num_types, type;\n+ uint8_t vencrypt_ver[2], ack;\n+ uint8_t num_sub;\n+ uint32_t subtype;\n+ gnutls_session_t session;\n+ int ret;\n+ bool success;\n+\n+ /* RFB version exchange */\n+ g_assert_cmpint(read(fd, buf, 12), ==, 12);\n+ g_assert_cmpint(write(fd, \"RFB 003.008\\n\", 12), ==, 12);\n+\n+ /* Select VeNCrypt (type 19) from the auth list */\n+ g_assert_cmpint(read(fd, &num_types, 1), ==, 1);\n+ for (int i = 0; i < num_types; i++) {\n+ g_assert_cmpint(read(fd, &type, 1), ==, 1);\n+ }\n+ type = 19;\n+ g_assert_cmpint(write(fd, &type, 1), ==, 1);\n+\n+ /* VeNCrypt version exchange */\n+ g_assert_cmpint(read(fd, vencrypt_ver, 2), ==, 2);\n+ g_assert_cmpint(write(fd, vencrypt_ver, 2), ==, 2);\n+ g_assert_cmpint(read(fd, &ack, 1), ==, 1);\n+ g_assert_cmpint(ack, ==, 0);\n+\n+ /* Select x509-none (260) sub-auth */\n+ g_assert_cmpint(read(fd, &num_sub, 1), ==, 1);\n+ for (int i = 0; i < num_sub; i++) {\n+ g_assert_cmpint(read(fd, &subtype, 4), ==, 4);\n+ }\n+ subtype = htonl(260);\n+ g_assert_cmpint(write(fd, &subtype, 4), ==, 4);\n+\n+ /* Server sends 1-byte ack (1 = accepted) before TLS starts */\n+ g_assert_cmpint(read(fd, &ack, 1), ==, 1);\n+ g_assert_cmpint(ack, ==, 1);\n+\n+ /* TLS handshake */\n+ g_assert_cmpint(gnutls_init(&session, GNUTLS_CLIENT), >=, 0);\n+ g_assert_cmpint(\n+ gnutls_set_default_priority(session), >=, 0);\n+ g_assert_cmpint(\n+ gnutls_credentials_set(session, GNUTLS_CRD_CERTIFICATE, cred),\n+ >=, 0);\n+ gnutls_transport_set_int(session, fd);\n+\n+ do {\n+ ret = gnutls_handshake(session);\n+ } while (ret == GNUTLS_E_AGAIN || ret == GNUTLS_E_INTERRUPTED);\n+\n+ if (ret < 0) {\n+ success = false;\n+ } else {\n+ /*\n+ * Try reading the VNC security-result (4 bytes) — if the\n+ * server rejected us it will have closed the connection.\n+ */\n+ char tmp[4];\n+ do {\n+ ret = gnutls_record_recv(session, tmp, sizeof(tmp));\n+ } while (ret == GNUTLS_E_AGAIN || ret == GNUTLS_E_INTERRUPTED);\n+ success = (ret > 0);\n+ }\n+\n+ gnutls_deinit(session);\n+ return success;\n+}\n+\n+static void\n+test_dbus_vnc_tls_server_props(void)\n+{\n+ DbusTest dt = { 0 };\n+ QemuVnc1Server *proxy = NULL;\n+ g_autoptr(GError) err = NULL;\n+ g_autofree char *tls_dir = NULL;\n+ g_autofree char *keyfile = NULL;\n+\n+ if (!g_getenv(\"QTEST_QEMU_VNC_BINARY\")) {\n+ g_test_skip(\"QTEST_QEMU_VNC_BINARY not set\");\n+ return;\n+ }\n+\n+ tls_dir = g_dir_make_tmp(\"dbus-vnc-tls-XXXXXX\", NULL);\n+ g_assert_nonnull(tls_dir);\n+ keyfile = create_tls_certs(tls_dir);\n+\n+ {\n+ const char *extra_args[] = {\n+ \"--tls-creds\", tls_dir, NULL\n+ };\n+ if (!setup_dbus_test_full(&dt, extra_args)) {\n+ goto cleanup;\n+ }\n+ }\n+\n+ proxy = create_server_proxy(dt.bus_conn, &err);\n+ g_assert_no_error(err);\n+ g_assert_nonnull(proxy);\n+\n+ g_assert_cmpstr(qemu_vnc1_server_get_auth(proxy), ==, \"vencrypt\");\n+ g_assert_cmpstr(qemu_vnc1_server_get_vencrypt_sub_auth(proxy), ==,\n+ \"x509-none\");\n+\n+ /*\n+ * With verify-peer=no, a client without a certificate should\n+ * be able to connect successfully through TLS.\n+ */\n+ {\n+ g_autofree char *ca_path =\n+ g_strdup_printf(\"%s/ca-cert.pem\", tls_dir);\n+ gnutls_certificate_credentials_t cred;\n+ int fd;\n+\n+ g_assert_cmpint(\n+ gnutls_certificate_allocate_credentials(&cred), >=, 0);\n+ g_assert_cmpint(\n+ gnutls_certificate_set_x509_trust_file(\n+ cred, ca_path, GNUTLS_X509_FMT_PEM), >=, 0);\n+\n+ fd = wait_for_vnc_socket(dt.vnc_sock_path, VNC_TEST_TIMEOUT_MS);\n+ g_assert(fd >= 0);\n+ g_assert_true(try_raw_tls_connect(fd, cred));\n+ close(fd);\n+\n+ gnutls_certificate_free_credentials(cred);\n+ }\n+\n+cleanup:\n+ g_clear_object(&proxy);\n+ cleanup_dbus_test(&dt);\n+ cleanup_tls_certs(tls_dir, keyfile);\n+}\n+\n+static void\n+test_dbus_vnc_tls_authz(void)\n+{\n+ DbusTest dt = { 0 };\n+ g_autofree char *tls_dir = NULL;\n+ g_autofree char *keyfile = NULL;\n+ g_autofree char *ca_path = NULL;\n+\n+ if (!g_getenv(\"QTEST_QEMU_VNC_BINARY\")) {\n+ g_test_skip(\"QTEST_QEMU_VNC_BINARY not set\");\n+ return;\n+ }\n+\n+ tls_dir = g_dir_make_tmp(\"dbus-vnc-tls-XXXXXX\", NULL);\n+ g_assert_nonnull(tls_dir);\n+ keyfile = create_tls_certs(tls_dir);\n+\n+ /*\n+ * The client cert has CN=qemu-vnc-test, so the DN string\n+ * reported by GnuTLS is \"CN=qemu-vnc-test\". Configure\n+ * authz-simple to accept exactly that identity.\n+ */\n+ {\n+ g_autofree char *identity =\n+ g_strdup_printf(\"CN=%s\", CLIENT_CERT_CN);\n+ const char *extra_args[] = {\n+ \"--tls-creds\", tls_dir,\n+ \"--object\",\n+ NULL, /* filled below */\n+ \"--tls-authz\", \"authz0\",\n+ NULL\n+ };\n+ g_autofree char *object_arg =\n+ g_strdup_printf(\"authz-simple,id=authz0,identity=%s\", identity);\n+ extra_args[3] = object_arg;\n+\n+ if (!setup_dbus_test_full(&dt, extra_args)) {\n+ goto cleanup;\n+ }\n+ }\n+\n+ ca_path = g_strdup_printf(\"%s/ca-cert.pem\", tls_dir);\n+\n+ /*\n+ * Connect without a client certificate.\n+ * With verify-peer=yes the TLS handshake must fail.\n+ */\n+ {\n+ gnutls_certificate_credentials_t cred;\n+ int fd;\n+\n+ g_assert_cmpint(\n+ gnutls_certificate_allocate_credentials(&cred), >=, 0);\n+ g_assert_cmpint(\n+ gnutls_certificate_set_x509_trust_file(\n+ cred, ca_path, GNUTLS_X509_FMT_PEM), >=, 0);\n+\n+ fd = wait_for_vnc_socket(dt.vnc_sock_path, VNC_TEST_TIMEOUT_MS);\n+ g_assert(fd >= 0);\n+ g_assert_false(try_raw_tls_connect(fd, cred));\n+ close(fd);\n+\n+ gnutls_certificate_free_credentials(cred);\n+ }\n+\n+ /*\n+ * Connect with a valid client certificate whose DN\n+ * matches the authz-simple identity. This must succeed.\n+ */\n+ {\n+ g_autofree char *cert_path =\n+ g_strdup_printf(\"%s/client-cert.pem\", tls_dir);\n+ g_autofree char *key_path =\n+ g_strdup_printf(\"%s/key.pem\", tls_dir);\n+ gnutls_certificate_credentials_t cred;\n+ int fd;\n+\n+ g_assert_cmpint(\n+ gnutls_certificate_allocate_credentials(&cred), >=, 0);\n+ g_assert_cmpint(\n+ gnutls_certificate_set_x509_trust_file(\n+ cred, ca_path, GNUTLS_X509_FMT_PEM), >=, 0);\n+ g_assert_cmpint(\n+ gnutls_certificate_set_x509_key_file(\n+ cred, cert_path, key_path, GNUTLS_X509_FMT_PEM), >=, 0);\n+\n+ fd = wait_for_vnc_socket(dt.vnc_sock_path, VNC_TEST_TIMEOUT_MS);\n+ g_assert(fd >= 0);\n+ g_assert_true(try_raw_tls_connect(fd, cred));\n+ close(fd);\n+\n+ gnutls_certificate_free_credentials(cred);\n+ }\n+\n+cleanup:\n+ cleanup_dbus_test(&dt);\n+ cleanup_tls_certs(tls_dir, keyfile);\n+}\n+#endif /* CONFIG_TASN1 */\n+\n+int\n+main(int argc, char **argv)\n+{\n+ g_log_set_always_fatal(G_LOG_LEVEL_WARNING | G_LOG_LEVEL_CRITICAL);\n+\n+ if (getenv(\"GTK_VNC_DEBUG\")) {\n+ vnc_util_set_debug(true);\n+ }\n+\n+ g_test_init(&argc, &argv, NULL);\n+\n+ qtest_add_func(\"/dbus-vnc/basic\", test_dbus_vnc_basic);\n+ qtest_add_func(\"/dbus-vnc/server-props\", test_dbus_vnc_server_props);\n+ qtest_add_func(\"/dbus-vnc/client-lifecycle\", test_dbus_vnc_client_lifecycle);\n+ qtest_add_func(\"/dbus-vnc/no-password\", test_dbus_vnc_no_password);\n+ qtest_add_func(\"/dbus-vnc/password-auth\", test_dbus_vnc_password_auth);\n+ qtest_add_func(\"/dbus-vnc/sasl-authz-no-sasl\",\n+ test_dbus_vnc_sasl_authz_no_sasl);\n+#ifdef CONFIG_VNC_SASL\n+ qtest_add_func(\"/dbus-vnc/sasl-server-props\",\n+ test_dbus_vnc_sasl_server_props);\n+ qtest_add_func(\"/dbus-vnc/sasl-auth\",\n+ test_dbus_vnc_sasl_auth);\n+ qtest_add_func(\"/dbus-vnc/sasl-auth-bad-password\",\n+ test_dbus_vnc_sasl_auth_bad_password);\n+ qtest_add_func(\"/dbus-vnc/sasl-authz-denied\",\n+ test_dbus_vnc_sasl_authz_denied);\n+#endif\n+ qtest_add_func(\"/dbus-vnc/tls-authz-no-creds\",\n+ test_dbus_vnc_tls_authz_no_creds);\n+#ifdef CONFIG_TASN1\n+ qtest_add_func(\"/dbus-vnc/tls-server-props\",\n+ test_dbus_vnc_tls_server_props);\n+ qtest_add_func(\"/dbus-vnc/tls-authz\",\n+ test_dbus_vnc_tls_authz);\n+#endif\n+\n+ return g_test_run();\n+}\ndiff --git a/tools/qemu-vnc/audio.c b/tools/qemu-vnc/audio.c\nnew file mode 100644\nindex 00000000000..b55b04bc92a\n--- /dev/null\n+++ b/tools/qemu-vnc/audio.c\n@@ -0,0 +1,307 @@\n+/*\n+ * Standalone VNC server connecting to QEMU via D-Bus display interface.\n+ * Audio support. Only one audio stream is tracked. Mixing/resampling could be added.\n+ *\n+ * Copyright (C) 2026 Red Hat, Inc.\n+ *\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+\n+#include \"qemu/audio.h\"\n+#include \"qemu/audio-capture.h\"\n+#include \"qemu/sockets.h\"\n+#include \"qemu/error-report.h\"\n+#include \"ui/dbus-display1.h\"\n+#include \"trace.h\"\n+#include \"qemu-vnc.h\"\n+\n+struct CaptureVoiceOut {\n+ struct audsettings as;\n+ struct audio_capture_ops ops;\n+ void *opaque;\n+ QLIST_ENTRY(CaptureVoiceOut) entries;\n+};\n+\n+typedef struct AudioOut {\n+ guint64 id;\n+ struct audsettings as;\n+} AudioOut;\n+\n+static QLIST_HEAD(, CaptureVoiceOut) capture_list =\n+ QLIST_HEAD_INITIALIZER(capture_list);\n+static GDBusConnection *audio_listener_conn;\n+static AudioOut audio_out;\n+\n+static bool audsettings_eq(const struct audsettings *a,\n+ const struct audsettings *b)\n+{\n+ return a->freq == b->freq &&\n+ a->nchannels == b->nchannels &&\n+ a->fmt == b->fmt &&\n+ a->big_endian == b->big_endian;\n+}\n+\n+static gboolean\n+on_audio_out_init(QemuDBusDisplay1AudioOutListener *listener,\n+ GDBusMethodInvocation *invocation,\n+ guint64 id, guchar bits, gboolean is_signed,\n+ gboolean is_float, guint freq, guchar nchannels,\n+ guint bytes_per_frame, guint bytes_per_second,\n+ gboolean be, gpointer user_data)\n+{\n+ AudioFormat fmt;\n+\n+ switch (bits) {\n+ case 8:\n+ fmt = is_signed ? AUDIO_FORMAT_S8 : AUDIO_FORMAT_U8;\n+ break;\n+ case 16:\n+ fmt = is_signed ? AUDIO_FORMAT_S16 : AUDIO_FORMAT_U16;\n+ break;\n+ case 32:\n+ fmt = is_float ? AUDIO_FORMAT_F32 :\n+ is_signed ? AUDIO_FORMAT_S32 : AUDIO_FORMAT_U32;\n+ break;\n+ default:\n+ g_return_val_if_reached(DBUS_METHOD_INVOCATION_HANDLED);\n+ }\n+\n+ struct audsettings as = {\n+ .freq = freq,\n+ .nchannels = nchannels,\n+ .fmt = fmt,\n+ .big_endian = be,\n+ };\n+ audio_out = (AudioOut) {\n+ .id = id,\n+ .as = as,\n+ };\n+\n+ trace_qemu_vnc_audio_out_init(id, freq, nchannels, bits);\n+\n+ qemu_dbus_display1_audio_out_listener_complete_init(\n+ listener, invocation);\n+ return DBUS_METHOD_INVOCATION_HANDLED;\n+}\n+\n+static gboolean\n+on_audio_out_fini(QemuDBusDisplay1AudioOutListener *listener,\n+ GDBusMethodInvocation *invocation,\n+ guint64 id, gpointer user_data)\n+{\n+ trace_qemu_vnc_audio_out_fini(id);\n+\n+ qemu_dbus_display1_audio_out_listener_complete_fini(\n+ listener, invocation);\n+ return DBUS_METHOD_INVOCATION_HANDLED;\n+}\n+\n+static gboolean\n+on_audio_out_set_enabled(QemuDBusDisplay1AudioOutListener *listener,\n+ GDBusMethodInvocation *invocation,\n+ guint64 id, gboolean enabled,\n+ gpointer user_data)\n+{\n+ CaptureVoiceOut *cap;\n+\n+ trace_qemu_vnc_audio_out_set_enabled(id, enabled);\n+\n+ if (id == audio_out.id) {\n+ QLIST_FOREACH(cap, &capture_list, entries) {\n+ cap->ops.notify(cap->opaque,\n+ enabled ? AUD_CNOTIFY_ENABLE\n+ : AUD_CNOTIFY_DISABLE);\n+ }\n+ }\n+\n+ qemu_dbus_display1_audio_out_listener_complete_set_enabled(\n+ listener, invocation);\n+ return DBUS_METHOD_INVOCATION_HANDLED;\n+}\n+\n+static gboolean\n+on_audio_out_set_volume(QemuDBusDisplay1AudioOutListener *listener,\n+ GDBusMethodInvocation *invocation,\n+ guint64 id, gboolean mute,\n+ GVariant *volume, gpointer user_data)\n+{\n+ qemu_dbus_display1_audio_out_listener_complete_set_volume(\n+ listener, invocation);\n+ return DBUS_METHOD_INVOCATION_HANDLED;\n+}\n+\n+static gboolean\n+on_audio_out_write(QemuDBusDisplay1AudioOutListener *listener,\n+ GDBusMethodInvocation *invocation,\n+ guint64 id, GVariant *data,\n+ gpointer user_data)\n+{\n+ CaptureVoiceOut *cap;\n+ gsize size;\n+ const void *buf;\n+\n+ if (id == audio_out.id) {\n+ buf = g_variant_get_fixed_array(data, &size, 1);\n+\n+ trace_qemu_vnc_audio_out_write(id, size);\n+\n+ QLIST_FOREACH(cap, &capture_list, entries) {\n+ /* we don't handle audio resampling/format conversion */\n+ if (audsettings_eq(&cap->as, &audio_out.as)) {\n+ cap->ops.capture(cap->opaque, buf, size);\n+ }\n+ }\n+ }\n+\n+ qemu_dbus_display1_audio_out_listener_complete_write(\n+ listener, invocation);\n+ return DBUS_METHOD_INVOCATION_HANDLED;\n+}\n+\n+CaptureVoiceOut *audio_be_add_capture(\n+ AudioBackend *be,\n+ const struct audsettings *as,\n+ const struct audio_capture_ops *ops,\n+ void *opaque)\n+{\n+ CaptureVoiceOut *cap;\n+\n+ if (!audio_listener_conn) {\n+ return NULL;\n+ }\n+\n+ cap = g_new0(CaptureVoiceOut, 1);\n+ cap->ops = *ops;\n+ cap->opaque = opaque;\n+ cap->as = *as;\n+ QLIST_INSERT_HEAD(&capture_list, cap, entries);\n+\n+ return cap;\n+}\n+\n+void audio_be_del_capture(\n+ AudioBackend *be,\n+ CaptureVoiceOut *cap,\n+ void *cb_opaque)\n+{\n+ if (!cap) {\n+ return;\n+ }\n+\n+ cap->ops.destroy(cap->opaque);\n+ QLIST_REMOVE(cap, entries);\n+ g_free(cap);\n+}\n+\n+/*\n+ * Dummy audio backend — the VNC server only needs a non-NULL pointer\n+ * so that audio capture registration doesn't bail out. The pointer\n+ * is never dereferenced by our code (audio_be_add_capture ignores it).\n+ */\n+static AudioBackend dummy_audio_be;\n+\n+AudioBackend *audio_get_default_audio_be(Error **errp)\n+{\n+ return &dummy_audio_be;\n+}\n+\n+AudioBackend *audio_be_by_name(const char *name, Error **errp)\n+{\n+ return NULL;\n+}\n+\n+static void\n+on_register_audio_listener_finished(GObject *source_object,\n+ GAsyncResult *res,\n+ gpointer user_data)\n+{\n+ GThread *thread = user_data;\n+ g_autoptr(GError) err = NULL;\n+ g_autoptr(GDBusObjectSkeleton) obj = NULL;\n+ GDBusObjectManagerServer *server;\n+ QemuDBusDisplay1AudioOutListener *audio_skel;\n+\n+ qemu_dbus_display1_audio_call_register_out_listener_finish(\n+ QEMU_DBUS_DISPLAY1_AUDIO(source_object),\n+ NULL, res, &err);\n+\n+ if (err) {\n+ error_report(\"RegisterOutListener failed: %s\", err->message);\n+ g_thread_join(thread);\n+ return;\n+ }\n+\n+ audio_listener_conn = g_thread_join(thread);\n+ if (!audio_listener_conn) {\n+ return;\n+ }\n+\n+ server = g_dbus_object_manager_server_new(DBUS_DISPLAY1_ROOT);\n+ obj = g_dbus_object_skeleton_new(\n+ DBUS_DISPLAY1_ROOT \"/AudioOutListener\");\n+\n+ audio_skel = qemu_dbus_display1_audio_out_listener_skeleton_new();\n+ g_object_connect(audio_skel,\n+ \"signal::handle-init\",\n+ on_audio_out_init, NULL,\n+ \"signal::handle-fini\",\n+ on_audio_out_fini, NULL,\n+ \"signal::handle-set-enabled\",\n+ on_audio_out_set_enabled, NULL,\n+ \"signal::handle-set-volume\",\n+ on_audio_out_set_volume, NULL,\n+ \"signal::handle-write\",\n+ on_audio_out_write, NULL,\n+ NULL);\n+ g_dbus_object_skeleton_add_interface(\n+ obj, G_DBUS_INTERFACE_SKELETON(audio_skel));\n+\n+ g_dbus_object_manager_server_export(server, obj);\n+ g_dbus_object_manager_server_set_connection(\n+ server, audio_listener_conn);\n+\n+ g_dbus_connection_start_message_processing(audio_listener_conn);\n+}\n+\n+void audio_setup(GDBusObjectManager *manager)\n+{\n+ g_autoptr(GError) err = NULL;\n+ g_autoptr(GUnixFDList) fd_list = NULL;\n+ g_autoptr(GDBusInterface) iface = NULL;\n+ GThread *thread;\n+ int pair[2];\n+ int idx;\n+\n+ iface = g_dbus_object_manager_get_interface(\n+ manager, DBUS_DISPLAY1_ROOT \"/Audio\",\n+ \"org.qemu.Display1.Audio\");\n+ if (!iface) {\n+ return;\n+ }\n+\n+ if (qemu_socketpair(AF_UNIX, SOCK_STREAM, 0, pair) < 0) {\n+ error_report(\"audio socketpair failed: %s\", strerror(errno));\n+ return;\n+ }\n+\n+ fd_list = g_unix_fd_list_new();\n+ idx = g_unix_fd_list_append(fd_list, pair[1], &err);\n+ close(pair[1]);\n+ if (idx < 0) {\n+ close(pair[0]);\n+ error_report(\"Failed to append fd: %s\", err->message);\n+ return;\n+ }\n+\n+ thread = p2p_dbus_thread_new(pair[0]);\n+\n+ qemu_dbus_display1_audio_call_register_out_listener(\n+ QEMU_DBUS_DISPLAY1_AUDIO(iface),\n+ g_variant_new_handle(idx),\n+ G_DBUS_CALL_FLAGS_NONE, -1,\n+ fd_list, NULL,\n+ on_register_audio_listener_finished,\n+ thread);\n+}\ndiff --git a/tools/qemu-vnc/chardev.c b/tools/qemu-vnc/chardev.c\nnew file mode 100644\nindex 00000000000..d9d51973724\n--- /dev/null\n+++ b/tools/qemu-vnc/chardev.c\n@@ -0,0 +1,127 @@\n+/*\n+ * Standalone VNC server connecting to QEMU via D-Bus display interface.\n+ *\n+ * Copyright (C) 2026 Red Hat, Inc.\n+ *\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+\n+#include \"qemu/sockets.h\"\n+#include \"qemu/error-report.h\"\n+#include \"ui/dbus-display1.h\"\n+#include \"trace.h\"\n+#include \"qemu-vnc.h\"\n+\n+typedef struct ChardevRegisterData {\n+ QemuDBusDisplay1Chardev *proxy;\n+ int local_fd;\n+ char *name;\n+ bool echo;\n+} ChardevRegisterData;\n+\n+static void\n+on_chardev_register_finished(GObject *source_object,\n+ GAsyncResult *res,\n+ gpointer user_data)\n+{\n+ ChardevRegisterData *data = user_data;\n+ g_autoptr(GError) err = NULL;\n+ QemuTextConsole *tc;\n+\n+ if (!qemu_dbus_display1_chardev_call_register_finish(\n+ data->proxy, NULL, res, &err)) {\n+ error_report(\"Chardev Register failed for %s: %s\",\n+ data->name, err->message);\n+ close(data->local_fd);\n+ goto out;\n+ }\n+\n+ tc = qemu_vnc_text_console_new(data->name, data->local_fd, data->echo);\n+ if (!tc) {\n+ close(data->local_fd);\n+ goto out;\n+ }\n+\n+ trace_qemu_vnc_chardev_connected(data->name);\n+\n+out:\n+ g_object_unref(data->proxy);\n+ g_free(data->name);\n+ g_free(data);\n+}\n+\n+/* Default chardevs to expose as VNC text consoles */\n+static const char * const default_names[] = {\n+ \"org.qemu.console.serial.0\",\n+ \"org.qemu.monitor.hmp.0\",\n+ NULL,\n+};\n+\n+/* Active chardev names list (points to CLI args or default_names) */\n+static const char * const *names;\n+\n+static void\n+chardev_register(QemuDBusDisplay1Chardev *proxy)\n+{\n+ g_autoptr(GUnixFDList) fd_list = NULL;\n+ ChardevRegisterData *data;\n+ const char *name;\n+ int pair[2];\n+ int idx;\n+\n+ name = qemu_dbus_display1_chardev_get_name(proxy);\n+ if (!name || !g_strv_contains(names, name)) {\n+ return;\n+ }\n+\n+ if (qemu_socketpair(AF_UNIX, SOCK_STREAM, 0, pair) < 0) {\n+ error_report(\"chardev socketpair failed: %s\", strerror(errno));\n+ return;\n+ }\n+\n+ fd_list = g_unix_fd_list_new();\n+ idx = g_unix_fd_list_append(fd_list, pair[1], NULL);\n+ close(pair[1]);\n+\n+ data = g_new0(ChardevRegisterData, 1);\n+ data->proxy = g_object_ref(proxy);\n+ data->local_fd = pair[0];\n+ data->name = g_strdup(name);\n+ data->echo = qemu_dbus_display1_chardev_get_echo(proxy);\n+\n+ qemu_dbus_display1_chardev_call_register(\n+ proxy, g_variant_new_handle(idx),\n+ G_DBUS_CALL_FLAGS_NONE, -1,\n+ fd_list, NULL,\n+ on_chardev_register_finished, data);\n+}\n+\n+void chardev_setup(const char * const *chardev_names,\n+ GDBusObjectManager *manager)\n+{\n+ GList *objects, *l;\n+\n+ names = chardev_names ? chardev_names : default_names;\n+\n+ objects = g_dbus_object_manager_get_objects(manager);\n+ for (l = objects; l; l = l->next) {\n+ GDBusObject *obj = l->data;\n+ const char *path = g_dbus_object_get_object_path(obj);\n+ g_autoptr(GDBusInterface) iface = NULL;\n+\n+ if (!g_str_has_prefix(path, DBUS_DISPLAY1_ROOT \"/Chardev_\")) {\n+ continue;\n+ }\n+\n+ iface = g_dbus_object_get_interface(\n+ obj, \"org.qemu.Display1.Chardev\");\n+ if (!iface) {\n+ continue;\n+ }\n+\n+ chardev_register(QEMU_DBUS_DISPLAY1_CHARDEV(iface));\n+ }\n+ g_list_free_full(objects, g_object_unref);\n+}\ndiff --git a/tools/qemu-vnc/clipboard.c b/tools/qemu-vnc/clipboard.c\nnew file mode 100644\nindex 00000000000..d1673b97899\n--- /dev/null\n+++ b/tools/qemu-vnc/clipboard.c\n@@ -0,0 +1,378 @@\n+/*\n+ * Standalone VNC server connecting to QEMU via D-Bus display interface.\n+ *\n+ * Copyright (C) 2026 Red Hat, Inc.\n+ *\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+\n+#include \"qemu/error-report.h\"\n+#include \"ui/clipboard.h\"\n+#include \"ui/dbus-display1.h\"\n+#include \"trace.h\"\n+#include \"qemu-vnc.h\"\n+\n+#define MIME_TEXT_PLAIN_UTF8 \"text/plain;charset=utf-8\"\n+\n+typedef struct {\n+ GDBusMethodInvocation *invocation;\n+ QemuClipboardType type;\n+ guint timeout_id;\n+} VncDBusClipboardRequest;\n+\n+static QemuDBusDisplay1Clipboard *clipboard_proxy;\n+static QemuDBusDisplay1Clipboard *clipboard_skel;\n+static QemuClipboardPeer clipboard_peer;\n+static uint32_t clipboard_serial;\n+static VncDBusClipboardRequest\n+ clipboard_request[QEMU_CLIPBOARD_SELECTION__COUNT];\n+\n+static void\n+vnc_dbus_clipboard_complete_request(\n+ GDBusMethodInvocation *invocation,\n+ QemuClipboardInfo *info,\n+ QemuClipboardType type)\n+{\n+ GVariant *v_data = g_variant_new_from_data(\n+ G_VARIANT_TYPE(\"ay\"),\n+ info->types[type].data,\n+ info->types[type].size,\n+ TRUE,\n+ (GDestroyNotify)qemu_clipboard_info_unref,\n+ qemu_clipboard_info_ref(info));\n+\n+ qemu_dbus_display1_clipboard_complete_request(\n+ clipboard_skel, invocation,\n+ MIME_TEXT_PLAIN_UTF8, v_data);\n+}\n+\n+static void\n+vnc_dbus_clipboard_request_cancelled(VncDBusClipboardRequest *req)\n+{\n+ if (!req->invocation) {\n+ return;\n+ }\n+\n+ g_dbus_method_invocation_return_error(\n+ req->invocation,\n+ G_DBUS_ERROR,\n+ G_DBUS_ERROR_FAILED,\n+ \"Cancelled clipboard request\");\n+\n+ g_clear_object(&req->invocation);\n+ g_source_remove(req->timeout_id);\n+ req->timeout_id = 0;\n+}\n+\n+static gboolean\n+vnc_dbus_clipboard_request_timeout(gpointer user_data)\n+{\n+ vnc_dbus_clipboard_request_cancelled(user_data);\n+ return G_SOURCE_REMOVE;\n+}\n+\n+static void\n+vnc_dbus_clipboard_request(QemuClipboardInfo *info,\n+ QemuClipboardType type)\n+{\n+ g_autofree char *mime = NULL;\n+ g_autoptr(GVariant) v_data = NULL;\n+ g_autoptr(GError) err = NULL;\n+ const char *data = NULL;\n+ const char *mimes[] = { MIME_TEXT_PLAIN_UTF8, NULL };\n+ size_t n;\n+\n+ if (type != QEMU_CLIPBOARD_TYPE_TEXT) {\n+ return;\n+ }\n+\n+ if (!clipboard_proxy) {\n+ return;\n+ }\n+\n+ if (!qemu_dbus_display1_clipboard_call_request_sync(\n+ clipboard_proxy,\n+ info->selection,\n+ mimes,\n+ G_DBUS_CALL_FLAGS_NONE, -1, &mime, &v_data, NULL, &err)) {\n+ error_report(\"Failed to request clipboard: %s\", err->message);\n+ return;\n+ }\n+\n+ if (!g_str_equal(mime, MIME_TEXT_PLAIN_UTF8)) {\n+ error_report(\"Unsupported returned MIME: %s\", mime);\n+ return;\n+ }\n+\n+ data = g_variant_get_fixed_array(v_data, &n, 1);\n+ qemu_clipboard_set_data(&clipboard_peer, info, type,\n+ n, data, true);\n+}\n+\n+static void\n+vnc_dbus_clipboard_update_info(QemuClipboardInfo *info)\n+{\n+ bool self_update = info->owner == &clipboard_peer;\n+ const char *mime[QEMU_CLIPBOARD_TYPE__COUNT + 1] = { 0, };\n+ VncDBusClipboardRequest *req;\n+ int i = 0;\n+\n+ if (info->owner == NULL) {\n+ if (clipboard_proxy) {\n+ qemu_dbus_display1_clipboard_call_release(\n+ clipboard_proxy,\n+ info->selection,\n+ G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);\n+ }\n+ return;\n+ }\n+\n+ if (self_update) {\n+ return;\n+ }\n+\n+ req = &clipboard_request[info->selection];\n+ if (req->invocation && info->types[req->type].data) {\n+ vnc_dbus_clipboard_complete_request(\n+ req->invocation, info, req->type);\n+ g_clear_object(&req->invocation);\n+ g_source_remove(req->timeout_id);\n+ req->timeout_id = 0;\n+ return;\n+ }\n+\n+ if (info->types[QEMU_CLIPBOARD_TYPE_TEXT].available) {\n+ mime[i++] = MIME_TEXT_PLAIN_UTF8;\n+ }\n+\n+ if (i > 0 && clipboard_proxy) {\n+ uint32_t serial = info->has_serial ?\n+ info->serial : ++clipboard_serial;\n+ qemu_dbus_display1_clipboard_call_grab(\n+ clipboard_proxy,\n+ info->selection,\n+ serial,\n+ mime,\n+ G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);\n+ }\n+}\n+\n+static void\n+vnc_dbus_clipboard_notify(Notifier *notifier, void *data)\n+{\n+ QemuClipboardNotify *notify = data;\n+\n+ switch (notify->type) {\n+ case QEMU_CLIPBOARD_UPDATE_INFO:\n+ vnc_dbus_clipboard_update_info(notify->info);\n+ return;\n+ case QEMU_CLIPBOARD_RESET_SERIAL:\n+ if (clipboard_proxy) {\n+ qemu_dbus_display1_clipboard_call_register(\n+ clipboard_proxy,\n+ G_DBUS_CALL_FLAGS_NONE,\n+ -1, NULL, NULL, NULL);\n+ }\n+ return;\n+ }\n+}\n+\n+static gboolean\n+on_clipboard_register(QemuDBusDisplay1Clipboard *clipboard,\n+ GDBusMethodInvocation *invocation,\n+ gpointer user_data)\n+{\n+ clipboard_serial = 0;\n+ qemu_clipboard_reset_serial();\n+\n+ qemu_dbus_display1_clipboard_complete_register(\n+ clipboard, invocation);\n+ return DBUS_METHOD_INVOCATION_HANDLED;\n+}\n+\n+static gboolean\n+on_clipboard_unregister(QemuDBusDisplay1Clipboard *clipboard,\n+ GDBusMethodInvocation *invocation,\n+ gpointer user_data)\n+{\n+ int i;\n+\n+ for (i = 0; i < G_N_ELEMENTS(clipboard_request); ++i) {\n+ vnc_dbus_clipboard_request_cancelled(&clipboard_request[i]);\n+ }\n+\n+ qemu_dbus_display1_clipboard_complete_unregister(\n+ clipboard, invocation);\n+ return DBUS_METHOD_INVOCATION_HANDLED;\n+}\n+\n+static gboolean\n+on_clipboard_grab(QemuDBusDisplay1Clipboard *clipboard,\n+ GDBusMethodInvocation *invocation,\n+ gint arg_selection,\n+ guint arg_serial,\n+ const gchar *const *arg_mimes,\n+ gpointer user_data)\n+{\n+ QemuClipboardSelection s = arg_selection;\n+ g_autoptr(QemuClipboardInfo) info = NULL;\n+\n+ if (s >= QEMU_CLIPBOARD_SELECTION__COUNT) {\n+ g_dbus_method_invocation_return_error(\n+ invocation,\n+ G_DBUS_ERROR,\n+ G_DBUS_ERROR_FAILED,\n+ \"Invalid clipboard selection: %d\", arg_selection);\n+ return DBUS_METHOD_INVOCATION_HANDLED;\n+ }\n+\n+ trace_qemu_vnc_clipboard_grab(arg_selection, arg_serial);\n+\n+ info = qemu_clipboard_info_new(&clipboard_peer, s);\n+ if (g_strv_contains(arg_mimes, MIME_TEXT_PLAIN_UTF8)) {\n+ info->types[QEMU_CLIPBOARD_TYPE_TEXT].available = true;\n+ }\n+ info->serial = arg_serial;\n+ info->has_serial = true;\n+ if (qemu_clipboard_check_serial(info, true)) {\n+ qemu_clipboard_update(info);\n+ }\n+\n+ qemu_dbus_display1_clipboard_complete_grab(\n+ clipboard, invocation);\n+ return DBUS_METHOD_INVOCATION_HANDLED;\n+}\n+\n+static gboolean\n+on_clipboard_release(QemuDBusDisplay1Clipboard *clipboard,\n+ GDBusMethodInvocation *invocation,\n+ gint arg_selection,\n+ gpointer user_data)\n+{\n+ trace_qemu_vnc_clipboard_release(arg_selection);\n+\n+ qemu_clipboard_peer_release(&clipboard_peer, arg_selection);\n+\n+ qemu_dbus_display1_clipboard_complete_release(\n+ clipboard, invocation);\n+ return DBUS_METHOD_INVOCATION_HANDLED;\n+}\n+\n+static gboolean\n+on_clipboard_request(QemuDBusDisplay1Clipboard *clipboard,\n+ GDBusMethodInvocation *invocation,\n+ gint arg_selection,\n+ const gchar *const *arg_mimes,\n+ gpointer user_data)\n+{\n+ QemuClipboardSelection s = arg_selection;\n+ QemuClipboardType type = QEMU_CLIPBOARD_TYPE_TEXT;\n+ QemuClipboardInfo *info = NULL;\n+\n+ trace_qemu_vnc_clipboard_request(arg_selection);\n+\n+ if (s >= QEMU_CLIPBOARD_SELECTION__COUNT) {\n+ g_dbus_method_invocation_return_error(\n+ invocation,\n+ G_DBUS_ERROR,\n+ G_DBUS_ERROR_FAILED,\n+ \"Invalid clipboard selection: %d\", arg_selection);\n+ return DBUS_METHOD_INVOCATION_HANDLED;\n+ }\n+\n+ if (clipboard_request[s].invocation) {\n+ g_dbus_method_invocation_return_error(\n+ invocation,\n+ G_DBUS_ERROR,\n+ G_DBUS_ERROR_FAILED,\n+ \"Pending request\");\n+ return DBUS_METHOD_INVOCATION_HANDLED;\n+ }\n+\n+ info = qemu_clipboard_info(s);\n+ if (!info || !info->owner || info->owner == &clipboard_peer) {\n+ g_dbus_method_invocation_return_error(\n+ invocation,\n+ G_DBUS_ERROR,\n+ G_DBUS_ERROR_FAILED,\n+ \"Empty clipboard\");\n+ return DBUS_METHOD_INVOCATION_HANDLED;\n+ }\n+\n+ if (!g_strv_contains(arg_mimes, MIME_TEXT_PLAIN_UTF8) ||\n+ !info->types[type].available) {\n+ g_dbus_method_invocation_return_error(\n+ invocation,\n+ G_DBUS_ERROR,\n+ G_DBUS_ERROR_FAILED,\n+ \"Unhandled MIME types requested\");\n+ return DBUS_METHOD_INVOCATION_HANDLED;\n+ }\n+\n+ if (info->types[type].data) {\n+ vnc_dbus_clipboard_complete_request(invocation, info, type);\n+ } else {\n+ qemu_clipboard_request(info, type);\n+\n+ clipboard_request[s].invocation = g_object_ref(invocation);\n+ clipboard_request[s].type = type;\n+ clipboard_request[s].timeout_id =\n+ g_timeout_add_seconds(5,\n+ vnc_dbus_clipboard_request_timeout,\n+ &clipboard_request[s]);\n+ }\n+\n+ return DBUS_METHOD_INVOCATION_HANDLED;\n+}\n+\n+void clipboard_setup(GDBusObjectManager *manager, GDBusConnection *bus)\n+{\n+ g_autoptr(GError) err = NULL;\n+ g_autoptr(GDBusInterface) iface = NULL;\n+\n+ iface = g_dbus_object_manager_get_interface(\n+ manager, DBUS_DISPLAY1_ROOT \"/Clipboard\",\n+ \"org.qemu.Display1.Clipboard\");\n+ if (!iface) {\n+ return;\n+ }\n+\n+ clipboard_proxy = g_object_ref(QEMU_DBUS_DISPLAY1_CLIPBOARD(iface));\n+\n+ clipboard_skel = qemu_dbus_display1_clipboard_skeleton_new();\n+ g_object_connect(clipboard_skel,\n+ \"signal::handle-register\",\n+ on_clipboard_register, NULL,\n+ \"signal::handle-unregister\",\n+ on_clipboard_unregister, NULL,\n+ \"signal::handle-grab\",\n+ on_clipboard_grab, NULL,\n+ \"signal::handle-release\",\n+ on_clipboard_release, NULL,\n+ \"signal::handle-request\",\n+ on_clipboard_request, NULL,\n+ NULL);\n+\n+ if (!g_dbus_interface_skeleton_export(\n+ G_DBUS_INTERFACE_SKELETON(clipboard_skel),\n+ bus,\n+ DBUS_DISPLAY1_ROOT \"/Clipboard\",\n+ &err)) {\n+ error_report(\"Failed to export clipboard: %s\", err->message);\n+ g_clear_object(&clipboard_skel);\n+ g_clear_object(&clipboard_proxy);\n+ return;\n+ }\n+\n+ clipboard_peer.name = \"dbus\";\n+ clipboard_peer.notifier.notify = vnc_dbus_clipboard_notify;\n+ clipboard_peer.request = vnc_dbus_clipboard_request;\n+ qemu_clipboard_peer_register(&clipboard_peer);\n+\n+ qemu_dbus_display1_clipboard_call_register(\n+ clipboard_proxy,\n+ G_DBUS_CALL_FLAGS_NONE,\n+ -1, NULL, NULL, NULL);\n+}\ndiff --git a/tools/qemu-vnc/console.c b/tools/qemu-vnc/console.c\nnew file mode 100644\nindex 00000000000..076365adf77\n--- /dev/null\n+++ b/tools/qemu-vnc/console.c\n@@ -0,0 +1,168 @@\n+/*\n+ * Minimal QemuConsole helpers for the standalone qemu-vnc binary.\n+ *\n+ * Copyright (C) 2026 Red Hat, Inc.\n+ *\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+\n+#include \"ui/console.h\"\n+#include \"ui/console-priv.h\"\n+#include \"ui/vt100.h\"\n+#include \"qemu-vnc.h\"\n+#include \"trace.h\"\n+\n+/*\n+ * Our own QemuTextConsole definition — the one in console-vc.c uses\n+ * a Chardev* backend which is not available in the standalone binary.\n+ * Here we drive the VT100 emulator directly over a raw file descriptor.\n+ */\n+typedef struct QemuTextConsole {\n+ QemuConsole parent;\n+ QemuVT100 vt;\n+ int chardev_fd;\n+ guint io_watch_id;\n+ char *name;\n+} QemuTextConsole;\n+\n+typedef QemuConsoleClass QemuTextConsoleClass;\n+\n+OBJECT_DEFINE_TYPE(QemuTextConsole, qemu_text_console,\n+ QEMU_TEXT_CONSOLE, QEMU_CONSOLE)\n+\n+static void qemu_text_console_class_init(ObjectClass *oc, const void *data)\n+{\n+}\n+\n+static void text_console_invalidate(void *opaque)\n+{\n+ QemuTextConsole *s = QEMU_TEXT_CONSOLE(opaque);\n+\n+ vt100_set_image(&s->vt, QEMU_CONSOLE(s)->surface->image);\n+ vt100_refresh(&s->vt);\n+}\n+\n+static const GraphicHwOps text_console_ops = {\n+ .invalidate = text_console_invalidate,\n+};\n+\n+static void qemu_text_console_init(Object *obj)\n+{\n+ QemuTextConsole *c = QEMU_TEXT_CONSOLE(obj);\n+\n+ QEMU_CONSOLE(c)->hw_ops = &text_console_ops;\n+ QEMU_CONSOLE(c)->hw = c;\n+}\n+\n+static void qemu_text_console_finalize(Object *obj)\n+{\n+ QemuTextConsole *tc = QEMU_TEXT_CONSOLE(obj);\n+\n+ vt100_fini(&tc->vt);\n+ if (tc->io_watch_id) {\n+ g_source_remove(tc->io_watch_id);\n+ }\n+ if (tc->chardev_fd >= 0) {\n+ close(tc->chardev_fd);\n+ }\n+ g_free(tc->name);\n+}\n+\n+\n+static void text_console_out_flush(QemuVT100 *vt)\n+{\n+ QemuTextConsole *tc = container_of(vt, QemuTextConsole, vt);\n+ const uint8_t *data;\n+ uint32_t len;\n+\n+ while (!fifo8_is_empty(&vt->out_fifo)) {\n+ ssize_t ret;\n+\n+ data = fifo8_pop_bufptr(&vt->out_fifo,\n+ fifo8_num_used(&vt->out_fifo), &len);\n+ ret = write(tc->chardev_fd, data, len);\n+ if (ret < 0) {\n+ trace_qemu_vnc_console_io_error(tc->name);\n+ break;\n+ }\n+ }\n+}\n+\n+static void text_console_image_update(QemuVT100 *vt, int x, int y, int w, int h)\n+{\n+ QemuTextConsole *tc = container_of(vt, QemuTextConsole, vt);\n+ QemuConsole *con = QEMU_CONSOLE(tc);\n+\n+ qemu_console_update(con, x, y, w, h);\n+}\n+\n+static gboolean text_console_io_cb(GIOChannel *source,\n+ GIOCondition cond, gpointer data)\n+{\n+ QemuTextConsole *tc = data;\n+ uint8_t buf[4096];\n+ ssize_t n;\n+\n+ if (cond & (G_IO_HUP | G_IO_ERR)) {\n+ tc->io_watch_id = 0;\n+ return G_SOURCE_REMOVE;\n+ }\n+\n+ n = read(tc->chardev_fd, buf, sizeof(buf));\n+ if (n <= 0) {\n+ trace_qemu_vnc_console_io_error(tc->name);\n+ tc->io_watch_id = 0;\n+ return G_SOURCE_REMOVE;\n+ }\n+\n+ vt100_input(&tc->vt, buf, n);\n+ return G_SOURCE_CONTINUE;\n+}\n+\n+QemuTextConsole *qemu_vnc_text_console_new(const char *name,\n+ int fd, bool echo)\n+{\n+ int w = TEXT_COLS * TEXT_FONT_WIDTH;\n+ int h = TEXT_ROWS * TEXT_FONT_HEIGHT;\n+ QemuTextConsole *tc;\n+ QemuConsole *con;\n+ pixman_image_t *image;\n+ GIOChannel *chan;\n+\n+ tc = QEMU_TEXT_CONSOLE(object_new(TYPE_QEMU_TEXT_CONSOLE));\n+ con = QEMU_CONSOLE(tc);\n+\n+ tc->name = g_strdup(name);\n+ tc->chardev_fd = fd;\n+\n+ image = pixman_image_create_bits(PIXMAN_x8r8g8b8, w, h, NULL, 0);\n+ con->surface = qemu_create_displaysurface_pixman(image);\n+ con->scanout.kind = SCANOUT_SURFACE;\n+ qemu_pixman_image_unref(image);\n+\n+ vt100_init(&tc->vt, con->surface->image,\n+ text_console_image_update, text_console_out_flush);\n+ tc->vt.echo = echo;\n+ vt100_refresh(&tc->vt);\n+\n+ chan = g_io_channel_unix_new(fd);\n+ g_io_channel_set_encoding(chan, NULL, NULL);\n+ tc->io_watch_id = g_io_add_watch(chan,\n+ G_IO_IN | G_IO_HUP | G_IO_ERR,\n+ text_console_io_cb, tc);\n+ g_io_channel_unref(chan);\n+\n+ return tc;\n+}\n+\n+void qemu_text_console_handle_keysym(QemuTextConsole *s, int keysym)\n+{\n+ vt100_keysym(&s->vt, keysym);\n+}\n+\n+void qemu_text_console_update_size(QemuTextConsole *c)\n+{\n+ qemu_console_text_resize(QEMU_CONSOLE(c), c->vt.width, c->vt.height);\n+}\ndiff --git a/tools/qemu-vnc/dbus.c b/tools/qemu-vnc/dbus.c\nnew file mode 100644\nindex 00000000000..0e5f52623ea\n--- /dev/null\n+++ b/tools/qemu-vnc/dbus.c\n@@ -0,0 +1,439 @@\n+/*\n+ * D-Bus interface for qemu-vnc standalone VNC server.\n+ *\n+ * Copyright (C) 2026 Red Hat, Inc.\n+ *\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+\n+#include \"qemu/cutils.h\"\n+#include \"qapi-types-trace.h\"\n+#include \"system/system.h\"\n+#include \"qapi/qapi-types-ui.h\"\n+#include \"qapi/qapi-commands-ui.h\"\n+#include \"qemu-vnc.h\"\n+#include \"qemu-vnc1.h\"\n+#include \"qapi/qapi-emit-events.h\"\n+#include \"qobject/qdict.h\"\n+#include \"ui/vnc.h\"\n+#include \"trace.h\"\n+\n+typedef struct VncDbusClient {\n+ QemuVnc1ClientSkeleton *skeleton;\n+ char *path;\n+ char *host;\n+ char *service;\n+ unsigned int id;\n+ QTAILQ_ENTRY(VncDbusClient) next;\n+} VncDbusClient;\n+\n+static QemuVnc1ServerSkeleton *server_skeleton;\n+static GDBusObjectManagerServer *obj_manager;\n+static unsigned int next_client_id;\n+\n+static QTAILQ_HEAD(, VncDbusClient)\n+ dbus_clients = QTAILQ_HEAD_INITIALIZER(dbus_clients);\n+\n+static VncDbusClient *vnc_dbus_find_client(const char *host,\n+ const char *service)\n+{\n+ VncDbusClient *c;\n+\n+ QTAILQ_FOREACH(c, &dbus_clients, next) {\n+ if (g_str_equal(c->host, host) &&\n+ g_str_equal(c->service, service)) {\n+ return c;\n+ }\n+ }\n+ return NULL;\n+}\n+\n+static void vnc_dbus_update_clients_property(void)\n+{\n+ VncDbusClient *c;\n+ GPtrArray *paths;\n+ const char **strv;\n+\n+ paths = g_ptr_array_new();\n+ QTAILQ_FOREACH(c, &dbus_clients, next) {\n+ g_ptr_array_add(paths, c->path);\n+ }\n+ g_ptr_array_add(paths, NULL);\n+\n+ strv = (const char **)paths->pdata;\n+ qemu_vnc1_server_set_clients(QEMU_VNC1_SERVER(server_skeleton), strv);\n+ g_ptr_array_free(paths, TRUE);\n+}\n+\n+void vnc_dbus_client_connected(const char *host, const char *service,\n+ const char *family, bool websocket)\n+{\n+ VncDbusClient *c;\n+ g_autoptr(GDBusObjectSkeleton) obj = NULL;\n+\n+ if (!server_skeleton) {\n+ return;\n+ }\n+\n+ c = g_new0(VncDbusClient, 1);\n+ c->id = next_client_id++;\n+ c->host = g_strdup(host);\n+ c->service = g_strdup(service);\n+ c->path = g_strdup_printf(\"/org/qemu/Vnc1/Client_%u\", c->id);\n+\n+ c->skeleton = QEMU_VNC1_CLIENT_SKELETON(qemu_vnc1_client_skeleton_new());\n+ qemu_vnc1_client_set_host(QEMU_VNC1_CLIENT(c->skeleton), host);\n+ qemu_vnc1_client_set_service(QEMU_VNC1_CLIENT(c->skeleton), service);\n+ qemu_vnc1_client_set_family(QEMU_VNC1_CLIENT(c->skeleton), family);\n+ qemu_vnc1_client_set_web_socket(QEMU_VNC1_CLIENT(c->skeleton), websocket);\n+ qemu_vnc1_client_set_x509_dname(QEMU_VNC1_CLIENT(c->skeleton), \"\");\n+ qemu_vnc1_client_set_sasl_username(QEMU_VNC1_CLIENT(c->skeleton), \"\");\n+\n+ obj = g_dbus_object_skeleton_new(c->path);\n+ g_dbus_object_skeleton_add_interface(\n+ obj, G_DBUS_INTERFACE_SKELETON(c->skeleton));\n+ g_dbus_object_manager_server_export(obj_manager, obj);\n+\n+ QTAILQ_INSERT_TAIL(&dbus_clients, c, next);\n+ vnc_dbus_update_clients_property();\n+\n+ qemu_vnc1_server_emit_client_connected(\n+ QEMU_VNC1_SERVER(server_skeleton), c->path);\n+}\n+\n+void vnc_dbus_client_initialized(const char *host, const char *service,\n+ const char *x509_dname,\n+ const char *sasl_username)\n+{\n+ VncDbusClient *c;\n+\n+ if (!server_skeleton) {\n+ return;\n+ }\n+\n+ c = vnc_dbus_find_client(host, service);\n+ if (!c) {\n+ trace_qemu_vnc_client_not_found(host, service);\n+ return;\n+ }\n+\n+ if (x509_dname) {\n+ qemu_vnc1_client_set_x509_dname(\n+ QEMU_VNC1_CLIENT(c->skeleton), x509_dname);\n+ }\n+ if (sasl_username) {\n+ qemu_vnc1_client_set_sasl_username(\n+ QEMU_VNC1_CLIENT(c->skeleton), sasl_username);\n+ }\n+\n+ qemu_vnc1_server_emit_client_initialized(\n+ QEMU_VNC1_SERVER(server_skeleton), c->path);\n+}\n+\n+void vnc_dbus_client_disconnected(const char *host, const char *service)\n+{\n+ VncDbusClient *c;\n+\n+ if (!server_skeleton) {\n+ return;\n+ }\n+\n+ c = vnc_dbus_find_client(host, service);\n+ if (!c) {\n+ trace_qemu_vnc_client_not_found(host, service);\n+ return;\n+ }\n+\n+ qemu_vnc1_server_emit_client_disconnected(\n+ QEMU_VNC1_SERVER(server_skeleton), c->path);\n+\n+ g_dbus_object_manager_server_unexport(obj_manager, c->path);\n+ QTAILQ_REMOVE(&dbus_clients, c, next);\n+ vnc_dbus_update_clients_property();\n+\n+ g_object_unref(c->skeleton);\n+ g_free(c->path);\n+ g_free(c->host);\n+ g_free(c->service);\n+ g_free(c);\n+}\n+\n+static gboolean\n+on_set_password(QemuVnc1Server *iface,\n+ GDBusMethodInvocation *invocation,\n+ const gchar *password,\n+ gpointer user_data)\n+{\n+ Error *err = NULL;\n+\n+ if (vnc_display_password(\"default\", password, &err) < 0) {\n+ g_dbus_method_invocation_return_error(\n+ invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,\n+ \"%s\", error_get_pretty(err));\n+ error_free(err);\n+ return TRUE;\n+ }\n+\n+ qemu_vnc1_server_complete_set_password(iface, invocation);\n+ return TRUE;\n+}\n+\n+static gboolean\n+on_expire_password(QemuVnc1Server *iface,\n+ GDBusMethodInvocation *invocation,\n+ const gchar *time_str,\n+ gpointer user_data)\n+{\n+ time_t when;\n+\n+ if (g_str_equal(time_str, \"now\")) {\n+ when = 0;\n+ } else if (g_str_equal(time_str, \"never\")) {\n+ when = TIME_MAX;\n+ } else if (time_str[0] == '+') {\n+ int seconds;\n+ if (qemu_strtoi(time_str + 1, NULL, 10, &seconds) < 0) {\n+ g_dbus_method_invocation_return_error(\n+ invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,\n+ \"Invalid time format: %s\", time_str);\n+ return TRUE;\n+ }\n+ when = time(NULL) + seconds;\n+ } else {\n+ int64_t epoch;\n+ if (qemu_strtoi64(time_str, NULL, 10, &epoch) < 0) {\n+ g_dbus_method_invocation_return_error(\n+ invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,\n+ \"Invalid time format: %s\", time_str);\n+ return TRUE;\n+ }\n+ when = epoch;\n+ }\n+\n+ if (vnc_display_pw_expire(\"default\", when) < 0) {\n+ g_dbus_method_invocation_return_error(\n+ invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,\n+ \"Failed to set password expiry\");\n+ return TRUE;\n+ }\n+\n+ qemu_vnc1_server_complete_expire_password(iface, invocation);\n+ return TRUE;\n+}\n+\n+static gboolean\n+on_reload_certificates(QemuVnc1Server *iface,\n+ GDBusMethodInvocation *invocation,\n+ gpointer user_data)\n+{\n+ Error *err = NULL;\n+\n+ if (!vnc_display_reload_certs(\"default\", &err)) {\n+ g_dbus_method_invocation_return_error(\n+ invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,\n+ \"%s\", error_get_pretty(err));\n+ error_free(err);\n+ return TRUE;\n+ }\n+\n+ qemu_vnc1_server_complete_reload_certificates(iface, invocation);\n+ return TRUE;\n+}\n+\n+static void vnc_dbus_add_listeners(VncInfo2 *info)\n+{\n+ GVariantBuilder builder;\n+ VncServerInfo2List *entry;\n+\n+ g_variant_builder_init(&builder, G_VARIANT_TYPE(\"aa{sv}\"));\n+\n+ for (entry = info->server; entry; entry = entry->next) {\n+ VncServerInfo2 *s = entry->value;\n+ const char *vencrypt_str = \"\";\n+\n+ if (s->has_vencrypt) {\n+ vencrypt_str = VncVencryptSubAuth_str(s->vencrypt);\n+ }\n+\n+ g_variant_builder_open(&builder, G_VARIANT_TYPE(\"a{sv}\"));\n+ g_variant_builder_add(&builder, \"{sv}\", \"Host\",\n+ g_variant_new_string(s->host));\n+ g_variant_builder_add(&builder, \"{sv}\", \"Service\",\n+ g_variant_new_string(s->service));\n+ g_variant_builder_add(&builder, \"{sv}\", \"Family\",\n+ g_variant_new_string(\n+ NetworkAddressFamily_str(s->family)));\n+ g_variant_builder_add(&builder, \"{sv}\", \"WebSocket\",\n+ g_variant_new_boolean(s->websocket));\n+ g_variant_builder_add(&builder, \"{sv}\", \"Auth\",\n+ g_variant_new_string(\n+ VncPrimaryAuth_str(s->auth)));\n+ g_variant_builder_add(&builder, \"{sv}\", \"VencryptSubAuth\",\n+ g_variant_new_string(vencrypt_str));\n+ g_variant_builder_close(&builder);\n+ }\n+\n+ qemu_vnc1_server_set_listeners(\n+ QEMU_VNC1_SERVER(server_skeleton),\n+ g_variant_builder_end(&builder));\n+}\n+\n+void vnc_dbus_setup(GDBusConnection *bus)\n+{\n+ g_autoptr(GDBusObjectSkeleton) server_obj = NULL;\n+ VncInfo2List *info_list;\n+ Error *err = NULL;\n+ const char *auth_str = \"none\";\n+ const char *vencrypt_str = \"\";\n+\n+ obj_manager = g_dbus_object_manager_server_new(\"/org/qemu/Vnc1\");\n+\n+ server_skeleton = QEMU_VNC1_SERVER_SKELETON(\n+ qemu_vnc1_server_skeleton_new());\n+\n+ qemu_vnc1_server_set_name(QEMU_VNC1_SERVER(server_skeleton),\n+ qemu_name ? qemu_name : \"\");\n+ qemu_vnc1_server_set_clients(QEMU_VNC1_SERVER(server_skeleton), NULL);\n+\n+ /* Query auth info from the VNC display */\n+ info_list = qmp_query_vnc_servers(&err);\n+ if (info_list) {\n+ VncInfo2 *info = info_list->value;\n+ auth_str = VncPrimaryAuth_str(info->auth);\n+ if (info->has_vencrypt) {\n+ vencrypt_str = VncVencryptSubAuth_str(info->vencrypt);\n+ }\n+ vnc_dbus_add_listeners(info);\n+ }\n+\n+ qemu_vnc1_server_set_auth(QEMU_VNC1_SERVER(server_skeleton), auth_str);\n+ qemu_vnc1_server_set_vencrypt_sub_auth(\n+ QEMU_VNC1_SERVER(server_skeleton), vencrypt_str);\n+\n+ qapi_free_VncInfo2List(info_list);\n+\n+ g_signal_connect(server_skeleton, \"handle-set-password\",\n+ G_CALLBACK(on_set_password), NULL);\n+ g_signal_connect(server_skeleton, \"handle-expire-password\",\n+ G_CALLBACK(on_expire_password), NULL);\n+ g_signal_connect(server_skeleton, \"handle-reload-certificates\",\n+ G_CALLBACK(on_reload_certificates), NULL);\n+\n+ server_obj = g_dbus_object_skeleton_new(\"/org/qemu/Vnc1/Server\");\n+ g_dbus_object_skeleton_add_interface(\n+ server_obj, G_DBUS_INTERFACE_SKELETON(server_skeleton));\n+ g_dbus_object_manager_server_export(obj_manager, server_obj);\n+\n+ g_dbus_object_manager_server_set_connection(obj_manager, bus);\n+\n+ if (g_dbus_connection_get_flags(bus)\n+ & G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION) {\n+ g_bus_own_name_on_connection(\n+ bus, \"org.qemu.vnc\",\n+ G_BUS_NAME_OWNER_FLAGS_NONE,\n+ NULL, NULL, NULL, NULL);\n+ }\n+}\n+\n+void vnc_action_shutdown(VncState *vs)\n+{\n+ VncDbusClient *c;\n+\n+ c = vnc_dbus_find_client(vs->info->host, vs->info->service);\n+ if (!c) {\n+ trace_qemu_vnc_client_not_found(vs->info->host, vs->info->service);\n+ return;\n+ }\n+\n+ qemu_vnc1_client_emit_shutdown_request(QEMU_VNC1_CLIENT(c->skeleton));\n+}\n+\n+void vnc_action_reset(VncState *vs)\n+{\n+ VncDbusClient *c;\n+\n+ c = vnc_dbus_find_client(vs->info->host, vs->info->service);\n+ if (!c) {\n+ trace_qemu_vnc_client_not_found(vs->info->host, vs->info->service);\n+ return;\n+ }\n+\n+ qemu_vnc1_client_emit_reset_request(QEMU_VNC1_CLIENT(c->skeleton));\n+}\n+\n+/*\n+ * Override the stub qapi_event_emit() to capture VNC events\n+ * and forward them to the D-Bus interface.\n+ */\n+void qapi_event_emit(QAPIEvent event, QDict *qdict)\n+{\n+ QDict *data, *client;\n+ const char *host, *service, *family;\n+ bool websocket;\n+\n+ if (event != QAPI_EVENT_VNC_CONNECTED &&\n+ event != QAPI_EVENT_VNC_INITIALIZED &&\n+ event != QAPI_EVENT_VNC_DISCONNECTED) {\n+ return;\n+ }\n+\n+ data = qdict_get_qdict(qdict, \"data\");\n+ if (!data) {\n+ return;\n+ }\n+\n+ client = qdict_get_qdict(data, \"client\");\n+ if (!client) {\n+ return;\n+ }\n+\n+ host = qdict_get_str(client, \"host\");\n+ service = qdict_get_str(client, \"service\");\n+ family = qdict_get_str(client, \"family\");\n+ websocket = qdict_get_bool(client, \"websocket\");\n+\n+ switch (event) {\n+ case QAPI_EVENT_VNC_CONNECTED:\n+ vnc_dbus_client_connected(host, service, family, websocket);\n+ break;\n+ case QAPI_EVENT_VNC_INITIALIZED: {\n+ const char *x509_dname = NULL;\n+ const char *sasl_username = NULL;\n+\n+ if (qdict_haskey(client, \"x509_dname\")) {\n+ x509_dname = qdict_get_str(client, \"x509_dname\");\n+ }\n+ if (qdict_haskey(client, \"sasl_username\")) {\n+ sasl_username = qdict_get_str(client, \"sasl_username\");\n+ }\n+ vnc_dbus_client_initialized(host, service,\n+ x509_dname, sasl_username);\n+ break;\n+ }\n+ case QAPI_EVENT_VNC_DISCONNECTED:\n+ vnc_dbus_client_disconnected(host, service);\n+ break;\n+ default:\n+ break;\n+ }\n+}\n+\n+void vnc_dbus_cleanup(void)\n+{\n+ VncDbusClient *c, *next;\n+\n+ QTAILQ_FOREACH_SAFE(c, &dbus_clients, next, next) {\n+ g_dbus_object_manager_server_unexport(obj_manager, c->path);\n+ QTAILQ_REMOVE(&dbus_clients, c, next);\n+ g_object_unref(c->skeleton);\n+ g_free(c->path);\n+ g_free(c->host);\n+ g_free(c->service);\n+ g_free(c);\n+ }\n+\n+ g_clear_object(&server_skeleton);\n+ g_clear_object(&obj_manager);\n+}\ndiff --git a/tools/qemu-vnc/display.c b/tools/qemu-vnc/display.c\nnew file mode 100644\nindex 00000000000..8fe9b6fc898\n--- /dev/null\n+++ b/tools/qemu-vnc/display.c\n@@ -0,0 +1,456 @@\n+/*\n+ * D-Bus display listener — scanout, update and cursor handling.\n+ *\n+ * Copyright (C) 2026 Red Hat, Inc.\n+ *\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+\n+#include \"qemu/sockets.h\"\n+#include \"qemu/error-report.h\"\n+#include \"ui/console-priv.h\"\n+#include \"ui/dbus-display1.h\"\n+#include \"ui/surface.h\"\n+#include \"trace.h\"\n+#include \"qemu-vnc.h\"\n+\n+typedef struct ConsoleData {\n+ QemuDBusDisplay1Console *console_proxy;\n+ QemuDBusDisplay1Keyboard *keyboard_proxy;\n+ QemuDBusDisplay1Mouse *mouse_proxy;\n+ QemuGraphicConsole *gfx_con;\n+ GDBusConnection *listener_conn;\n+ /*\n+ * When true the surface is backed by a read-only mmap (ScanoutMap path)\n+ * and Update messages must be rejected because compositing into the\n+ * surface is not possible. The plain Scanout path provides a writable\n+ * copy and clears this flag.\n+ */\n+ bool read_only;\n+} ConsoleData;\n+\n+static void display_ui_info(void *opaque, uint32_t head, QemuUIInfo *info)\n+{\n+ ConsoleData *cd = opaque;\n+ g_autoptr(GError) err = NULL;\n+\n+ if (!cd || !cd->console_proxy) {\n+ return;\n+ }\n+\n+ qemu_dbus_display1_console_call_set_uiinfo_sync(\n+ cd->console_proxy,\n+ info->width_mm, info->height_mm,\n+ info->xoff, info->yoff,\n+ info->width, info->height,\n+ G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err);\n+ if (err) {\n+ error_report(\"SetUIInfo failed: %s\", err->message);\n+ }\n+}\n+\n+static void\n+scanout_image_destroy(pixman_image_t *image, void *data)\n+{\n+ g_variant_unref(data);\n+}\n+\n+typedef struct {\n+ void *addr;\n+ size_t len;\n+} ScanoutMapData;\n+\n+static void\n+scanout_map_destroy(pixman_image_t *image, void *data)\n+{\n+ ScanoutMapData *map = data;\n+ munmap(map->addr, map->len);\n+ g_free(map);\n+}\n+\n+static gboolean\n+on_scanout(QemuDBusDisplay1Listener *listener,\n+ GDBusMethodInvocation *invocation,\n+ guint width, guint height, guint stride,\n+ guint pixman_format, GVariant *data,\n+ gpointer user_data)\n+{\n+ ConsoleData *cd = user_data;\n+ QemuConsole *con = QEMU_CONSOLE(cd->gfx_con);\n+ gsize size;\n+ const uint8_t *pixels;\n+ pixman_image_t *image;\n+ DisplaySurface *surface;\n+\n+ trace_qemu_vnc_scanout(width, height, stride, pixman_format);\n+\n+ pixels = g_variant_get_fixed_array(data, &size, 1);\n+\n+ image = pixman_image_create_bits((pixman_format_code_t)pixman_format,\n+ width, height, (uint32_t *)pixels, stride);\n+ assert(image);\n+\n+ g_variant_ref(data);\n+ pixman_image_set_destroy_function(image, scanout_image_destroy, data);\n+\n+ cd->read_only = false;\n+ surface = qemu_create_displaysurface_pixman(image);\n+ qemu_console_set_surface(con, surface);\n+\n+ qemu_dbus_display1_listener_complete_scanout(listener, invocation);\n+ return DBUS_METHOD_INVOCATION_HANDLED;\n+}\n+\n+static gboolean\n+on_update(QemuDBusDisplay1Listener *listener,\n+ GDBusMethodInvocation *invocation,\n+ gint x, gint y, gint w, gint h,\n+ guint stride, guint pixman_format, GVariant *data,\n+ gpointer user_data)\n+{\n+ ConsoleData *cd = user_data;\n+ QemuConsole *con = QEMU_CONSOLE(cd->gfx_con);\n+ DisplaySurface *surface = qemu_console_surface(con);\n+ gsize size;\n+ const uint8_t *pixels;\n+ pixman_image_t *src;\n+\n+ trace_qemu_vnc_update(x, y, w, h, stride, pixman_format);\n+ if (!surface || cd->read_only) {\n+ g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,\n+ G_DBUS_ERROR_FAILED, \"No active or writable console\");\n+ return DBUS_METHOD_INVOCATION_HANDLED;\n+ }\n+\n+ pixels = g_variant_get_fixed_array(data, &size, 1);\n+ src = pixman_image_create_bits((pixman_format_code_t)pixman_format,\n+ w, h, (uint32_t *)pixels, stride);\n+ assert(src);\n+ pixman_image_composite(PIXMAN_OP_SRC, src, NULL,\n+ surface->image,\n+ 0, 0, 0, 0, x, y, w, h);\n+ pixman_image_unref(src);\n+\n+ qemu_console_update(con, x, y, w, h);\n+\n+ qemu_dbus_display1_listener_complete_update(listener, invocation);\n+ return DBUS_METHOD_INVOCATION_HANDLED;\n+}\n+\n+static gboolean\n+on_scanout_map(QemuDBusDisplay1ListenerUnixMap *listener,\n+ GDBusMethodInvocation *invocation,\n+ GUnixFDList *fd_list,\n+ GVariant *arg_handle,\n+ guint offset, guint width, guint height,\n+ guint stride, guint pixman_format,\n+ gpointer user_data)\n+{\n+ ConsoleData *cd = user_data;\n+ gint32 handle = g_variant_get_handle(arg_handle);\n+ g_autoptr(GError) err = NULL;\n+ DisplaySurface *surface;\n+ int fd;\n+ void *addr;\n+ size_t len = (size_t)height * stride;\n+ pixman_image_t *image;\n+\n+ trace_qemu_vnc_scanout_map(width, height, stride, pixman_format, offset);\n+\n+ fd = g_unix_fd_list_get(fd_list, handle, &err);\n+ if (fd < 0) {\n+ g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,\n+ G_DBUS_ERROR_FAILED, \"Failed to get fd: %s\", err->message);\n+ return DBUS_METHOD_INVOCATION_HANDLED;\n+ }\n+\n+ /* MAP_PRIVATE: we only read; avoid propagating writes back to QEMU */\n+ addr = mmap(NULL, len, PROT_READ, MAP_PRIVATE, fd, offset);\n+ close(fd);\n+ if (addr == MAP_FAILED) {\n+ g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,\n+ G_DBUS_ERROR_FAILED, \"mmap failed: %s\", g_strerror(errno));\n+ return DBUS_METHOD_INVOCATION_HANDLED;\n+ }\n+\n+ image = pixman_image_create_bits((pixman_format_code_t)pixman_format,\n+ width, height, addr, stride);\n+ assert(image);\n+ {\n+ ScanoutMapData *map = g_new0(ScanoutMapData, 1);\n+ map->addr = addr;\n+ map->len = len;\n+ pixman_image_set_destroy_function(image, scanout_map_destroy, map);\n+ }\n+\n+ cd->read_only = true;\n+ surface = qemu_create_displaysurface_pixman(image);\n+ qemu_console_set_surface(QEMU_CONSOLE(cd->gfx_con), surface);\n+\n+ qemu_dbus_display1_listener_unix_map_complete_scanout_map(\n+ listener, invocation, NULL);\n+ return DBUS_METHOD_INVOCATION_HANDLED;\n+}\n+\n+static gboolean\n+on_update_map(QemuDBusDisplay1ListenerUnixMap *listener,\n+ GDBusMethodInvocation *invocation,\n+ guint x, guint y, guint w, guint h,\n+ gpointer user_data)\n+{\n+ ConsoleData *cd = user_data;\n+\n+ trace_qemu_vnc_update_map(x, y, w, h);\n+\n+ qemu_console_update(QEMU_CONSOLE(cd->gfx_con), x, y, w, h);\n+\n+ qemu_dbus_display1_listener_unix_map_complete_update_map(\n+ listener, invocation);\n+ return DBUS_METHOD_INVOCATION_HANDLED;\n+}\n+\n+static gboolean\n+on_cursor_define(QemuDBusDisplay1Listener *listener,\n+ GDBusMethodInvocation *invocation,\n+ gint width, gint height,\n+ gint hot_x, gint hot_y,\n+ GVariant *data,\n+ gpointer user_data)\n+{\n+ ConsoleData *cd = user_data;\n+ gsize size;\n+ const uint8_t *pixels;\n+ QEMUCursor *c;\n+\n+ trace_qemu_vnc_cursor_define(width, height, hot_x, hot_y);\n+\n+ c = cursor_alloc(width, height);\n+ if (!c) {\n+ qemu_dbus_display1_listener_complete_cursor_define(\n+ listener, invocation);\n+ return DBUS_METHOD_INVOCATION_HANDLED;\n+ }\n+\n+ c->hot_x = hot_x;\n+ c->hot_y = hot_y;\n+\n+ pixels = g_variant_get_fixed_array(data, &size, 1);\n+ memcpy(c->data, pixels, MIN(size, (gsize)width * height * 4));\n+\n+ qemu_console_set_cursor(QEMU_CONSOLE(cd->gfx_con), c);\n+ cursor_unref(c);\n+\n+ qemu_dbus_display1_listener_complete_cursor_define(\n+ listener, invocation);\n+ return DBUS_METHOD_INVOCATION_HANDLED;\n+}\n+\n+typedef struct {\n+ GMainLoop *loop;\n+ GThread *thread;\n+ GDBusConnection *listener_conn;\n+} ListenerSetupData;\n+\n+static void\n+on_register_listener_finished(GObject *source_object,\n+ GAsyncResult *res,\n+ gpointer user_data)\n+{\n+ ListenerSetupData *data = user_data;\n+ g_autoptr(GError) err = NULL;\n+\n+ qemu_dbus_display1_console_call_register_listener_finish(\n+ QEMU_DBUS_DISPLAY1_CONSOLE(source_object),\n+ NULL,\n+ res, &err);\n+\n+ if (err) {\n+ error_report(\"RegisterListener failed: %s\", err->message);\n+ g_main_loop_quit(data->loop);\n+ return;\n+ }\n+\n+ data->listener_conn = g_thread_join(data->thread);\n+ g_main_loop_quit(data->loop);\n+}\n+\n+static GDBusConnection *\n+console_register_display_listener(QemuDBusDisplay1Console *console)\n+{\n+ g_autoptr(GError) err = NULL;\n+ g_autoptr(GMainLoop) loop = NULL;\n+ g_autoptr(GUnixFDList) fd_list = NULL;\n+ ListenerSetupData data = { 0 };\n+ int pair[2];\n+ int idx;\n+\n+ if (qemu_socketpair(AF_UNIX, SOCK_STREAM, 0, pair) < 0) {\n+ error_report(\"socketpair failed: %s\", strerror(errno));\n+ return NULL;\n+ }\n+\n+ fd_list = g_unix_fd_list_new();\n+ idx = g_unix_fd_list_append(fd_list, pair[1], &err);\n+ close(pair[1]);\n+ if (idx < 0) {\n+ close(pair[0]);\n+ error_report(\"Failed to append fd: %s\", err->message);\n+ return NULL;\n+ }\n+\n+ loop = g_main_loop_new(NULL, FALSE);\n+ data.loop = loop;\n+ data.thread = p2p_dbus_thread_new(pair[0]);\n+\n+ qemu_dbus_display1_console_call_register_listener(\n+ console,\n+ g_variant_new_handle(idx),\n+ G_DBUS_CALL_FLAGS_NONE,\n+ -1,\n+ fd_list,\n+ NULL,\n+ on_register_listener_finished,\n+ &data);\n+\n+ g_main_loop_run(loop);\n+\n+ return data.listener_conn;\n+}\n+\n+static void\n+setup_display_listener(ConsoleData *cd)\n+{\n+ g_autoptr(GDBusObjectSkeleton) obj = NULL;\n+ GDBusObjectManagerServer *server;\n+ QemuDBusDisplay1Listener *iface;\n+ QemuDBusDisplay1ListenerUnixMap *iface_map;\n+\n+ server = g_dbus_object_manager_server_new(DBUS_DISPLAY1_ROOT);\n+ obj = g_dbus_object_skeleton_new(DBUS_DISPLAY1_ROOT \"/Listener\");\n+\n+ /* Main listener interface */\n+ iface = qemu_dbus_display1_listener_skeleton_new();\n+ g_object_connect(iface,\n+ \"signal::handle-scanout\", on_scanout, cd,\n+ \"signal::handle-update\", on_update, cd,\n+ \"signal::handle-cursor-define\", on_cursor_define, cd,\n+ NULL);\n+ g_dbus_object_skeleton_add_interface(obj,\n+ G_DBUS_INTERFACE_SKELETON(iface));\n+\n+ /* Unix shared memory map interface */\n+ iface_map = qemu_dbus_display1_listener_unix_map_skeleton_new();\n+ g_object_connect(iface_map,\n+ \"signal::handle-scanout-map\", on_scanout_map, cd,\n+ \"signal::handle-update-map\", on_update_map, cd,\n+ NULL);\n+ g_dbus_object_skeleton_add_interface(obj,\n+ G_DBUS_INTERFACE_SKELETON(iface_map));\n+\n+ {\n+ const gchar *ifaces[] = {\n+ \"org.qemu.Display1.Listener.Unix.Map\", NULL\n+ };\n+ g_object_set(iface, \"interfaces\", ifaces, NULL);\n+ }\n+\n+ g_dbus_object_manager_server_export(server, obj);\n+ g_dbus_object_manager_server_set_connection(server,\n+ cd->listener_conn);\n+\n+ g_dbus_connection_start_message_processing(cd->listener_conn);\n+}\n+\n+static const GraphicHwOps vnc_hw_ops = {\n+ .ui_info = display_ui_info,\n+};\n+\n+bool console_setup(GDBusConnection *bus, const char *bus_name,\n+ const char *console_path)\n+{\n+ g_autoptr(GError) err = NULL;\n+ ConsoleData *cd;\n+ QemuConsole *con;\n+\n+ cd = g_new0(ConsoleData, 1);\n+\n+ cd->console_proxy = qemu_dbus_display1_console_proxy_new_sync(\n+ bus, G_DBUS_PROXY_FLAGS_NONE, bus_name,\n+ console_path, NULL, &err);\n+ if (!cd->console_proxy) {\n+ error_report(\"Failed to create console proxy for %s: %s\",\n+ console_path, err->message);\n+ g_free(cd);\n+ return false;\n+ }\n+\n+ cd->keyboard_proxy = QEMU_DBUS_DISPLAY1_KEYBOARD(\n+ qemu_dbus_display1_keyboard_proxy_new_sync(\n+ bus, G_DBUS_PROXY_FLAGS_NONE, bus_name,\n+ console_path, NULL, &err));\n+ if (!cd->keyboard_proxy) {\n+ error_report(\"Failed to create keyboard proxy for %s: %s\",\n+ console_path, err->message);\n+ g_object_unref(cd->console_proxy);\n+ g_free(cd);\n+ return false;\n+ }\n+\n+ g_clear_error(&err);\n+ cd->mouse_proxy = QEMU_DBUS_DISPLAY1_MOUSE(\n+ qemu_dbus_display1_mouse_proxy_new_sync(\n+ bus, G_DBUS_PROXY_FLAGS_NONE, bus_name,\n+ console_path, NULL, &err));\n+ if (!cd->mouse_proxy) {\n+ error_report(\"Failed to create mouse proxy for %s: %s\",\n+ console_path, err->message);\n+ g_object_unref(cd->keyboard_proxy);\n+ g_object_unref(cd->console_proxy);\n+ g_free(cd);\n+ return false;\n+ }\n+\n+ con = qemu_graphic_console_create(NULL, 0, &vnc_hw_ops, cd);\n+ cd->gfx_con = QEMU_GRAPHIC_CONSOLE(con);\n+\n+ cd->listener_conn = console_register_display_listener(\n+ cd->console_proxy);\n+ if (!cd->listener_conn) {\n+ error_report(\"Failed to setup D-Bus listener for %s\",\n+ console_path);\n+ g_object_unref(cd->mouse_proxy);\n+ g_object_unref(cd->keyboard_proxy);\n+ g_object_unref(cd->console_proxy);\n+ g_free(cd);\n+ return false;\n+ }\n+\n+ setup_display_listener(cd);\n+ input_setup(cd->keyboard_proxy, cd->mouse_proxy);\n+\n+ return true;\n+}\n+\n+QemuDBusDisplay1Keyboard *console_get_keyboard(QemuConsole *con)\n+{\n+ ConsoleData *cd;\n+\n+ if (!QEMU_IS_GRAPHIC_CONSOLE(con)) {\n+ return NULL;\n+ }\n+ cd = con->hw;\n+ return cd ? cd->keyboard_proxy : NULL;\n+}\n+\n+QemuDBusDisplay1Mouse *console_get_mouse(QemuConsole *con)\n+{\n+ ConsoleData *cd;\n+\n+ if (!QEMU_IS_GRAPHIC_CONSOLE(con)) {\n+ return NULL;\n+ }\n+ cd = con->hw;\n+ return cd ? cd->mouse_proxy : NULL;\n+}\ndiff --git a/tools/qemu-vnc/input.c b/tools/qemu-vnc/input.c\nnew file mode 100644\nindex 00000000000..2313b0a7c77\n--- /dev/null\n+++ b/tools/qemu-vnc/input.c\n@@ -0,0 +1,239 @@\n+/*\n+ * Keyboard and mouse input dispatch via D-Bus.\n+ *\n+ * Copyright (C) 2026 Red Hat, Inc.\n+ *\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+\n+#include \"ui/dbus-display1.h\"\n+#include \"ui/input.h\"\n+#include \"trace.h\"\n+#include \"qemu-vnc.h\"\n+\n+struct QEMUPutLEDEntry {\n+ QEMUPutLEDEvent *put_led;\n+ void *opaque;\n+ QTAILQ_ENTRY(QEMUPutLEDEntry) next;\n+};\n+\n+static NotifierList mouse_mode_notifiers =\n+ NOTIFIER_LIST_INITIALIZER(mouse_mode_notifiers);\n+static QTAILQ_HEAD(, QEMUPutLEDEntry) led_handlers =\n+ QTAILQ_HEAD_INITIALIZER(led_handlers);\n+\n+/* Track the target console for pending mouse events (used by sync) */\n+static QemuConsole *mouse_target;\n+\n+QEMUPutLEDEntry *qemu_add_led_event_handler(QEMUPutLEDEvent *func,\n+ void *opaque)\n+{\n+ QEMUPutLEDEntry *s;\n+\n+ s = g_new0(QEMUPutLEDEntry, 1);\n+ s->put_led = func;\n+ s->opaque = opaque;\n+ QTAILQ_INSERT_TAIL(&led_handlers, s, next);\n+ return s;\n+}\n+\n+void qemu_remove_led_event_handler(QEMUPutLEDEntry *entry)\n+{\n+ if (!entry) {\n+ return;\n+ }\n+ QTAILQ_REMOVE(&led_handlers, entry, next);\n+ g_free(entry);\n+}\n+\n+static void\n+on_keyboard_modifiers_changed(GObject *gobject, GParamSpec *pspec,\n+ gpointer user_data)\n+{\n+ guint modifiers;\n+ QEMUPutLEDEntry *cursor;\n+\n+ modifiers = qemu_dbus_display1_keyboard_get_modifiers(\n+ QEMU_DBUS_DISPLAY1_KEYBOARD(gobject));\n+\n+ /*\n+ * The D-Bus Keyboard.Modifiers property uses the same\n+ * bit layout as QEMU's LED constants.\n+ */\n+ QTAILQ_FOREACH(cursor, &led_handlers, next) {\n+ cursor->put_led(cursor->opaque, modifiers);\n+ }\n+}\n+\n+void qemu_add_mouse_mode_change_notifier(Notifier *notify)\n+{\n+ notifier_list_add(&mouse_mode_notifiers, notify);\n+}\n+\n+void qemu_remove_mouse_mode_change_notifier(Notifier *notify)\n+{\n+ notifier_remove(notify);\n+}\n+\n+void qemu_input_event_send_key_delay(uint32_t delay_ms)\n+{\n+}\n+\n+void qemu_input_event_send_key_qcode(QemuConsole *src, QKeyCode q, bool down)\n+{\n+ QemuDBusDisplay1Keyboard *kbd;\n+ guint qnum;\n+\n+ trace_qemu_vnc_key_event(q, down);\n+\n+ if (!src) {\n+ return;\n+ }\n+ kbd = console_get_keyboard(src);\n+ if (!kbd) {\n+ return;\n+ }\n+\n+ if (q >= qemu_input_map_qcode_to_qnum_len) {\n+ return;\n+ }\n+ qnum = qemu_input_map_qcode_to_qnum[q];\n+\n+ if (down) {\n+ qemu_dbus_display1_keyboard_call_press(\n+ kbd, qnum,\n+ G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);\n+ } else {\n+ qemu_dbus_display1_keyboard_call_release(\n+ kbd, qnum,\n+ G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);\n+ }\n+}\n+\n+static guint abs_x, abs_y;\n+static bool abs_pending;\n+static gint rel_dx, rel_dy;\n+static bool rel_pending;\n+\n+void qemu_input_queue_abs(QemuConsole *src, InputAxis axis,\n+ int value, int min_in, int max_in)\n+{\n+ if (axis == INPUT_AXIS_X) {\n+ abs_x = value;\n+ } else if (axis == INPUT_AXIS_Y) {\n+ abs_y = value;\n+ }\n+ abs_pending = true;\n+ mouse_target = src;\n+}\n+\n+void qemu_input_queue_rel(QemuConsole *src, InputAxis axis, int value)\n+{\n+ if (axis == INPUT_AXIS_X) {\n+ rel_dx += value;\n+ } else if (axis == INPUT_AXIS_Y) {\n+ rel_dy += value;\n+ }\n+ rel_pending = true;\n+ mouse_target = src;\n+}\n+\n+void qemu_input_event_sync(void)\n+{\n+ QemuDBusDisplay1Mouse *mouse;\n+\n+ if (!mouse_target) {\n+ return;\n+ }\n+\n+ mouse = console_get_mouse(mouse_target);\n+ if (!mouse) {\n+ abs_pending = false;\n+ rel_pending = false;\n+ return;\n+ }\n+\n+ if (abs_pending) {\n+ trace_qemu_vnc_input_abs(abs_x, abs_y);\n+ abs_pending = false;\n+ qemu_dbus_display1_mouse_call_set_abs_position(\n+ mouse, abs_x, abs_y,\n+ G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);\n+ }\n+\n+ if (rel_pending) {\n+ trace_qemu_vnc_input_rel(rel_dx, rel_dy);\n+ rel_pending = false;\n+ qemu_dbus_display1_mouse_call_rel_motion(\n+ mouse, rel_dx, rel_dy,\n+ G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);\n+ rel_dx = 0;\n+ rel_dy = 0;\n+ }\n+}\n+\n+bool qemu_input_is_absolute(QemuConsole *con)\n+{\n+ QemuDBusDisplay1Mouse *mouse;\n+\n+ if (!con) {\n+ return false;\n+ }\n+ mouse = console_get_mouse(con);\n+\n+ if (!mouse) {\n+ return false;\n+ }\n+ return qemu_dbus_display1_mouse_get_is_absolute(mouse);\n+}\n+\n+static void\n+on_mouse_is_absolute_changed(GObject *gobject, GParamSpec *pspec,\n+ gpointer user_data)\n+{\n+ notifier_list_notify(&mouse_mode_notifiers, NULL);\n+}\n+\n+void qemu_input_update_buttons(QemuConsole *src, uint32_t *button_map,\n+ uint32_t button_old, uint32_t button_new)\n+{\n+ QemuDBusDisplay1Mouse *mouse;\n+ uint32_t changed;\n+ int i;\n+\n+ if (!src) {\n+ return;\n+ }\n+ mouse = console_get_mouse(src);\n+ if (!mouse) {\n+ return;\n+ }\n+\n+ changed = button_old ^ button_new;\n+ for (i = 0; i < 32; i++) {\n+ if (!(changed & (1u << i))) {\n+ continue;\n+ }\n+ trace_qemu_vnc_input_btn(i, !!(button_new & (1u << i)));\n+ if (button_new & (1u << i)) {\n+ qemu_dbus_display1_mouse_call_press(\n+ mouse, i,\n+ G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);\n+ } else {\n+ qemu_dbus_display1_mouse_call_release(\n+ mouse, i,\n+ G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);\n+ }\n+ }\n+}\n+\n+void input_setup(QemuDBusDisplay1Keyboard *kbd,\n+ QemuDBusDisplay1Mouse *mouse)\n+{\n+ g_signal_connect(kbd, \"notify::modifiers\",\n+ G_CALLBACK(on_keyboard_modifiers_changed), NULL);\n+ g_signal_connect(mouse, \"notify::is-absolute\",\n+ G_CALLBACK(on_mouse_is_absolute_changed), NULL);\n+}\ndiff --git a/tools/qemu-vnc/qemu-vnc.c b/tools/qemu-vnc/qemu-vnc.c\nnew file mode 100644\nindex 00000000000..d063aff7a62\n--- /dev/null\n+++ b/tools/qemu-vnc/qemu-vnc.c\n@@ -0,0 +1,491 @@\n+/*\n+ * Standalone VNC server connecting to QEMU via D-Bus display interface.\n+ *\n+ * Copyright (C) 2026 Red Hat, Inc.\n+ *\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+\n+#include \"qemu/cutils.h\"\n+#include \"qemu/datadir.h\"\n+#include \"qemu/error-report.h\"\n+#include \"qemu/config-file.h\"\n+#include \"qemu/option.h\"\n+#include \"qemu/log.h\"\n+#include \"qemu/main-loop.h\"\n+#include \"qemu-version.h\"\n+#include \"ui/vnc.h\"\n+#include \"crypto/secret.h\"\n+#include \"crypto/tlscredsx509.h\"\n+#include \"qom/object_interfaces.h\"\n+#include \"trace.h\"\n+#include \"qemu-vnc.h\"\n+\n+const char *qemu_name;\n+const char *keyboard_layout;\n+\n+static bool terminate;\n+static VncDisplay *vd;\n+\n+static GType\n+dbus_display_get_proxy_type(GDBusObjectManagerClient *manager,\n+ const gchar *object_path,\n+ const gchar *interface_name,\n+ gpointer user_data)\n+{\n+ static const struct {\n+ const char *iface;\n+ GType (*get_type)(void);\n+ } types[] = {\n+ { \"org.qemu.Display1.Clipboard\",\n+ qemu_dbus_display1_clipboard_proxy_get_type },\n+ { \"org.qemu.Display1.Audio\",\n+ qemu_dbus_display1_audio_proxy_get_type },\n+ { \"org.qemu.Display1.Chardev\",\n+ qemu_dbus_display1_chardev_proxy_get_type },\n+ };\n+\n+ if (!interface_name) {\n+ return G_TYPE_DBUS_OBJECT_PROXY;\n+ }\n+\n+ for (int i = 0; i < G_N_ELEMENTS(types); i++) {\n+ if (g_str_equal(interface_name, types[i].iface)) {\n+ return types[i].get_type();\n+ }\n+ }\n+\n+ return G_TYPE_DBUS_PROXY;\n+}\n+\n+static void\n+on_bus_closed(GDBusConnection *connection,\n+ gboolean remote_peer_vanished,\n+ GError *error,\n+ gpointer user_data)\n+{\n+ terminate = true;\n+ qemu_notify_event();\n+}\n+\n+static void\n+on_owner_vanished(GDBusConnection *connection,\n+ const gchar *name,\n+ gpointer user_data)\n+{\n+ trace_qemu_vnc_owner_vanished(name);\n+ error_report(\"D-Bus peer %s vanished, terminating\", name);\n+ terminate = true;\n+ qemu_notify_event();\n+}\n+\n+typedef struct {\n+ GDBusConnection *bus;\n+ const char *bus_name;\n+ const char * const *chardev_names;\n+ bool no_vt;\n+} ManagerSetupData;\n+\n+static int\n+compare_console_paths(const void *a, const void *b)\n+{\n+ const char *pa = *(const char **)a;\n+ const char *pb = *(const char **)b;\n+ return strcmp(pa, pb);\n+}\n+\n+static void\n+on_manager_ready(GObject *source_object,\n+ GAsyncResult *res,\n+ gpointer user_data)\n+{\n+ ManagerSetupData *data = user_data;\n+ g_autoptr(GError) err = NULL;\n+ g_autoptr(GDBusObjectManager) manager = NULL;\n+ GList *objects, *l;\n+ g_autoptr(GPtrArray) console_paths = NULL;\n+ bool found = false;\n+\n+ manager = G_DBUS_OBJECT_MANAGER(\n+ g_dbus_object_manager_client_new_finish(res, &err));\n+ if (!manager) {\n+ error_report(\"Failed to create object manager: %s\",\n+ err->message);\n+ terminate = true;\n+ qemu_notify_event();\n+ g_free(data);\n+ return;\n+ }\n+\n+ /*\n+ * Discover all Console objects and sort them so that console\n+ * indices are assigned in a predictable order matching QEMU's.\n+ */\n+ console_paths = g_ptr_array_new_with_free_func(g_free);\n+ objects = g_dbus_object_manager_get_objects(manager);\n+ for (l = objects; l; l = l->next) {\n+ GDBusObject *obj = l->data;\n+ const char *path = g_dbus_object_get_object_path(obj);\n+\n+ if (g_str_has_prefix(path, DBUS_DISPLAY1_ROOT \"/Console_\")) {\n+ g_ptr_array_add(console_paths, g_strdup(path));\n+ }\n+ }\n+ g_list_free_full(objects, g_object_unref);\n+\n+ g_ptr_array_sort(console_paths, compare_console_paths);\n+\n+ for (guint i = 0; i < console_paths->len; i++) {\n+ const char *path = g_ptr_array_index(console_paths, i);\n+\n+ if (!console_setup(data->bus, data->bus_name, path)) {\n+ error_report(\"Failed to setup console %s\", path);\n+ continue;\n+ }\n+ found = true;\n+ }\n+\n+ if (!found) {\n+ error_report(\"No consoles found\");\n+ terminate = true;\n+ qemu_notify_event();\n+ g_free(data);\n+ return;\n+ }\n+\n+ /*\n+ * Create the VNC display now that consoles exist, so that the\n+ * display change listener is registered against a valid console.\n+ */\n+ {\n+ Error *local_err = NULL;\n+\n+ vd = vnc_display_new(\"default\", &local_err);\n+ if (!vd) {\n+ error_report_err(local_err);\n+ terminate = true;\n+ qemu_notify_event();\n+ g_free(data);\n+ return;\n+ }\n+ }\n+\n+ vnc_dbus_setup(data->bus);\n+\n+ clipboard_setup(manager, data->bus);\n+ audio_setup(manager);\n+ if (!data->no_vt) {\n+ chardev_setup(data->chardev_names, manager);\n+ }\n+ g_free(data);\n+}\n+\n+int main(int argc, char *argv[])\n+{\n+ Error *local_err = NULL;\n+ g_autoptr(GError) err = NULL;\n+ g_autoptr(GDBusConnection) bus = NULL;\n+ g_autofree char *dbus_address = NULL;\n+ g_autofree char *bus_name = NULL;\n+ int dbus_p2p_fd = -1;\n+ g_autofree char *vnc_addr = NULL;\n+ g_autofree char *ws_addr = NULL;\n+ g_autofree char *share = NULL;\n+ g_autofree char *tls_creds_dir = NULL;\n+ g_autofree char *tls_authz = NULL;\n+ g_autofree char *sasl_authz = NULL;\n+ g_autofree char *trace_opt = NULL;\n+ g_auto(GStrv) chardev_names = NULL;\n+ g_auto(GStrv) object_strs = NULL;\n+ const char *creds_dir;\n+ bool has_vnc_password = false;\n+ bool show_version = false;\n+ bool no_vt = false;\n+ bool password = false;\n+ bool sasl = false;\n+ bool lossy = false;\n+ bool non_adaptive = false;\n+ g_autoptr(GOptionContext) context = NULL;\n+ GOptionEntry entries[] = {\n+ { \"dbus-address\", 'a', 0, G_OPTION_ARG_STRING, &dbus_address,\n+ \"D-Bus address to connect to (default: session bus)\", \"ADDRESS\" },\n+ { \"dbus-p2p-fd\", 'p', 0, G_OPTION_ARG_INT, &dbus_p2p_fd,\n+ \"D-Bus peer-to-peer socket file descriptor\", \"FD\" },\n+ { \"bus-name\", 'n', 0, G_OPTION_ARG_STRING, &bus_name,\n+ \"D-Bus bus name (default: org.qemu)\", \"NAME\" },\n+ { \"vnc-addr\", 'l', 0, G_OPTION_ARG_STRING, &vnc_addr,\n+ \"VNC display address (default localhost:0)\", \"ADDR\" },\n+ { \"websocket\", 'w', 0, G_OPTION_ARG_STRING, &ws_addr,\n+ \"WebSocket address (e.g. port number or addr:port)\", \"ADDR\" },\n+ { \"share\", 's', 0, G_OPTION_ARG_STRING, &share,\n+ \"Display sharing policy \"\n+ \"(allow-exclusive|force-shared|ignore)\", \"POLICY\" },\n+ { \"tls-creds\", 't', 0, G_OPTION_ARG_STRING, &tls_creds_dir,\n+ \"TLS x509 credentials directory\", \"DIR\" },\n+ { \"tls-authz\", 0, 0, G_OPTION_ARG_STRING, &tls_authz,\n+ \"ID of a QAuthZ object for TLS client certificate \"\n+ \"authorization\", \"ID\" },\n+ { \"object\", 'O', 0, G_OPTION_ARG_STRING_ARRAY, &object_strs,\n+ \"QEMU user-creatable object \"\n+ \"(e.g. authz-list-file,id=auth0,filename=acl.json)\", \"OBJDEF\" },\n+ { \"vt-chardev\", 'C', 0, G_OPTION_ARG_STRING_ARRAY, &chardev_names,\n+ \"Chardev type names to expose as text console (repeatable, \"\n+ \"default: serial & hmp)\", \"NAME\" },\n+ { \"no-vt\", 'N', 0, G_OPTION_ARG_NONE, &no_vt,\n+ \"Do not expose any chardevs as text consoles\", NULL },\n+ { \"keyboard-layout\", 'k', 0, G_OPTION_ARG_STRING, &keyboard_layout,\n+ \"Keyboard layout\", \"LAYOUT\" },\n+ { \"trace\", 'T', 0, G_OPTION_ARG_STRING, &trace_opt,\n+ \"Trace options (same as QEMU -trace)\", \"PATTERN\" },\n+ { \"version\", 'V', 0, G_OPTION_ARG_NONE, &show_version,\n+ \"Print version information and exit\", NULL },\n+ { \"password\", 0, 0, G_OPTION_ARG_NONE, &password,\n+ \"Require password authentication (use D-Bus SetPassword to set)\",\n+ NULL },\n+ { \"lossy\", 0, 0, G_OPTION_ARG_NONE, &lossy,\n+ \"Enable lossy compression\", NULL },\n+ { \"non-adaptive\", 0, 0, G_OPTION_ARG_NONE, &non_adaptive,\n+ \"Disable adaptive encodings\", NULL },\n+ { \"sasl\", 0, 0, G_OPTION_ARG_NONE, &sasl,\n+ \"Enable SASL authentication\", NULL },\n+ { \"sasl-authz\", 0, 0, G_OPTION_ARG_STRING, &sasl_authz,\n+ \"ID of a QAuthZ object for SASL username \"\n+ \"authorization\", \"ID\" },\n+ { NULL }\n+ };\n+\n+ qemu_init_exec_dir(argv[0]);\n+ qemu_add_data_dir(get_relocated_path(CONFIG_QEMU_DATADIR));\n+\n+ module_call_init(MODULE_INIT_TRACE);\n+ module_call_init(MODULE_INIT_QOM);\n+ module_call_init(MODULE_INIT_OPTS);\n+ qemu_add_opts(&qemu_trace_opts);\n+\n+ context = g_option_context_new(\"- standalone VNC server for QEMU\");\n+ g_option_context_add_main_entries(context, entries, NULL);\n+ if (!g_option_context_parse(context, &argc, &argv, &err)) {\n+ error_report(\"Option parsing failed: %s\", err->message);\n+ return 1;\n+ }\n+\n+ if (show_version) {\n+ printf(\"qemu-vnc \" QEMU_FULL_VERSION \"\\n\");\n+ return 0;\n+ }\n+\n+ if (trace_opt) {\n+ trace_opt_parse(trace_opt);\n+ qemu_set_log(LOG_TRACE, &local_err);\n+ if (local_err) {\n+ error_report_err(local_err);\n+ return 1;\n+ }\n+ }\n+ trace_init_file();\n+\n+ if (qemu_init_main_loop(&local_err)) {\n+ error_report_err(local_err);\n+ return 1;\n+ }\n+\n+ if (!vnc_addr) {\n+ vnc_addr = g_strdup(\"localhost:0\");\n+ }\n+\n+ if (object_strs) {\n+ for (int i = 0; object_strs[i]; i++) {\n+ user_creatable_process_cmdline(object_strs[i]);\n+ }\n+ }\n+\n+ if (tls_authz && !tls_creds_dir) {\n+ error_report(\"--tls-authz requires --tls-creds\");\n+ return 1;\n+ }\n+\n+ if (sasl_authz && !sasl) {\n+ error_report(\"--sasl-authz requires --sasl\");\n+ return 1;\n+ }\n+\n+ if (dbus_p2p_fd >= 0 && dbus_address) {\n+ error_report(\"--dbus-p2p-fd and --dbus-address are\"\n+ \" mutually exclusive\");\n+ return 1;\n+ }\n+\n+ if (dbus_p2p_fd >= 0) {\n+ g_autoptr(GSocket) socket = NULL;\n+ g_autoptr(GSocketConnection) socketc = NULL;\n+\n+ if (bus_name) {\n+ error_report(\"--bus-name is not supported with --dbus-p2p-fd\");\n+ return 1;\n+ }\n+\n+ socket = g_socket_new_from_fd(dbus_p2p_fd, &err);\n+ if (!socket) {\n+ error_report(\"Failed to create socket from fd %d: %s\",\n+ dbus_p2p_fd, err->message);\n+ return 1;\n+ }\n+\n+ socketc = g_socket_connection_factory_create_connection(socket);\n+ if (!socketc) {\n+ error_report(\"Failed to create socket connection\");\n+ return 1;\n+ }\n+\n+ bus = g_dbus_connection_new_sync(\n+ G_IO_STREAM(socketc), NULL,\n+ G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT,\n+ NULL, NULL, &err);\n+ } else if (dbus_address) {\n+ GDBusConnectionFlags flags =\n+ G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT;\n+ if (bus_name) {\n+ flags |= G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION;\n+ }\n+ bus = g_dbus_connection_new_for_address_sync(\n+ dbus_address, flags, NULL, NULL, &err);\n+ } else {\n+ bus = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &err);\n+ if (!bus_name) {\n+ bus_name = g_strdup(\"org.qemu\");\n+ }\n+ }\n+ if (!bus) {\n+ error_report(\"Failed to connect to D-Bus: %s\", err->message);\n+ return 1;\n+ }\n+\n+ {\n+ g_autoptr(QemuDBusDisplay1VMProxy) vm_proxy = QEMU_DBUS_DISPLAY1_VM_PROXY(\n+ qemu_dbus_display1_vm_proxy_new_sync(\n+ bus, G_DBUS_PROXY_FLAGS_NONE, bus_name,\n+ DBUS_DISPLAY1_ROOT \"/VM\", NULL, NULL));\n+ if (vm_proxy) {\n+ qemu_name = g_strdup(qemu_dbus_display1_vm_get_name(\n+ QEMU_DBUS_DISPLAY1_VM(vm_proxy)));\n+ }\n+ }\n+\n+ /*\n+ * Set up TLS credentials if requested. The object must exist\n+ * before vnc_display_open() which looks it up by ID.\n+ */\n+ if (tls_creds_dir) {\n+ if (!object_new_with_props(TYPE_QCRYPTO_TLS_CREDS_X509,\n+ object_get_objects_root(),\n+ \"tlscreds0\",\n+ &local_err,\n+ \"endpoint\", \"server\",\n+ \"dir\", tls_creds_dir,\n+ \"verify-peer\", tls_authz ? \"yes\" : \"no\",\n+ NULL)) {\n+ error_report_err(local_err);\n+ return 1;\n+ }\n+ }\n+\n+ /*\n+ * Check for systemd credentials: if a vnc-password credential\n+ * file exists, create a QCryptoSecret and enable VNC password auth.\n+ */\n+ creds_dir = g_getenv(\"CREDENTIALS_DIRECTORY\");\n+ if (creds_dir) {\n+ g_autofree char *password_path =\n+ g_build_filename(creds_dir, \"vnc-password\", NULL);\n+ if (g_file_test(password_path, G_FILE_TEST_EXISTS)) {\n+ if (!object_new_with_props(TYPE_QCRYPTO_SECRET,\n+ object_get_objects_root(),\n+ \"vncsecret0\",\n+ &local_err,\n+ \"file\", password_path,\n+ NULL)) {\n+ error_report_err(local_err);\n+ return 1;\n+ }\n+ has_vnc_password = true;\n+ }\n+ }\n+\n+ {\n+ g_autoptr(GString) vnc_opts = g_string_new(vnc_addr);\n+ QemuOptsList *olist = qemu_find_opts(\"vnc\");\n+ QemuOpts *opts;\n+\n+ if (tls_creds_dir) {\n+ g_string_append(vnc_opts, \",tls-creds=tlscreds0\");\n+ }\n+ if (tls_authz) {\n+ g_string_append_printf(vnc_opts, \",tls-authz=%s\", tls_authz);\n+ }\n+ if (sasl) {\n+ g_string_append(vnc_opts, \",sasl=on\");\n+ }\n+ if (sasl_authz) {\n+ g_string_append_printf(vnc_opts, \",sasl-authz=%s\", sasl_authz);\n+ }\n+ if (has_vnc_password) {\n+ g_string_append(vnc_opts, \",password-secret=vncsecret0\");\n+ }\n+ if (ws_addr) {\n+ g_string_append_printf(vnc_opts, \",websocket=%s\", ws_addr);\n+ }\n+ if (share) {\n+ g_string_append_printf(vnc_opts, \",share=%s\", share);\n+ }\n+ if (password && !has_vnc_password) {\n+ g_string_append(vnc_opts, \",password=on\");\n+ }\n+ if (lossy) {\n+ g_string_append(vnc_opts, \",lossy=on\");\n+ }\n+ if (non_adaptive) {\n+ g_string_append(vnc_opts, \",non-adaptive=on\");\n+ }\n+\n+ opts = qemu_opts_parse_noisily(olist, vnc_opts->str, true);\n+ if (!opts) {\n+ return 1;\n+ }\n+ qemu_opts_set_id(opts, g_strdup(\"default\"));\n+ }\n+\n+ {\n+ ManagerSetupData *mgr_data = g_new0(ManagerSetupData, 1);\n+ mgr_data->bus = bus;\n+ mgr_data->bus_name = bus_name;\n+ mgr_data->chardev_names = (const char * const *)chardev_names;\n+ mgr_data->no_vt = no_vt;\n+\n+ g_dbus_object_manager_client_new(\n+ bus, G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_NONE,\n+ bus_name, DBUS_DISPLAY1_ROOT,\n+ dbus_display_get_proxy_type,\n+ NULL, NULL, NULL,\n+ on_manager_ready, mgr_data);\n+ }\n+\n+ g_signal_connect(bus, \"closed\", G_CALLBACK(on_bus_closed), NULL);\n+\n+ if (bus_name) {\n+ g_bus_watch_name_on_connection(bus, bus_name,\n+ G_BUS_NAME_WATCHER_FLAGS_NONE,\n+ NULL, on_owner_vanished,\n+ NULL, NULL);\n+ }\n+\n+ while (!terminate) {\n+ main_loop_wait(false);\n+ }\n+\n+ vnc_dbus_cleanup();\n+ vnc_cleanup();\n+\n+ return 0;\n+}\ndiff --git a/tools/qemu-vnc/stubs.c b/tools/qemu-vnc/stubs.c\nnew file mode 100644\nindex 00000000000..a865ce85f04\n--- /dev/null\n+++ b/tools/qemu-vnc/stubs.c\n@@ -0,0 +1,62 @@\n+/*\n+ * Stubs for qemu-vnc standalone binary.\n+ *\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+\n+#include \"system/runstate.h\"\n+#include \"hw/core/qdev.h\"\n+#include \"monitor/monitor.h\"\n+#include \"migration/vmstate.h\"\n+\n+bool runstate_is_running(void)\n+{\n+ return true;\n+}\n+\n+bool phase_check(MachineInitPhase phase)\n+{\n+ return true;\n+}\n+\n+DeviceState *qdev_find_recursive(BusState *bus, const char *id)\n+{\n+ return NULL;\n+}\n+\n+/*\n+ * Provide the monitor stubs locally so that the linker does not\n+ * pull stubs/monitor-core.c.o from libqemuutil.a (which would\n+ * bring a conflicting qapi_event_emit definition).\n+ */\n+Monitor *monitor_cur(void)\n+{\n+ return NULL;\n+}\n+\n+bool monitor_cur_is_qmp(void)\n+{\n+ return false;\n+}\n+\n+Monitor *monitor_set_cur(Coroutine *co, Monitor *mon)\n+{\n+ return NULL;\n+}\n+\n+int monitor_vprintf(Monitor *mon, const char *fmt, va_list ap)\n+{\n+ return -1;\n+}\n+\n+/*\n+ * Link-time stubs for VMState symbols referenced by VNC code.\n+ * The standalone binary never performs migration, so these are\n+ * never actually used at runtime.\n+ */\n+const VMStateInfo vmstate_info_bool = {};\n+const VMStateInfo vmstate_info_int32 = {};\n+const VMStateInfo vmstate_info_uint32 = {};\n+const VMStateInfo vmstate_info_buffer = {};\ndiff --git a/tools/qemu-vnc/utils.c b/tools/qemu-vnc/utils.c\nnew file mode 100644\nindex 00000000000..d261aa9eaf0\n--- /dev/null\n+++ b/tools/qemu-vnc/utils.c\n@@ -0,0 +1,59 @@\n+/*\n+ * Standalone VNC server connecting to QEMU via D-Bus display interface.\n+ *\n+ * Copyright (C) 2026 Red Hat, Inc.\n+ *\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+\n+#include \"qemu/error-report.h\"\n+#include \"qemu-vnc.h\"\n+\n+static GDBusConnection *\n+dbus_p2p_from_fd(int fd)\n+{\n+ g_autoptr(GError) err = NULL;\n+ g_autoptr(GSocket) socket = NULL;\n+ g_autoptr(GSocketConnection) socketc = NULL;\n+ GDBusConnection *conn;\n+\n+ socket = g_socket_new_from_fd(fd, &err);\n+ if (!socket) {\n+ error_report(\"Failed to create socket: %s\", err->message);\n+ return NULL;\n+ }\n+\n+ socketc = g_socket_connection_factory_create_connection(socket);\n+ if (!socketc) {\n+ error_report(\"Failed to create socket connection\");\n+ return NULL;\n+ }\n+\n+ conn = g_dbus_connection_new_sync(\n+ G_IO_STREAM(socketc), NULL,\n+ G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT |\n+ G_DBUS_CONNECTION_FLAGS_DELAY_MESSAGE_PROCESSING,\n+ NULL, NULL, &err);\n+ if (!conn) {\n+ error_report(\"Failed to create D-Bus connection: %s\", err->message);\n+ return NULL;\n+ }\n+\n+ return conn;\n+}\n+\n+static gpointer\n+p2p_server_setup_thread(gpointer data)\n+{\n+ return dbus_p2p_from_fd(GPOINTER_TO_INT(data));\n+}\n+\n+GThread *\n+p2p_dbus_thread_new(int fd)\n+{\n+ return g_thread_new(\"p2p-server-setup\",\n+ p2p_server_setup_thread,\n+ GINT_TO_POINTER(fd));\n+}\ndiff --git a/meson_options.txt b/meson_options.txt\nindex 31d5916cfce..ef938e74793 100644\n--- a/meson_options.txt\n+++ b/meson_options.txt\n@@ -119,6 +119,8 @@ option('vfio_user_server', type: 'feature', value: 'disabled',\n description: 'vfio-user server support')\n option('dbus_display', type: 'feature', value: 'auto',\n description: '-display dbus support')\n+option('qemu_vnc', type: 'feature', value: 'auto',\n+ description: 'standalone VNC server over D-Bus')\n option('tpm', type : 'feature', value : 'auto',\n description: 'TPM support')\n option('valgrind', type : 'feature', value: 'auto',\ndiff --git a/scripts/meson-buildoptions.sh b/scripts/meson-buildoptions.sh\nindex ca5b113119a..5f7a351ca4a 100644\n--- a/scripts/meson-buildoptions.sh\n+++ b/scripts/meson-buildoptions.sh\n@@ -174,6 +174,7 @@ meson_options_help() {\n printf \"%s\\n\" ' qatzip QATzip compression support'\n printf \"%s\\n\" ' qcow1 qcow1 image format support'\n printf \"%s\\n\" ' qed qed image format support'\n+ printf \"%s\\n\" ' qemu-vnc standalone VNC server over D-Bus'\n printf \"%s\\n\" ' qga-vss build QGA VSS support (broken with MinGW)'\n printf \"%s\\n\" ' qpl Query Processing Library support'\n printf \"%s\\n\" ' rbd Ceph block device driver'\n@@ -458,6 +459,8 @@ _meson_option_parse() {\n --qemu-ga-manufacturer=*) quote_sh \"-Dqemu_ga_manufacturer=$2\" ;;\n --qemu-ga-version=*) quote_sh \"-Dqemu_ga_version=$2\" ;;\n --with-suffix=*) quote_sh \"-Dqemu_suffix=$2\" ;;\n+ --enable-qemu-vnc) printf \"%s\" -Dqemu_vnc=enabled ;;\n+ --disable-qemu-vnc) printf \"%s\" -Dqemu_vnc=disabled ;;\n --enable-qga-vss) printf \"%s\" -Dqga_vss=enabled ;;\n --disable-qga-vss) printf \"%s\" -Dqga_vss=disabled ;;\n --enable-qom-cast-debug) printf \"%s\" -Dqom_cast_debug=true ;;\ndiff --git a/tests/dbus-daemon.sh b/tests/dbus-daemon.sh\nindex c4a50c73774..85f9597db43 100755\n--- a/tests/dbus-daemon.sh\n+++ b/tests/dbus-daemon.sh\n@@ -62,9 +62,17 @@ write_config()\n <deny send_destination=\"org.freedesktop.DBus\"\n send_interface=\"org.freedesktop.systemd1.Activator\"/>\n \n- <allow own=\"org.qemu.VMState1\"/>\n- <allow send_destination=\"org.qemu.VMState1\"/>\n- <allow receive_sender=\"org.qemu.VMState1\"/>\n+ <allow own=\"org.qemu\"/>\n+ <allow send_destination=\"org.qemu\"/>\n+ <allow receive_sender=\"org.qemu\"/>\n+\n+ <allow own=\"org.qemu.VMState1\"/>\n+ <allow send_destination=\"org.qemu.VMState1\"/>\n+ <allow receive_sender=\"org.qemu.VMState1\"/>\n+\n+ <allow own=\"org.qemu.vnc\"/>\n+ <allow send_destination=\"org.qemu.vnc\"/>\n+ <allow receive_sender=\"org.qemu.vnc\"/>\n \n </policy>\n \ndiff --git a/tests/qtest/meson.build b/tests/qtest/meson.build\nindex 5f8cff172c8..e9d23003f8c 100644\n--- a/tests/qtest/meson.build\n+++ b/tests/qtest/meson.build\n@@ -411,6 +411,15 @@ if vnc.found()\n if gvnc.found()\n qtests += {'vnc-display-test': [gvnc, keymap_targets]}\n qtests_generic += [ 'vnc-display-test' ]\n+ if have_qemu_vnc and dbus_display and config_all_devices.has_key('CONFIG_VGA')\n+ dbus_vnc_test_deps = [dbus_display1, qemu_vnc1, gio, gvnc, keymap_targets]\n+ if gnutls.found() and tasn1.found()\n+ dbus_vnc_test_deps += [files('../unit/crypto-tls-x509-helpers.c'),\n+ gnutls, tasn1]\n+ endif\n+ qtests += {'dbus-vnc-test': dbus_vnc_test_deps}\n+ qtests_x86_64 += ['dbus-vnc-test']\n+ endif\n endif\n endif\n \n@@ -442,6 +451,10 @@ foreach dir : target_dirs\n qtest_env.set('QTEST_QEMU_STORAGE_DAEMON_BINARY', './storage-daemon/qemu-storage-daemon')\n test_deps += [qsd]\n endif\n+ if have_qemu_vnc\n+ qtest_env.set('QTEST_QEMU_VNC_BINARY', './tools/qemu-vnc/qemu-vnc')\n+ test_deps += [qemu_vnc]\n+ endif\n \n qtest_env.set('PYTHON', python.full_path())\n \ndiff --git a/tools/qemu-vnc/meson.build b/tools/qemu-vnc/meson.build\nnew file mode 100644\nindex 00000000000..08168da0630\n--- /dev/null\n+++ b/tools/qemu-vnc/meson.build\n@@ -0,0 +1,26 @@\n+vnca = vnc_ss.apply({}, strict: false)\n+\n+qemu_vnc1 = custom_target('qemu-vnc1 gdbus-codegen',\n+ output: ['qemu-vnc1.h', 'qemu-vnc1.c'],\n+ input: files('qemu-vnc1.xml'),\n+ command: [gdbus_codegen, '@INPUT@',\n+ '--glib-min-required', '2.64',\n+ '--output-directory', meson.current_build_dir(),\n+ '--interface-prefix', 'org.qemu.',\n+ '--c-namespace', 'Qemu',\n+ '--generate-c-code', '@BASENAME@'])\n+\n+qemu_vnc = executable('qemu-vnc',\n+ sources: ['qemu-vnc.c', 'display.c', 'input.c',\n+ 'audio.c', 'chardev.c', 'clipboard.c', 'console.c',\n+ 'dbus.c', 'stubs.c', 'utils.c',\n+ vnca.sources(), dbus_display1, qemu_vnc1],\n+ dependencies: [vnca.dependencies(), io, crypto, qemuutil, gio, ui])\n+\n+# The executable lives in a subdirectory of the build tree, but\n+# get_relocated_path() looks for qemu-bundle relative to the binary.\n+# Create a symlink so that firmware/keymap lookup works during development.\n+run_command('ln', '-sfn',\n+ '../../qemu-bundle',\n+ meson.current_build_dir() / 'qemu-bundle',\n+ check: false)\ndiff --git a/tools/qemu-vnc/qemu-vnc1.xml b/tools/qemu-vnc/qemu-vnc1.xml\nnew file mode 100644\nindex 00000000000..2037e72ae2a\n--- /dev/null\n+++ b/tools/qemu-vnc/qemu-vnc1.xml\n@@ -0,0 +1,174 @@\n+<?xml version=\"1.0\" encoding=\"utf-8\"?>\n+<node>\n+ <!--\n+ org.qemu.Vnc1.Server:\n+\n+ This interface is implemented on ``/org/qemu/Vnc1/Server``.\n+ It provides management and monitoring of the VNC server.\n+ -->\n+ <interface name=\"org.qemu.Vnc1.Server\">\n+ <!--\n+ Name:\n+\n+ The VM name.\n+ -->\n+ <property name=\"Name\" type=\"s\" access=\"read\"/>\n+\n+ <!--\n+ Auth:\n+\n+ Primary authentication method (none, vnc, vencrypt, sasl, etc.).\n+ -->\n+ <property name=\"Auth\" type=\"s\" access=\"read\"/>\n+\n+ <!--\n+ VencryptSubAuth:\n+\n+ VEncrypt sub-authentication method, if applicable.\n+ Empty string otherwise.\n+ -->\n+ <property name=\"VencryptSubAuth\" type=\"s\" access=\"read\"/>\n+\n+ <!--\n+ Clients:\n+\n+ Object paths of connected VNC clients.\n+ -->\n+ <property name=\"Clients\" type=\"ao\" access=\"read\"/>\n+\n+ <!--\n+ Listeners:\n+\n+ List of listening sockets. Each entry is a dictionary with keys:\n+ ``Host`` (s), ``Service`` (s), ``Family`` (s),\n+ ``WebSocket`` (b), ``Auth`` (s), ``VencryptSubAuth`` (s).\n+ -->\n+ <property name=\"Listeners\" type=\"aa{sv}\" access=\"read\"/>\n+\n+ <!--\n+ SetPassword:\n+ @password: The new VNC password.\n+\n+ Change the VNC password. Existing clients are unaffected.\n+ -->\n+ <method name=\"SetPassword\">\n+ <arg type=\"s\" name=\"password\" direction=\"in\"/>\n+ </method>\n+\n+ <!--\n+ ExpirePassword:\n+ @time: Expiry specification.\n+\n+ Set password expiry. Values: ``\"now\"``, ``\"never\"``,\n+ ``\"+N\"`` (seconds from now), ``\"N\"`` (absolute epoch seconds).\n+ -->\n+ <method name=\"ExpirePassword\">\n+ <arg type=\"s\" name=\"time\" direction=\"in\"/>\n+ </method>\n+\n+ <!--\n+ ReloadCertificates:\n+\n+ Reload TLS certificates from disk.\n+ -->\n+ <method name=\"ReloadCertificates\"/>\n+\n+ <!--\n+ ClientConnected:\n+ @client: Object path of the new client.\n+\n+ Emitted when a VNC client TCP connection is established\n+ (before authentication).\n+ -->\n+ <signal name=\"ClientConnected\">\n+ <arg type=\"o\" name=\"client\"/>\n+ </signal>\n+\n+ <!--\n+ ClientInitialized:\n+ @client: Object path of the client.\n+\n+ Emitted when a VNC client has completed authentication\n+ and is active.\n+ -->\n+ <signal name=\"ClientInitialized\">\n+ <arg type=\"o\" name=\"client\"/>\n+ </signal>\n+\n+ <!--\n+ ClientDisconnected:\n+ @client: Object path of the client.\n+\n+ Emitted when a VNC client disconnects.\n+ -->\n+ <signal name=\"ClientDisconnected\">\n+ <arg type=\"o\" name=\"client\"/>\n+ </signal>\n+ </interface>\n+\n+ <!--\n+ org.qemu.Vnc1.Client:\n+\n+ This interface is implemented on ``/org/qemu/Vnc1/Client_$id``.\n+ It exposes information about a connected VNC client.\n+ -->\n+ <interface name=\"org.qemu.Vnc1.Client\">\n+ <!--\n+ Host:\n+\n+ Client IP address.\n+ -->\n+ <property name=\"Host\" type=\"s\" access=\"read\"/>\n+\n+ <!--\n+ Service:\n+\n+ Client port or service name. This may depend on the host system’s\n+ service database so symbolic names should not be relied on.\n+ -->\n+ <property name=\"Service\" type=\"s\" access=\"read\"/>\n+\n+ <!--\n+ Family:\n+\n+ Address family (ipv4, ipv6, unix).\n+ -->\n+ <property name=\"Family\" type=\"s\" access=\"read\"/>\n+\n+ <!--\n+ WebSocket:\n+\n+ Whether this is a WebSocket connection.\n+ -->\n+ <property name=\"WebSocket\" type=\"b\" access=\"read\"/>\n+\n+ <!--\n+ X509Dname:\n+\n+ X.509 distinguished name (empty if not applicable).\n+ -->\n+ <property name=\"X509Dname\" type=\"s\" access=\"read\"/>\n+\n+ <!--\n+ SaslUsername:\n+\n+ SASL username (empty if not applicable).\n+ -->\n+ <property name=\"SaslUsername\" type=\"s\" access=\"read\"/>\n+\n+ <!--\n+ ShutdownRequest:\n+\n+ Emitted when the VNC client requests a guest shutdown.\n+ -->\n+ <signal name=\"ShutdownRequest\"/>\n+\n+ <!--\n+ ResetRequest:\n+\n+ Emitted when the VNC client requests a guest reset.\n+ -->\n+ <signal name=\"ResetRequest\"/>\n+ </interface>\n+\n+</node>\ndiff --git a/tools/qemu-vnc/trace-events b/tools/qemu-vnc/trace-events\nnew file mode 100644\nindex 00000000000..f2d66a80986\n--- /dev/null\n+++ b/tools/qemu-vnc/trace-events\n@@ -0,0 +1,20 @@\n+qemu_vnc_audio_out_fini(uint64_t id) \"id=%\" PRIu64\n+qemu_vnc_audio_out_init(uint64_t id, uint32_t freq, uint8_t channels, uint8_t bits) \"id=%\" PRIu64 \" freq=%u ch=%u bits=%u\"\n+qemu_vnc_audio_out_set_enabled(uint64_t id, bool enabled) \"id=%\" PRIu64 \" enabled=%d\"\n+qemu_vnc_audio_out_write(uint64_t id, size_t size) \"id=%\" PRIu64 \" size=%zu\"\n+qemu_vnc_chardev_connected(const char *name) \"name=%s\"\n+qemu_vnc_clipboard_grab(int selection, uint32_t serial) \"selection=%d serial=%u\"\n+qemu_vnc_clipboard_release(int selection) \"selection=%d\"\n+qemu_vnc_clipboard_request(int selection) \"selection=%d\"\n+qemu_vnc_client_not_found(const char *host, const char *service) \"host=%s service=%s\"\n+qemu_vnc_console_io_error(const char *name) \"name=%s\"\n+qemu_vnc_cursor_define(int width, int height, int hot_x, int hot_y) \"w=%d h=%d hot=%d,%d\"\n+qemu_vnc_input_abs(uint32_t x, uint32_t y) \"x=%u y=%u\"\n+qemu_vnc_input_btn(int button, bool press) \"button=%d press=%d\"\n+qemu_vnc_input_rel(int dx, int dy) \"dx=%d dy=%d\"\n+qemu_vnc_key_event(int qcode, bool down) \"qcode=%d down=%d\"\n+qemu_vnc_owner_vanished(const char *name) \"peer=%s\"\n+qemu_vnc_scanout(uint32_t width, uint32_t height, uint32_t stride, uint32_t format) \"w=%u h=%u stride=%u fmt=0x%x\"\n+qemu_vnc_scanout_map(uint32_t width, uint32_t height, uint32_t stride, uint32_t format, uint32_t offset) \"w=%u h=%u stride=%u fmt=0x%x offset=%u\"\n+qemu_vnc_update(int x, int y, int w, int h, uint32_t stride, uint32_t format) \"x=%d y=%d w=%d h=%d stride=%u fmt=0x%x\"\n+qemu_vnc_update_map(uint32_t x, uint32_t y, uint32_t w, uint32_t h) \"x=%u y=%u w=%u h=%u\"\n", "prefixes": [ "v2", "67/67" ] }