@samitouri / QOSamiQemu / commits / 3a453266e3

tools/qemu-vnc: add standalone VNC server over D-Bus

Add a standalone VNC server binary that connects to a running QEMU instance via the D-Bus display interface (org.qemu.Display1, via the bus or directly p2p). This allows serving a VNC display without compiling VNC support directly into the QEMU system emulator, and enables running the VNC server as a separate process with independent lifecycle and privilege domain. Built only when both VNC and D-Bus display support are enabled. If we wanted to have qemu -vnc disabled, and qemu-vnc built, we would need to split CONFIG_VNC. This is left as a future exercise. Current omissions include some QEMU VNC runtime features (better handled via restart), legacy options, and Windows support. Reviewed-by: Daniel P. Berrangé <berrange@redhat.com> Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>

Marc-André Lureau committed Apr 2, 2026 at 17:19 UTC 3a453266e3345cc1117238d147bb66806d49c960
29 files changed +4831 -3
MAINTAINERS
+5
@@ -2830,6 +2830,11 @@ F: docs/interop/vhost-user-gpu.rst
2830 F: contrib/vhost-user-gpu
2831 F: hw/display/vhost-user-*
2832
2833 +qemu-vnc:
2834 +M: Marc-André Lureau <marcandre.lureau@redhat.com>
2835 +S: Maintained
2836 +F: tools/qemu-vnc
2837 +
2838 Cirrus VGA
2839 M: Gerd Hoffmann <kraxel@redhat.com>
2840 S: Odd Fixes
docs/conf.py
+3
@@ -333,6 +333,9 @@ man_pages = [
333 ('tools/qemu-trace-stap', 'qemu-trace-stap',
334 'QEMU SystemTap trace tool',
335 [], 1),
336 + ('tools/qemu-vnc', 'qemu-vnc',
337 + 'QEMU standalone VNC server',
338 + [], 1),
339 ]
340 man_make_section_directory = False
341
docs/interop/dbus-display.rst
+2
@@ -1,3 +1,5 @@
1 +.. _dbus-display:
2 +
3 D-Bus display
4 =============
5
docs/interop/dbus-vnc.rst new
+26
@@ -0,0 +1,26 @@
1 +D-Bus VNC
2 +=========
3 +
4 +The ``qemu-vnc`` standalone VNC server exposes a D-Bus interface for management
5 +and monitoring of VNC connections.
6 +
7 +The service is available on the bus under the well-known name ``org.qemu.vnc``.
8 +Objects are exported under ``/org/qemu/Vnc1/``.
9 +
10 +.. contents::
11 + :local:
12 + :depth: 1
13 +
14 +.. only:: sphinx4
15 +
16 + .. dbus-doc:: tools/qemu-vnc/qemu-vnc1.xml
17 +
18 +.. only:: not sphinx4
19 +
20 + .. warning::
21 + Sphinx 4 is required to build D-Bus documentation.
22 +
23 + This is the content of ``tools/qemu-vnc/qemu-vnc1.xml``:
24 +
25 + .. literalinclude:: ../../tools/qemu-vnc/qemu-vnc1.xml
26 + :language: xml
docs/interop/index.rst
+1
@@ -13,6 +13,7 @@ are useful for making QEMU interoperate with other software.
13 dbus
14 dbus-vmstate
15 dbus-display
16 + dbus-vnc
17 live-block-operations
18 nbd
19 parallels
docs/meson.build
+1
@@ -54,6 +54,7 @@ if build_docs
54 'qemu-pr-helper.8': (have_tools ? 'man8' : ''),
55 'qemu-storage-daemon.1': (have_tools ? 'man1' : ''),
56 'qemu-trace-stap.1': (stap.found() ? 'man1' : ''),
57 + 'qemu-vnc.1': (have_qemu_vnc ? 'man1' : ''),
58 'qemu.1': 'man1',
59 'qemu-block-drivers.7': 'man7',
60 'qemu-cpu-models.7': 'man7'
docs/tools/index.rst
+1
@@ -16,3 +16,4 @@ command line utilities and other standalone programs.
16 qemu-pr-helper
17 qemu-trace-stap
18 qemu-vmsr-helper
19 + qemu-vnc
docs/tools/qemu-vnc.rst new
+226
@@ -0,0 +1,226 @@
1 +.. _qemu-vnc:
2 +
3 +==========================
4 +QEMU standalone VNC server
5 +==========================
6 +
7 +Synopsis
8 +--------
9 +
10 +**qemu-vnc** [*OPTION*]...
11 +
12 +Description
13 +-----------
14 +
15 +``qemu-vnc`` is a standalone VNC server that connects to a running QEMU instance
16 +via the D-Bus display interface (:ref:`dbus-display`). It serves the guest
17 +display, input, audio, clipboard, and serial console chardevs over the VNC
18 +protocol, allowing VNC clients to interact with the virtual machine without QEMU
19 +itself binding a VNC socket.
20 +
21 +Options
22 +-------
23 +
24 +.. program:: qemu-vnc
25 +
26 +.. option:: -h, --help
27 +
28 + Display help and exit.
29 +
30 +.. option:: -V, --version
31 +
32 + Print version information and exit.
33 +
34 +.. option:: -a ADDRESS, --dbus-address=ADDRESS
35 +
36 + D-Bus address to connect to. When not specified, ``qemu-vnc`` connects to the
37 + session bus.
38 +
39 +.. option:: -p FD, --dbus-p2p-fd=FD
40 +
41 + File descriptor of an inherited Unix socket for a peer-to-peer D-Bus
42 + connection to QEMU. This is mutually exclusive with ``--dbus-address`` and
43 + ``--bus-name``.
44 +
45 +.. option:: -n NAME, --bus-name=NAME
46 +
47 + D-Bus bus name of the QEMU instance to connect to. The default is
48 + ``org.qemu``. When a custom ``--dbus-address`` is given without a bus name,
49 + peer-to-peer D-Bus is used.
50 +
51 +.. option:: -W, --wait
52 +
53 + Wait for the D-Bus bus name to appear. Only for bus connections.
54 +
55 +.. option:: --password
56 +
57 + Require VNC password authentication from connecting clients. The password is
58 + set at runtime via the D-Bus ``SetPassword`` method (see
59 + :doc:`/interop/dbus-vnc`). Clients will not be able to connect until a
60 + password has been set.
61 +
62 + This option is ignored when a systemd credential password is present, since
63 + password authentication is already enabled via ``password-secret`` in that
64 + case.
65 +
66 +.. option:: -l ADDR, --vnc-addr=ADDR
67 +
68 + VNC listen address in the same format as the QEMU ``-vnc`` option (default
69 + ``localhost:0``, i.e. TCP port 5900).
70 +
71 +.. option:: -w ADDR, --websocket=ADDR
72 +
73 + Enable WebSocket transport on the given address. *ADDR* can be a port number
74 + or an *address:port* pair.
75 +
76 +.. option:: -O OBJDEF, --object=OBJDEF
77 +
78 + Create a QEMU user-creatable object. *OBJDEF* uses the same key=value syntax
79 + as the QEMU ``-object`` option. This option may be given multiple times. It is
80 + needed, for example, to create authorization objects referenced by
81 + ``--tls-authz``.
82 +
83 +.. option:: -t DIR, --tls-creds=DIR
84 +
85 + Directory containing TLS x509 credentials (``ca-cert.pem``,
86 + ``server-cert.pem``, ``server-key.pem``). When specified, the VNC server
87 + requires TLS from connecting clients.
88 +
89 +.. option:: --tls-authz=ID
90 +
91 + ID of a ``QAuthZ`` object previously created with ``--object`` for TLS client
92 + certificate authorization. When specified, the TLS credentials are created
93 + with ``verify-peer=yes`` so connecting clients must present a valid
94 + certificate. After the TLS handshake, the client certificate Distinguished
95 + Name is checked against the authorization object. This option requires
96 + ``--tls-creds``.
97 +
98 +.. option:: --sasl
99 +
100 + Require that the client use SASL to authenticate with the VNC server. The
101 + exact choice of authentication method used is controlled from the system /
102 + user's SASL configuration file for the 'qemu' service. This is typically found
103 + in ``/etc/sasl2/qemu.conf``. If running QEMU as an unprivileged user, an
104 + environment variable ``SASL_CONF_PATH`` can be used to make it search
105 + alternate locations for the service config. While some SASL auth methods can
106 + also provide data encryption (eg GSSAPI), it is recommended that SASL always
107 + be combined with the 'tls' and 'x509' settings to enable use of SSL and server
108 + certificates. This ensures a data encryption preventing compromise of
109 + authentication credentials. See the :ref:`VNC security` section in the System
110 + Emulation Users Guide for details on using SASL authentication.
111 +
112 +.. option:: --sasl-authz=ID
113 +
114 + ID of a ``QAuthZ`` object previously created with ``--object`` for SASL
115 + username authorization. After successful SASL authentication, the
116 + authenticated username is checked against the authorization object. If the
117 + check fails, the client is disconnected. This option requires ``--sasl``.
118 +
119 +.. option:: -s POLICY, --share=POLICY
120 +
121 + Set display sharing policy. *POLICY* is one of ``allow-exclusive``,
122 + ``force-shared``, or ``ignore``.
123 +
124 + ``allow-exclusive`` allows clients to ask for exclusive access. As suggested
125 + by the RFB spec this is implemented by dropping other connections. Connecting
126 + multiple clients in parallel requires all clients asking for a shared session
127 + (vncviewer: -shared switch). This is the default.
128 +
129 + ``force-shared`` disables exclusive client access. Useful for shared desktop
130 + sessions, where you don't want someone forgetting to specify -shared
131 + disconnect everybody else.
132 +
133 + ``ignore`` completely ignores the shared flag and allows everybody to connect
134 + unconditionally. Doesn't conform to the RFB spec but is traditional QEMU
135 + behavior.
136 +
137 +.. option:: -C NAME, --vt-chardev=NAME
138 +
139 + Chardev type name to expose as a VNC text console. This option may be given
140 + multiple times to expose several chardevs. When not specified, the defaults
141 + ``org.qemu.console.serial.0`` and ``org.qemu.monitor.hmp.0`` are used.
142 +
143 +.. option:: -N, --no-vt
144 +
145 + Do not expose any chardevs as text consoles. This overrides the default
146 + chardev list and any ``--vt-chardev`` options.
147 +
148 +.. option:: -k LAYOUT, --keyboard-layout=LAYOUT
149 +
150 + Keyboard layout (e.g. ``en-us``). Passed through to the VNC server for
151 + key-code translation.
152 +
153 +.. option:: --lossy
154 +
155 + Enable lossy compression methods (gradient, JPEG, ...). If this option is set,
156 + VNC client may receive lossy framebuffer updates depending on its encoding
157 + settings. Enabling this option can save a lot of bandwidth at the expense of
158 + quality.
159 +
160 +.. option:: --non-adaptive
161 +
162 + Disable adaptive encodings. Adaptive encodings are enabled by default. An
163 + adaptive encoding will try to detect frequently updated screen regions, and
164 + send updates in these regions using a lossy encoding (like JPEG). This can be
165 + really helpful to save bandwidth when playing videos. Disabling adaptive
166 + encodings restores the original static behavior of encodings like Tight.
167 +
168 +.. option:: -T, --trace [[enable=]PATTERN][,events=FILE][,file=FILE]
169 +
170 + .. include:: ../qemu-option-trace.rst.inc
171 +
172 +Examples
173 +--------
174 +
175 +Start QEMU with the D-Bus display backend::
176 +
177 + qemu-system-x86_64 -display dbus ...
178 +
179 +Then attach ``qemu-vnc``::
180 +
181 + qemu-vnc
182 +
183 +A VNC client can now connect to ``localhost:5900``.
184 +
185 +To listen on a different port with TLS::
186 +
187 + qemu-vnc --vnc-addr localhost:1 --tls-creds /etc/pki/qemu-vnc
188 +
189 +To require TLS with client certificate authorization::
190 +
191 + qemu-vnc --object authz-list-file,id=auth0,filename=/etc/qemu/vnc.acl,refresh=on \
192 + --tls-creds /etc/pki/qemu-vnc --tls-authz auth0
193 +
194 +To enable SASL authentication with TLS::
195 +
196 + qemu-vnc --tls-creds /etc/pki/qemu-vnc --sasl
197 +
198 +VNC password authentication
199 +----------------------------
200 +
201 +There are two ways to enable VNC password authentication:
202 +
203 +1. ``--password`` flag -- start ``qemu-vnc`` with ``--password`` and
204 + then set the password at runtime using the D-Bus ``SetPassword``
205 + method. Clients will be rejected until a password is set.
206 +
207 +2. systemd credentials -- if the ``CREDENTIALS_DIRECTORY``
208 + environment variable is set (see :manpage:`systemd.exec(5)`) and
209 + contains a file named ``vnc-password``, the VNC server will use
210 + that file's contents as the password automatically. The
211 + ``--password`` flag is not needed in this case.
212 +
213 +D-Bus interface
214 +---------------
215 +
216 +``qemu-vnc`` exposes a D-Bus interface for management and monitoring of
217 +VNC connections. See :doc:`/interop/dbus-vnc` for the full interface
218 +reference.
219 +
220 +See also
221 +--------
222 +
223 +:manpage:`qemu(1)`,
224 +:doc:`/interop/dbus-display`,
225 +:doc:`/interop/dbus-vnc`,
226 +`The RFB Protocol <https://github.com/rfbproto/rfbproto>`_
meson.build
+17
@@ -2338,6 +2338,17 @@ dbus_display = get_option('dbus_display') \
2338 error_message: gdbus_codegen_error.format('-display dbus')) \
2339 .allowed()
2340
2341 +have_qemu_vnc = get_option('qemu_vnc') \
2342 + .require(have_tools,
2343 + error_message: 'qemu-vnc requires tools support') \
2344 + .require(dbus_display,
2345 + error_message: 'qemu-vnc requires dbus-display support') \
2346 + .require(vnc.found(),
2347 + error_message: 'qemu-vnc requires vnc support') \
2348 + .require(host_os != 'windows',
2349 + error_message: 'qemu-vnc is not currently supported on Windows') \
2350 + .allowed()
2351 +
2352 have_virtfs = get_option('virtfs') \
2353 .require(host_os == 'linux' or host_os == 'darwin' or host_os == 'freebsd',
2354 error_message: 'virtio-9p (virtfs) requires Linux or macOS or FreeBSD') \
@@ -3591,6 +3602,7 @@ trace_events_subdirs = [
3602 'monitor',
3603 'util',
3604 'gdbstub',
3605 + 'tools/qemu-vnc',
3606 ]
3607 if have_linux_user
3608 trace_events_subdirs += [ 'linux-user' ]
@@ -4580,6 +4592,10 @@ if have_tools
4592 subdir('contrib/ivshmem-client')
4593 subdir('contrib/ivshmem-server')
4594 endif
4595 +
4596 + if have_qemu_vnc
4597 + subdir('tools/qemu-vnc')
4598 + endif
4599 endif
4600
4601 if stap.found()
@@ -4915,6 +4931,7 @@ if vnc.found()
4931 summary_info += {'VNC SASL support': sasl}
4932 summary_info += {'VNC JPEG support': jpeg}
4933 endif
4934 +summary_info += {'VNC D-Bus server (qemu-vnc)': have_qemu_vnc}
4935 summary_info += {'spice protocol support': spice_protocol}
4936 if spice_protocol.found()
4937 summary_info += {' spice server support': spice}
meson_options.txt
+2
@@ -119,6 +119,8 @@ option('vfio_user_server', type: 'feature', value: 'disabled',
119 description: 'vfio-user server support')
120 option('dbus_display', type: 'feature', value: 'auto',
121 description: '-display dbus support')
122 +option('qemu_vnc', type: 'feature', value: 'auto',
123 + description: 'standalone VNC server over D-Bus')
124 option('tpm', type : 'feature', value : 'auto',
125 description: 'TPM support')
126 option('valgrind', type : 'feature', value: 'auto',
scripts/meson-buildoptions.sh
+3
@@ -173,6 +173,7 @@ meson_options_help() {
173 printf "%s\n" ' qatzip QATzip compression support'
174 printf "%s\n" ' qcow1 qcow1 image format support'
175 printf "%s\n" ' qed qed image format support'
176 + printf "%s\n" ' qemu-vnc standalone VNC server over D-Bus'
177 printf "%s\n" ' qga-vss build QGA VSS support (broken with MinGW)'
178 printf "%s\n" ' qpl Query Processing Library support'
179 printf "%s\n" ' rbd Ceph block device driver'
@@ -455,6 +456,8 @@ _meson_option_parse() {
456 --qemu-ga-manufacturer=*) quote_sh "-Dqemu_ga_manufacturer=$2" ;;
457 --qemu-ga-version=*) quote_sh "-Dqemu_ga_version=$2" ;;
458 --with-suffix=*) quote_sh "-Dqemu_suffix=$2" ;;
459 + --enable-qemu-vnc) printf "%s" -Dqemu_vnc=enabled ;;
460 + --disable-qemu-vnc) printf "%s" -Dqemu_vnc=disabled ;;
461 --enable-qga-vss) printf "%s" -Dqga_vss=enabled ;;
462 --disable-qga-vss) printf "%s" -Dqga_vss=disabled ;;
463 --enable-qom-cast-debug) printf "%s" -Dqom_cast_debug=true ;;
tests/dbus-daemon.sh
+11 -3
@@ -62,9 +62,17 @@ write_config()
62 <deny send_destination="org.freedesktop.DBus"
63 send_interface="org.freedesktop.systemd1.Activator"/>
64
65 - <allow own="org.qemu.VMState1"/>
66 - <allow send_destination="org.qemu.VMState1"/>
67 - <allow receive_sender="org.qemu.VMState1"/>
65 + <allow own="org.qemu"/>
66 + <allow send_destination="org.qemu"/>
67 + <allow receive_sender="org.qemu"/>
68 +
69 + <allow own="org.qemu.VMState1"/>
70 + <allow send_destination="org.qemu.VMState1"/>
71 + <allow receive_sender="org.qemu.VMState1"/>
72 +
73 + <allow own="org.qemu.vnc"/>
74 + <allow send_destination="org.qemu.vnc"/>
75 + <allow receive_sender="org.qemu.vnc"/>
76
77 </policy>
78
tests/qtest/dbus-vnc-test.c new
+1346
@@ -0,0 +1,1346 @@
1 +/*
2 + * D-Bus VNC server (qemu-vnc) end-to-end test
3 + *
4 + * Copyright (c) 2026 Red Hat, Inc.
5 + *
6 + * SPDX-License-Identifier: GPL-2.0-or-later
7 + */
8 +
9 +#include "qemu/osdep.h"
10 +#include <gio/gio.h>
11 +#include <gvnc.h>
12 +#include <sys/un.h>
13 +#include "qemu/sockets.h"
14 +#include "libqtest.h"
15 +#include "qemu-vnc1.h"
16 +#ifdef CONFIG_TASN1
17 +#include "tests/unit/crypto-tls-x509-helpers.h"
18 +#endif
19 +
20 +#define VNC_TEST_TIMEOUT_MS 10000
21 +
22 +typedef struct DbusTest {
23 + QTestState *qts;
24 + GSubprocess *vnc_subprocess;
25 + GTestDBus *bus;
26 + GDBusConnection *bus_conn;
27 + GMainLoop *loop;
28 + char *vnc_sock_path;
29 + char *tmp_dir;
30 + char *bus_addr;
31 +} DbusTest;
32 +
33 +typedef struct LifecycleData {
34 + DbusTest *dt;
35 + QemuVnc1Server *server_proxy;
36 + VncConnection *conn;
37 + char *client_path;
38 + gboolean got_connected;
39 + gboolean got_initialized;
40 + gboolean got_disconnected;
41 +} LifecycleData;
42 +
43 +static QemuVnc1Server *
44 +create_server_proxy(GDBusConnection *bus_conn, GError **errp)
45 +{
46 + return qemu_vnc1_server_proxy_new_sync(
47 + bus_conn,
48 + G_DBUS_PROXY_FLAGS_NONE,
49 + "org.qemu.vnc",
50 + "/org/qemu/Vnc1/Server",
51 + NULL, errp);
52 +}
53 +
54 +static void
55 +on_vnc_error(VncConnection *self, const char *msg)
56 +{
57 + g_error("vnc-error: %s", msg);
58 +}
59 +
60 +static void
61 +on_vnc_auth_failure(VncConnection *self, const char *msg)
62 +{
63 + g_error("vnc-auth-failure: %s", msg);
64 +}
65 +
66 +static void
67 +on_vnc_initialized(VncConnection *self, GMainLoop *loop)
68 +{
69 + const char *name = vnc_connection_get_name(self);
70 +
71 + g_assert_cmpstr(name, ==, "QEMU (dbus-vnc-test)");
72 + g_main_loop_quit(loop);
73 +}
74 +
75 +static gboolean
76 +timeout_cb(gpointer data)
77 +{
78 + g_error("test timed out");
79 + return G_SOURCE_REMOVE;
80 +}
81 +
82 +static int
83 +connect_unix_socket(const char *path)
84 +{
85 + int fd;
86 + struct sockaddr_un addr = { .sun_family = AF_UNIX };
87 +
88 + fd = socket(AF_UNIX, SOCK_STREAM, 0);
89 + g_assert(fd >= 0);
90 +
91 + snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", path);
92 +
93 + if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
94 + close(fd);
95 + return -1;
96 + }
97 + return fd;
98 +}
99 +
100 +static int
101 +wait_for_vnc_socket(const char *path, int timeout_ms)
102 +{
103 + int elapsed = 0;
104 + const int interval = 50;
105 +
106 + while (elapsed < timeout_ms) {
107 + int fd = connect_unix_socket(path);
108 +
109 + if (fd >= 0) {
110 + return fd;
111 + }
112 +
113 + g_usleep(interval * 1000);
114 + elapsed += interval;
115 + }
116 + return -1;
117 +}
118 +
119 +static GSubprocess *
120 +spawn_qemu_vnc(int dbus_fd, const char *sock_path)
121 +{
122 + const char *binary;
123 + g_autoptr(GError) err = NULL;
124 + g_autoptr(GSubprocessLauncher) launcher = NULL;
125 + GSubprocess *proc;
126 + g_autofree char *fd_str = NULL;
127 + g_autofree char *vnc_addr = NULL;
128 +
129 + binary = g_getenv("QTEST_QEMU_VNC_BINARY");
130 + g_assert(binary != NULL);
131 +
132 + fd_str = g_strdup_printf("%d", dbus_fd);
133 + vnc_addr = g_strdup_printf("unix:%s", sock_path);
134 +
135 + launcher = g_subprocess_launcher_new(G_SUBPROCESS_FLAGS_NONE);
136 + g_subprocess_launcher_take_fd(launcher, dbus_fd, dbus_fd);
137 +
138 + proc = g_subprocess_launcher_spawn(launcher, &err,
139 + binary,
140 + "--dbus-p2p-fd", fd_str,
141 + "--vnc-addr", vnc_addr,
142 + NULL);
143 + g_assert_no_error(err);
144 + g_assert(proc != NULL);
145 +
146 + return proc;
147 +}
148 +
149 +static GSubprocess *
150 +spawn_qemu_vnc_bus_full(const char *dbus_addr, const char *sock_path,
151 + const char *const *extra_args)
152 +{
153 + const char *binary;
154 + g_autoptr(GError) err = NULL;
155 + g_autoptr(GSubprocessLauncher) launcher = NULL;
156 + g_autoptr(GPtrArray) argv = NULL;
157 + GSubprocess *proc;
158 + g_autofree char *vnc_addr = NULL;
159 +
160 + binary = g_getenv("QTEST_QEMU_VNC_BINARY");
161 + g_assert(binary != NULL);
162 +
163 + vnc_addr = g_strdup_printf("unix:%s", sock_path);
164 +
165 + argv = g_ptr_array_new();
166 + g_ptr_array_add(argv, (gpointer)binary);
167 + g_ptr_array_add(argv, (gpointer)"--dbus-address");
168 + g_ptr_array_add(argv, (gpointer)dbus_addr);
169 + g_ptr_array_add(argv, (gpointer)"--bus-name");
170 + g_ptr_array_add(argv, (gpointer)"org.qemu");
171 + g_ptr_array_add(argv, (gpointer)"--vnc-addr");
172 + g_ptr_array_add(argv, (gpointer)vnc_addr);
173 +
174 + if (extra_args) {
175 + for (int i = 0; extra_args[i]; i++) {
176 + g_ptr_array_add(argv, (gpointer)extra_args[i]);
177 + }
178 + }
179 +
180 + g_ptr_array_add(argv, NULL);
181 +
182 + launcher = g_subprocess_launcher_new(G_SUBPROCESS_FLAGS_NONE);
183 + proc = g_subprocess_launcher_spawnv(launcher,
184 + (const char *const *)argv->pdata, &err);
185 + g_assert_no_error(err);
186 + g_assert(proc != NULL);
187 +
188 + return proc;
189 +}
190 +
191 +
192 +static void
193 +name_appeared_cb(GDBusConnection *connection,
194 + const gchar *name,
195 + const gchar *name_owner,
196 + gpointer user_data)
197 +{
198 + gboolean *appeared = user_data;
199 + *appeared = TRUE;
200 +}
201 +
202 +static bool
203 +setup_dbus_test_full(DbusTest *dt, const char *const *vnc_extra_args)
204 +{
205 + g_autoptr(GError) err = NULL;
206 + g_auto(GStrv) addr_parts = NULL;
207 + g_autofree char *qemu_args = NULL;
208 +
209 + if (!g_getenv("QTEST_QEMU_VNC_BINARY")) {
210 + g_test_skip("QTEST_QEMU_VNC_BINARY not set");
211 + return false;
212 + }
213 +
214 + dt->bus = g_test_dbus_new(G_TEST_DBUS_NONE);
215 + g_test_dbus_up(dt->bus);
216 +
217 + /* remove ,guid=foo part */
218 + addr_parts = g_strsplit(g_test_dbus_get_bus_address(dt->bus), ",", 2);
219 + dt->bus_addr = g_strdup(addr_parts[0]);
220 +
221 + dt->bus_conn = g_dbus_connection_new_for_address_sync(
222 + g_test_dbus_get_bus_address(dt->bus),
223 + G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT |
224 + G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION,
225 + NULL, NULL, &err);
226 + g_assert_no_error(err);
227 +
228 + qemu_args = g_strdup_printf("-display dbus,addr=%s "
229 + "-name dbus-vnc-test", dt->bus_addr);
230 + dt->qts = qtest_init(qemu_args);
231 +
232 + dt->tmp_dir = g_dir_make_tmp("dbus-vnc-test-XXXXXX", NULL);
233 + g_assert(dt->tmp_dir != NULL);
234 + dt->vnc_sock_path = g_build_filename(dt->tmp_dir, "vnc.sock", NULL);
235 + dt->vnc_subprocess = spawn_qemu_vnc_bus_full(dt->bus_addr,
236 + dt->vnc_sock_path,
237 + vnc_extra_args);
238 +
239 + /*
240 + * Wait for the org.qemu.vnc bus name to appear, which indicates
241 + * qemu-vnc has fully initialized (connected to QEMU, set up the
242 + * display, exported its D-Bus interfaces, and opened the VNC
243 + * socket).
244 + */
245 + {
246 + guint watch_id, timeout_id;
247 + gboolean appeared = FALSE;
248 +
249 + watch_id = g_bus_watch_name_on_connection(
250 + dt->bus_conn, "org.qemu.vnc",
251 + G_BUS_NAME_WATCHER_FLAGS_NONE,
252 + name_appeared_cb, NULL, &appeared, NULL);
253 + timeout_id = g_timeout_add_seconds(10, timeout_cb, NULL);
254 +
255 + while (!appeared) {
256 + if (!g_main_context_iteration(NULL, TRUE)) {
257 + break;
258 + }
259 + }
260 +
261 + g_bus_unwatch_name(watch_id);
262 + g_source_remove(timeout_id);
263 +
264 + if (!appeared) {
265 + g_test_fail();
266 + g_test_message("Timed out waiting for org.qemu.vnc bus name");
267 + return false;
268 + }
269 + }
270 +
271 + return true;
272 +}
273 +
274 +static bool
275 +setup_dbus_test(DbusTest *dt)
276 +{
277 + return setup_dbus_test_full(dt, NULL);
278 +}
279 +
280 +static void
281 +cleanup_dbus_test(DbusTest *dt)
282 +{
283 + if (dt->bus_conn) {
284 + g_dbus_connection_close_sync(dt->bus_conn, NULL, NULL);
285 + g_object_unref(dt->bus_conn);
286 + }
287 + if (dt->vnc_subprocess) {
288 + g_subprocess_force_exit(dt->vnc_subprocess);
289 + g_subprocess_wait(dt->vnc_subprocess, NULL, NULL);
290 + g_object_unref(dt->vnc_subprocess);
291 + }
292 + if (dt->vnc_sock_path) {
293 + unlink(dt->vnc_sock_path);
294 + g_free(dt->vnc_sock_path);
295 + }
296 + if (dt->tmp_dir) {
297 + rmdir(dt->tmp_dir);
298 + g_free(dt->tmp_dir);
299 + }
300 + if (dt->qts) {
301 + qtest_quit(dt->qts);
302 + }
303 + if (dt->bus) {
304 + g_test_dbus_down(dt->bus);
305 + g_object_unref(dt->bus);
306 + }
307 + g_free(dt->bus_addr);
308 +}
309 +
310 +static void
311 +test_dbus_vnc_basic(void)
312 +{
313 + DbusTest dt = { 0 };
314 + VncConnection *conn = NULL;
315 + GMainLoop *loop = NULL;
316 + int pair[2];
317 + int vnc_fd;
318 + guint timeout_id;
319 +
320 + if (!g_getenv("QTEST_QEMU_VNC_BINARY")) {
321 + g_test_skip("QTEST_QEMU_VNC_BINARY not set");
322 + return;
323 + }
324 +
325 + dt.qts = qtest_init("-display dbus,p2p=yes -name dbus-vnc-test");
326 +
327 + g_assert_cmpint(qemu_socketpair(AF_UNIX, SOCK_STREAM, 0, pair), ==, 0);
328 + qtest_qmp_add_client(dt.qts, "@dbus-display", pair[1]);
329 + close(pair[1]);
330 +
331 + dt.tmp_dir = g_dir_make_tmp("dbus-vnc-test-XXXXXX", NULL);
332 + g_assert(dt.tmp_dir != NULL);
333 + dt.vnc_sock_path = g_build_filename(dt.tmp_dir, "vnc.sock", NULL);
334 +
335 + dt.vnc_subprocess = spawn_qemu_vnc(pair[0], dt.vnc_sock_path);
336 +
337 + vnc_fd = wait_for_vnc_socket(dt.vnc_sock_path, VNC_TEST_TIMEOUT_MS);
338 + g_assert(vnc_fd >= 0);
339 +
340 + loop = g_main_loop_new(NULL, FALSE);
341 +
342 + conn = vnc_connection_new();
343 + g_signal_connect(conn, "vnc-error",
344 + G_CALLBACK(on_vnc_error), NULL);
345 + g_signal_connect(conn, "vnc-auth-failure",
346 + G_CALLBACK(on_vnc_auth_failure), NULL);
347 + g_signal_connect(conn, "vnc-initialized",
348 + G_CALLBACK(on_vnc_initialized), loop);
349 + vnc_connection_set_auth_type(conn, VNC_CONNECTION_AUTH_NONE);
350 + vnc_connection_open_fd(conn, vnc_fd);
351 +
352 + timeout_id = g_timeout_add_seconds(10, timeout_cb, NULL);
353 + g_main_loop_run(loop);
354 + g_source_remove(timeout_id);
355 +
356 + if (conn) {
357 + vnc_connection_shutdown(conn);
358 + g_object_unref(conn);
359 + }
360 + g_clear_pointer(&loop, g_main_loop_unref);
361 + cleanup_dbus_test(&dt);
362 +}
363 +
364 +static void
365 +test_dbus_vnc_server_props(void)
366 +{
367 + DbusTest dt = { 0 };
368 + QemuVnc1Server *proxy = NULL;
369 + g_autoptr(GError) err = NULL;
370 + const gchar *const *clients;
371 + GVariant *listeners;
372 +
373 + if (!setup_dbus_test(&dt)) {
374 + goto cleanup;
375 + }
376 +
377 + proxy = create_server_proxy(dt.bus_conn, &err);
378 + g_assert_no_error(err);
379 + g_assert_nonnull(proxy);
380 +
381 + g_assert_cmpstr(qemu_vnc1_server_get_name(proxy), ==,
382 + "dbus-vnc-test");
383 + g_assert_cmpstr(qemu_vnc1_server_get_auth(proxy), ==,
384 + "none");
385 + g_assert_cmpstr(qemu_vnc1_server_get_vencrypt_sub_auth(proxy), ==,
386 + "");
387 +
388 + clients = qemu_vnc1_server_get_clients(proxy);
389 + g_assert_nonnull(clients);
390 + g_assert_cmpint(g_strv_length((gchar **)clients), ==, 0);
391 +
392 + listeners = qemu_vnc1_server_get_listeners(proxy);
393 + g_assert_nonnull(listeners);
394 + g_assert_cmpint(g_variant_n_children(listeners), >, 0);
395 +
396 +cleanup:
397 + g_clear_object(&proxy);
398 + cleanup_dbus_test(&dt);
399 +}
400 +
401 +static void
402 +on_client_connected(QemuVnc1Server *proxy,
403 + const gchar *client_path,
404 + LifecycleData *data)
405 +{
406 + data->got_connected = TRUE;
407 + data->client_path = g_strdup(client_path);
408 +}
409 +
410 +static void
411 +on_client_initialized(QemuVnc1Server *proxy,
412 + const gchar *client_path,
413 + LifecycleData *data)
414 +{
415 + data->got_initialized = TRUE;
416 + g_main_loop_quit(data->dt->loop);
417 +}
418 +
419 +static void
420 +on_client_disconnected(QemuVnc1Server *proxy,
421 + const gchar *client_path,
422 + LifecycleData *data)
423 +{
424 + data->got_disconnected = TRUE;
425 + g_main_loop_quit(data->dt->loop);
426 +}
427 +
428 +static void
429 +test_dbus_vnc_client_lifecycle(void)
430 +{
431 + DbusTest dt = { 0 };
432 + QemuVnc1Server *server_proxy = NULL;
433 + QemuVnc1Client *client_proxy = NULL;
434 + g_autoptr(GError) err = NULL;
435 + LifecycleData ldata = { 0 };
436 + int vnc_fd;
437 + guint timeout_id;
438 +
439 + if (!setup_dbus_test(&dt)) {
440 + goto cleanup;
441 + }
442 +
443 + server_proxy = create_server_proxy(dt.bus_conn, &err);
444 + g_assert_no_error(err);
445 +
446 + ldata.dt = &dt;
447 + ldata.server_proxy = server_proxy;
448 +
449 + g_signal_connect(server_proxy, "client-connected",
450 + G_CALLBACK(on_client_connected), &ldata);
451 + g_signal_connect(server_proxy, "client-initialized",
452 + G_CALLBACK(on_client_initialized), &ldata);
453 + g_signal_connect(server_proxy, "client-disconnected",
454 + G_CALLBACK(on_client_disconnected), &ldata);
455 +
456 + vnc_fd = wait_for_vnc_socket(dt.vnc_sock_path, VNC_TEST_TIMEOUT_MS);
457 + g_assert(vnc_fd >= 0);
458 +
459 + ldata.conn = vnc_connection_new();
460 + g_signal_connect(ldata.conn, "vnc-error",
461 + G_CALLBACK(on_vnc_error), NULL);
462 + g_signal_connect(ldata.conn, "vnc-auth-failure",
463 + G_CALLBACK(on_vnc_auth_failure), NULL);
464 + vnc_connection_set_auth_type(ldata.conn, VNC_CONNECTION_AUTH_NONE);
465 + vnc_connection_open_fd(ldata.conn, vnc_fd);
466 +
467 + /* wait for ClientInitialized */
468 + dt.loop = g_main_loop_new(NULL, FALSE);
469 + timeout_id = g_timeout_add_seconds(10, timeout_cb, NULL);
470 + g_main_loop_run(dt.loop);
471 + g_source_remove(timeout_id);
472 +
473 + g_assert_true(ldata.got_connected);
474 + g_assert_true(ldata.got_initialized);
475 + g_assert_nonnull(ldata.client_path);
476 +
477 + /* Check client properties while still connected */
478 + client_proxy = qemu_vnc1_client_proxy_new_sync(
479 + dt.bus_conn,
480 + G_DBUS_PROXY_FLAGS_NONE,
481 + "org.qemu.vnc",
482 + ldata.client_path,
483 + NULL, &err);
484 + g_assert_no_error(err);
485 +
486 + g_assert_cmpstr(qemu_vnc1_client_get_family(client_proxy), ==,
487 + "unix");
488 + g_assert_false(qemu_vnc1_client_get_web_socket(client_proxy));
489 + g_assert_cmpstr(qemu_vnc1_client_get_x509_dname(client_proxy), ==,
490 + "");
491 + g_assert_cmpstr(qemu_vnc1_client_get_sasl_username(client_proxy),
492 + ==, "");
493 +
494 + /* disconnect and wait for ClientDisconnected */
495 + vnc_connection_shutdown(ldata.conn);
496 + timeout_id = g_timeout_add_seconds(10, timeout_cb, NULL);
497 + g_main_loop_run(dt.loop);
498 + g_source_remove(timeout_id);
499 +
500 + g_assert_true(ldata.got_disconnected);
501 +
502 + g_object_unref(ldata.conn);
503 + g_main_loop_unref(dt.loop);
504 + dt.loop = NULL;
505 + g_free(ldata.client_path);
506 +
507 +cleanup:
508 + g_clear_object(&server_proxy);
509 + g_clear_object(&client_proxy);
510 + cleanup_dbus_test(&dt);
511 +}
512 +
513 +static void
514 +test_dbus_vnc_no_password(void)
515 +{
516 + DbusTest dt = { 0 };
517 + QemuVnc1Server *proxy = NULL;
518 + g_autoptr(GError) err = NULL;
519 + gboolean ret;
520 +
521 + if (!setup_dbus_test(&dt)) {
522 + goto cleanup;
523 + }
524 +
525 + proxy = create_server_proxy(dt.bus_conn, &err);
526 + g_assert_no_error(err);
527 +
528 + /*
529 + * With default auth=none, SetPassword should return an error
530 + * because VNC password authentication is not enabled.
531 + */
532 + ret = qemu_vnc1_server_call_set_password_sync(
533 + proxy, "secret",
534 + G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err);
535 + g_assert_false(ret);
536 + g_assert_error(err, G_DBUS_ERROR, G_DBUS_ERROR_FAILED);
537 + g_clear_error(&err);
538 +
539 + ret = qemu_vnc1_server_call_expire_password_sync(
540 + proxy, "never",
541 + G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err);
542 + g_assert_no_error(err);
543 + g_assert_true(ret);
544 +
545 + ret = qemu_vnc1_server_call_expire_password_sync(
546 + proxy, "+3600",
547 + G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err);
548 + g_assert_no_error(err);
549 + g_assert_true(ret);
550 +
551 +cleanup:
552 + g_clear_object(&proxy);
553 + cleanup_dbus_test(&dt);
554 +}
555 +
556 +typedef struct PasswordData {
557 + DbusTest *dt;
558 + VncConnection *conn;
559 + const char *password;
560 + gboolean auth_succeeded;
561 + gboolean auth_failed;
562 +} PasswordData;
563 +
564 +G_GNUC_BEGIN_IGNORE_DEPRECATIONS
565 +static void
566 +on_pw_vnc_auth_credential(VncConnection *conn, GValueArray *creds,
567 + PasswordData *data)
568 +{
569 + for (guint i = 0; i < creds->n_values; i++) {
570 + int type = g_value_get_enum(g_value_array_get_nth(creds, i));
571 +
572 + if (type == VNC_CONNECTION_CREDENTIAL_PASSWORD) {
573 + vnc_connection_set_credential(conn, type, data->password);
574 + }
575 + }
576 +}
577 +G_GNUC_END_IGNORE_DEPRECATIONS
578 +
579 +static void
580 +on_pw_vnc_initialized(VncConnection *conn, PasswordData *data)
581 +{
582 + data->auth_succeeded = TRUE;
583 + g_main_loop_quit(data->dt->loop);
584 +}
585 +
586 +static void
587 +on_pw_vnc_auth_failure(VncConnection *conn, const char *msg,
588 + PasswordData *data)
589 +{
590 + data->auth_failed = TRUE;
591 + g_main_loop_quit(data->dt->loop);
592 +}
593 +
594 +static void
595 +on_pw_vnc_error(VncConnection *conn, const char *msg,
596 + PasswordData *data)
597 +{
598 + data->auth_failed = TRUE;
599 + g_main_loop_quit(data->dt->loop);
600 +}
601 +
602 +static void
603 +test_dbus_vnc_password_auth(void)
604 +{
605 + DbusTest dt = { 0 };
606 + QemuVnc1Server *proxy = NULL;
607 + g_autoptr(GError) err = NULL;
608 + PasswordData pdata = { 0 };
609 + const char *extra_args[] = { "--password", NULL };
610 + int vnc_fd;
611 + guint timeout_id;
612 + gboolean ret;
613 +
614 + if (!setup_dbus_test_full(&dt, extra_args)) {
615 + goto cleanup;
616 + }
617 +
618 + proxy = create_server_proxy(dt.bus_conn, &err);
619 + g_assert_no_error(err);
620 +
621 + g_assert_cmpstr(qemu_vnc1_server_get_auth(proxy), ==, "vnc");
622 +
623 + ret = qemu_vnc1_server_call_set_password_sync(
624 + proxy, "testpass123",
625 + G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err);
626 + g_assert_no_error(err);
627 + g_assert_true(ret);
628 +
629 + vnc_fd = wait_for_vnc_socket(dt.vnc_sock_path, VNC_TEST_TIMEOUT_MS);
630 + g_assert(vnc_fd >= 0);
631 +
632 + pdata.dt = &dt;
633 + pdata.password = "testpass123";
634 + pdata.conn = vnc_connection_new();
635 +
636 + g_signal_connect(pdata.conn, "vnc-error",
637 + G_CALLBACK(on_pw_vnc_error), &pdata);
638 + g_signal_connect(pdata.conn, "vnc-auth-failure",
639 + G_CALLBACK(on_pw_vnc_auth_failure), &pdata);
640 + g_signal_connect(pdata.conn, "vnc-auth-credential",
641 + G_CALLBACK(on_pw_vnc_auth_credential), &pdata);
642 + g_signal_connect(pdata.conn, "vnc-initialized",
643 + G_CALLBACK(on_pw_vnc_initialized), &pdata);
644 + vnc_connection_set_auth_type(pdata.conn, VNC_CONNECTION_AUTH_VNC);
645 + vnc_connection_open_fd(pdata.conn, vnc_fd);
646 +
647 + dt.loop = g_main_loop_new(NULL, FALSE);
648 + timeout_id = g_timeout_add_seconds(10, timeout_cb, NULL);
649 + g_main_loop_run(dt.loop);
650 + g_source_remove(timeout_id);
651 +
652 + g_assert_true(pdata.auth_succeeded);
653 + g_assert_false(pdata.auth_failed);
654 +
655 + vnc_connection_shutdown(pdata.conn);
656 + g_object_unref(pdata.conn);
657 + g_main_loop_unref(dt.loop);
658 + dt.loop = NULL;
659 +
660 +cleanup:
661 + g_clear_object(&proxy);
662 + cleanup_dbus_test(&dt);
663 +}
664 +
665 +static void
666 +test_dbus_vnc_sasl_authz_no_sasl(void)
667 +{
668 + const char *binary;
669 + g_autoptr(GError) err = NULL;
670 + g_autoptr(GSubprocess) proc = NULL;
671 + gboolean ok;
672 +
673 + binary = g_getenv("QTEST_QEMU_VNC_BINARY");
674 + if (!binary) {
675 + g_test_skip("QTEST_QEMU_VNC_BINARY not set");
676 + return;
677 + }
678 +
679 + proc = g_subprocess_new(G_SUBPROCESS_FLAGS_STDERR_SILENCE,
680 + &err,
681 + binary,
682 + "--sasl-authz", "authz0",
683 + NULL);
684 + g_assert_no_error(err);
685 + g_assert_nonnull(proc);
686 +
687 + ok = g_subprocess_wait(proc, NULL, &err);
688 + g_assert_no_error(err);
689 + g_assert_true(ok);
690 + g_assert_false(g_subprocess_get_successful(proc));
691 +}
692 +
693 +#ifdef CONFIG_VNC_SASL
694 +static void
695 +test_dbus_vnc_sasl_server_props(void)
696 +{
697 + DbusTest dt = { 0 };
698 + QemuVnc1Server *proxy = NULL;
699 + g_autoptr(GError) err = NULL;
700 + const char *extra_args[] = { "--sasl", NULL };
701 +
702 + if (!setup_dbus_test_full(&dt, extra_args)) {
703 + goto cleanup;
704 + }
705 +
706 + proxy = create_server_proxy(dt.bus_conn, &err);
707 + g_assert_no_error(err);
708 + g_assert_nonnull(proxy);
709 +
710 + g_assert_cmpstr(qemu_vnc1_server_get_auth(proxy), ==, "sasl");
711 +
712 +cleanup:
713 + g_clear_object(&proxy);
714 + cleanup_dbus_test(&dt);
715 +}
716 +
717 +#define SASL_TEST_USER "testuser"
718 +#define SASL_TEST_PASS "testpass123"
719 +
720 +typedef struct SaslAuthData {
721 + DbusTest *dt;
722 + const char *username;
723 + const char *password;
724 + gboolean auth_succeeded;
725 + gboolean auth_failed;
726 +} SaslAuthData;
727 +
728 +typedef struct SaslTestData {
729 + DbusTest dt;
730 + SaslAuthData sdata;
731 + char *sasl_dir;
732 + char *db_path;
733 +} SaslTestData;
734 +
735 +G_GNUC_BEGIN_IGNORE_DEPRECATIONS
736 +static void
737 +on_sasl_vnc_auth_credential(VncConnection *conn, GValueArray *creds,
738 + SaslAuthData *data)
739 +{
740 + for (guint i = 0; i < creds->n_values; i++) {
741 + int type = g_value_get_enum(g_value_array_get_nth(creds, i));
742 +
743 + switch (type) {
744 + case VNC_CONNECTION_CREDENTIAL_USERNAME:
745 + vnc_connection_set_credential(conn, type, data->username);
746 + break;
747 + case VNC_CONNECTION_CREDENTIAL_PASSWORD:
748 + vnc_connection_set_credential(conn, type, data->password);
749 + break;
750 + }
751 + }
752 +}
753 +G_GNUC_END_IGNORE_DEPRECATIONS
754 +
755 +static void
756 +on_sasl_vnc_initialized(VncConnection *conn, SaslAuthData *data)
757 +{
758 + data->auth_succeeded = TRUE;
759 + g_main_loop_quit(data->dt->loop);
760 +}
761 +
762 +static void
763 +on_sasl_vnc_auth_failure(VncConnection *conn, const char *msg,
764 + SaslAuthData *data)
765 +{
766 + data->auth_failed = TRUE;
767 + g_main_loop_quit(data->dt->loop);
768 +}
769 +
770 +static void
771 +on_sasl_vnc_error(VncConnection *conn, const char *msg,
772 + SaslAuthData *data)
773 +{
774 + data->auth_failed = TRUE;
775 + g_main_loop_quit(data->dt->loop);
776 +}
777 +
778 +/*
779 + * Create a SASL configuration directory with a qemu.conf and a
780 + * sasldb2 user database. Returns the path to the sasldb file,
781 + * or NULL if saslpasswd2 is not available.
782 + */
783 +static char *
784 +create_sasl_config(const char *dir, const char *username,
785 + const char *password)
786 +{
787 + g_autofree char *conf_path = g_strdup_printf("%s/qemu.conf", dir);
788 + g_autofree char *db_path = g_strdup_printf("%s/sasldb2", dir);
789 + g_autoptr(GError) err = NULL;
790 + g_autoptr(GSubprocess) proc = NULL;
791 + g_autofree char *conf = NULL;
792 + GOutputStream *stdin_stream;
793 + gboolean ok;
794 +
795 + /* use PLAIN, and local auxprop sasldb plugin */
796 + conf = g_strdup_printf(
797 + "mech_list: plain\n"
798 + "pwcheck_method: auxprop\n"
799 + "auxprop_plugin: sasldb\n"
800 + "sasldb_path: %s\n", db_path);
801 + g_assert_true(g_file_set_contents(conf_path, conf, -1, NULL));
802 +
803 + proc = g_subprocess_new(
804 + G_SUBPROCESS_FLAGS_STDIN_PIPE |
805 + G_SUBPROCESS_FLAGS_STDOUT_SILENCE |
806 + G_SUBPROCESS_FLAGS_STDERR_SILENCE,
807 + &err,
808 + "saslpasswd2", "-f", db_path, "-a", "qemu", "-p", "-c",
809 + username, NULL);
810 + if (!proc) {
811 + return NULL;
812 + }
813 +
814 + stdin_stream = g_subprocess_get_stdin_pipe(proc);
815 + g_output_stream_write_all(stdin_stream, password,
816 + strlen(password), NULL, NULL, NULL);
817 + g_output_stream_close(stdin_stream, NULL, NULL);
818 +
819 + ok = g_subprocess_wait_check(proc, NULL, &err);
820 + if (!ok) {
821 + return NULL;
822 + }
823 +
824 + return g_strdup(db_path);
825 +}
826 +
827 +static void
828 +cleanup_sasl_config(const char *dir, const char *db_path)
829 +{
830 + g_autofree char *conf = g_strdup_printf("%s/qemu.conf", dir);
831 +
832 + unlink(conf);
833 + if (db_path) {
834 + unlink(db_path);
835 + }
836 + rmdir(dir);
837 +}
838 +
839 +/*
840 + * Set up SASL environment: create temp config dir, sasldb, and
841 + * start qemu-vnc with the given extra_args. Returns FALSE if the
842 + * test should be skipped.
843 + */
844 +static gboolean
845 +setup_sasl_test(SaslTestData *st, const char **extra_args)
846 +{
847 + if (!g_getenv("QTEST_QEMU_VNC_BINARY")) {
848 + g_test_skip("QTEST_QEMU_VNC_BINARY not set");
849 + return FALSE;
850 + }
851 +
852 + st->sasl_dir = g_dir_make_tmp("dbus-vnc-sasl-XXXXXX", NULL);
853 + g_assert_nonnull(st->sasl_dir);
854 +
855 + st->db_path = create_sasl_config(st->sasl_dir, SASL_TEST_USER,
856 + SASL_TEST_PASS);
857 + if (!st->db_path) {
858 + g_test_skip("saslpasswd2 not available or failed");
859 + cleanup_sasl_config(st->sasl_dir, NULL);
860 + return FALSE;
861 + }
862 +
863 + g_setenv("SASL_CONF_PATH", st->sasl_dir, TRUE);
864 +
865 + if (!setup_dbus_test_full(&st->dt, extra_args)) {
866 + return FALSE;
867 + }
868 +
869 + return TRUE;
870 +}
871 +
872 +/*
873 + * Connect to the VNC server using SASL and run the main loop
874 + * until authentication completes or times out.
875 + */
876 +static void
877 +run_sasl_auth(SaslTestData *st, const char *username,
878 + const char *password)
879 +{
880 + VncConnection *conn;
881 + guint timeout_id;
882 + int vnc_fd;
883 +
884 + st->sdata.dt = &st->dt;
885 + st->sdata.username = username;
886 + st->sdata.password = password;
887 +
888 + vnc_fd = wait_for_vnc_socket(st->dt.vnc_sock_path, VNC_TEST_TIMEOUT_MS);
889 + g_assert(vnc_fd >= 0);
890 +
891 + conn = vnc_connection_new();
892 + g_signal_connect(conn, "vnc-error",
893 + G_CALLBACK(on_sasl_vnc_error), &st->sdata);
894 + g_signal_connect(conn, "vnc-auth-failure",
895 + G_CALLBACK(on_sasl_vnc_auth_failure), &st->sdata);
896 + g_signal_connect(conn, "vnc-auth-credential",
897 + G_CALLBACK(on_sasl_vnc_auth_credential),
898 + &st->sdata);
899 + g_signal_connect(conn, "vnc-initialized",
900 + G_CALLBACK(on_sasl_vnc_initialized), &st->sdata);
901 + vnc_connection_set_auth_type(conn, VNC_CONNECTION_AUTH_SASL);
902 + vnc_connection_open_fd(conn, vnc_fd);
903 +
904 + st->dt.loop = g_main_loop_new(NULL, FALSE);
905 + timeout_id = g_timeout_add_seconds(10, timeout_cb, NULL);
906 + g_main_loop_run(st->dt.loop);
907 + g_source_remove(timeout_id);
908 +
909 + g_signal_handlers_disconnect_by_data(conn, &st->sdata);
910 + vnc_connection_shutdown(conn);
911 + g_object_unref(conn);
912 + g_main_loop_unref(st->dt.loop);
913 + st->dt.loop = NULL;
914 +}
915 +
916 +static void
917 +cleanup_sasl_test(SaslTestData *st)
918 +{
919 + cleanup_dbus_test(&st->dt);
920 + g_unsetenv("SASL_CONF_PATH");
921 + cleanup_sasl_config(st->sasl_dir, st->db_path);
922 + g_free(st->sasl_dir);
923 + g_free(st->db_path);
924 +}
925 +
926 +static void
927 +test_dbus_vnc_sasl_auth(void)
928 +{
929 + SaslTestData st = { 0 };
930 + const char *extra_args[] = { "--sasl", NULL };
931 +
932 + if (!setup_sasl_test(&st, extra_args)) {
933 + return;
934 + }
935 +
936 + run_sasl_auth(&st, SASL_TEST_USER, SASL_TEST_PASS);
937 +
938 + g_assert_true(st.sdata.auth_succeeded);
939 + g_assert_false(st.sdata.auth_failed);
940 +
941 + cleanup_sasl_test(&st);
942 +}
943 +
944 +static void
945 +test_dbus_vnc_sasl_auth_bad_password(void)
946 +{
947 + SaslTestData st = { 0 };
948 + const char *extra_args[] = { "--sasl", NULL };
949 +
950 + if (!setup_sasl_test(&st, extra_args)) {
951 + return;
952 + }
953 +
954 + run_sasl_auth(&st, SASL_TEST_USER, "wrongpassword");
955 +
956 + g_assert_false(st.sdata.auth_succeeded);
957 + g_assert_true(st.sdata.auth_failed);
958 +
959 + cleanup_sasl_test(&st);
960 +}
961 +
962 +static void
963 +test_dbus_vnc_sasl_authz_denied(void)
964 +{
965 + SaslTestData st = { 0 };
966 + const char *extra_args[] = {
967 + "--sasl",
968 + "--object",
969 + "authz-simple,id=authz0,identity=otheruser",
970 + "--sasl-authz", "authz0",
971 + NULL
972 + };
973 +
974 + if (!setup_sasl_test(&st, extra_args)) {
975 + return;
976 + }
977 +
978 + run_sasl_auth(&st, SASL_TEST_USER, SASL_TEST_PASS);
979 +
980 + g_assert_false(st.sdata.auth_succeeded);
981 + g_assert_true(st.sdata.auth_failed);
982 +
983 + cleanup_sasl_test(&st);
984 +}
985 +#endif /* CONFIG_VNC_SASL */
986 +
987 +static void
988 +test_dbus_vnc_tls_authz_no_creds(void)
989 +{
990 + const char *binary;
991 + g_autoptr(GError) err = NULL;
992 + g_autoptr(GSubprocess) proc = NULL;
993 + gboolean ok;
994 +
995 + binary = g_getenv("QTEST_QEMU_VNC_BINARY");
996 + if (!binary) {
997 + g_test_skip("QTEST_QEMU_VNC_BINARY not set");
998 + return;
999 + }
1000 +
1001 + proc = g_subprocess_new(G_SUBPROCESS_FLAGS_STDERR_SILENCE,
1002 + &err,
1003 + binary,
1004 + "--tls-authz", "authz0",
1005 + NULL);
1006 + g_assert_no_error(err);
1007 + g_assert_nonnull(proc);
1008 +
1009 + ok = g_subprocess_wait(proc, NULL, &err);
1010 + g_assert_no_error(err);
1011 + g_assert_true(ok);
1012 + g_assert_false(g_subprocess_get_successful(proc));
1013 +}
1014 +
1015 +#ifdef CONFIG_TASN1
1016 +#define CLIENT_CERT_CN "qemu-vnc-test"
1017 +
1018 +static char *
1019 +create_tls_certs(const char *dir)
1020 +{
1021 + char *keyfile = g_strdup_printf("%s/key.pem", dir);
1022 + char *cacert = g_strdup_printf("%s/ca-cert.pem", dir);
1023 + char *servercert = g_strdup_printf("%s/server-cert.pem", dir);
1024 + char *serverkey = g_strdup_printf("%s/server-key.pem", dir);
1025 + char *clientcert = g_strdup_printf("%s/client-cert.pem", dir);
1026 +
1027 + test_tls_init(keyfile);
1028 + g_assert(link(keyfile, serverkey) == 0);
1029 +
1030 + TLS_ROOT_REQ_SIMPLE(cacertreq, cacert);
1031 + TLS_CERT_REQ_SIMPLE_SERVER(servercertreq, cacertreq,
1032 + servercert, "localhost", NULL);
1033 + TLS_CERT_REQ_SIMPLE_CLIENT(clientcertreq, cacertreq,
1034 + CLIENT_CERT_CN, clientcert);
1035 +
1036 + test_tls_deinit_cert(&clientcertreq);
1037 + test_tls_deinit_cert(&servercertreq);
1038 + test_tls_deinit_cert(&cacertreq);
1039 +
1040 + g_free(cacert);
1041 + g_free(servercert);
1042 + g_free(serverkey);
1043 + g_free(clientcert);
1044 + return keyfile;
1045 +}
1046 +
1047 +static void
1048 +cleanup_tls_certs(const char *dir, const char *keyfile)
1049 +{
1050 + g_autofree char *cacert = g_strdup_printf("%s/ca-cert.pem", dir);
1051 + g_autofree char *servercert = g_strdup_printf("%s/server-cert.pem", dir);
1052 + g_autofree char *serverkey = g_strdup_printf("%s/server-key.pem", dir);
1053 + g_autofree char *clientcert = g_strdup_printf("%s/client-cert.pem", dir);
1054 +
1055 + unlink(cacert);
1056 + unlink(servercert);
1057 + unlink(serverkey);
1058 + unlink(clientcert);
1059 + unlink(keyfile);
1060 + test_tls_cleanup(keyfile);
1061 + rmdir(dir);
1062 +}
1063 +
1064 +/*
1065 + * Do a minimal VNC/VeNCrypt negotiation on @fd up to the point where
1066 + * the TLS handshake should begin, then perform a GnuTLS handshake
1067 + * using the given credentials.
1068 + */
1069 +static bool
1070 +try_raw_tls_connect(int fd, gnutls_certificate_credentials_t cred)
1071 +{
1072 + char buf[13];
1073 + uint8_t num_types, type;
1074 + uint8_t vencrypt_ver[2], ack;
1075 + uint8_t num_sub;
1076 + uint32_t subtype;
1077 + gnutls_session_t session;
1078 + int ret;
1079 + bool success;
1080 +
1081 + /* RFB version exchange */
1082 + g_assert_cmpint(read(fd, buf, 12), ==, 12);
1083 + g_assert_cmpint(write(fd, "RFB 003.008\n", 12), ==, 12);
1084 +
1085 + /* Select VeNCrypt (type 19) from the auth list */
1086 + g_assert_cmpint(read(fd, &num_types, 1), ==, 1);
1087 + for (int i = 0; i < num_types; i++) {
1088 + g_assert_cmpint(read(fd, &type, 1), ==, 1);
1089 + }
1090 + type = 19;
1091 + g_assert_cmpint(write(fd, &type, 1), ==, 1);
1092 +
1093 + /* VeNCrypt version exchange */
1094 + g_assert_cmpint(read(fd, vencrypt_ver, 2), ==, 2);
1095 + g_assert_cmpint(write(fd, vencrypt_ver, 2), ==, 2);
1096 + g_assert_cmpint(read(fd, &ack, 1), ==, 1);
1097 + g_assert_cmpint(ack, ==, 0);
1098 +
1099 + /* Select x509-none (260) sub-auth */
1100 + g_assert_cmpint(read(fd, &num_sub, 1), ==, 1);
1101 + for (int i = 0; i < num_sub; i++) {
1102 + g_assert_cmpint(read(fd, &subtype, 4), ==, 4);
1103 + }
1104 + subtype = htonl(260);
1105 + g_assert_cmpint(write(fd, &subtype, 4), ==, 4);
1106 +
1107 + /* Server sends 1-byte ack (1 = accepted) before TLS starts */
1108 + g_assert_cmpint(read(fd, &ack, 1), ==, 1);
1109 + g_assert_cmpint(ack, ==, 1);
1110 +
1111 + /* TLS handshake */
1112 + g_assert_cmpint(gnutls_init(&session, GNUTLS_CLIENT), >=, 0);
1113 + g_assert_cmpint(
1114 + gnutls_set_default_priority(session), >=, 0);
1115 + g_assert_cmpint(
1116 + gnutls_credentials_set(session, GNUTLS_CRD_CERTIFICATE, cred),
1117 + >=, 0);
1118 + gnutls_transport_set_int(session, fd);
1119 +
1120 + do {
1121 + ret = gnutls_handshake(session);
1122 + } while (ret == GNUTLS_E_AGAIN || ret == GNUTLS_E_INTERRUPTED);
1123 +
1124 + if (ret < 0) {
1125 + success = false;
1126 + } else {
1127 + /*
1128 + * Try reading the VNC security-result (4 bytes) — if the
1129 + * server rejected us it will have closed the connection.
1130 + */
1131 + char tmp[4];
1132 + do {
1133 + ret = gnutls_record_recv(session, tmp, sizeof(tmp));
1134 + } while (ret == GNUTLS_E_AGAIN || ret == GNUTLS_E_INTERRUPTED);
1135 + success = (ret > 0);
1136 + }
1137 +
1138 + gnutls_deinit(session);
1139 + return success;
1140 +}
1141 +
1142 +static void
1143 +test_dbus_vnc_tls_server_props(void)
1144 +{
1145 + DbusTest dt = { 0 };
1146 + QemuVnc1Server *proxy = NULL;
1147 + g_autoptr(GError) err = NULL;
1148 + g_autofree char *tls_dir = NULL;
1149 + g_autofree char *keyfile = NULL;
1150 +
1151 + if (!g_getenv("QTEST_QEMU_VNC_BINARY")) {
1152 + g_test_skip("QTEST_QEMU_VNC_BINARY not set");
1153 + return;
1154 + }
1155 +
1156 + tls_dir = g_dir_make_tmp("dbus-vnc-tls-XXXXXX", NULL);
1157 + g_assert_nonnull(tls_dir);
1158 + keyfile = create_tls_certs(tls_dir);
1159 +
1160 + {
1161 + const char *extra_args[] = {
1162 + "--tls-creds", tls_dir, NULL
1163 + };
1164 + if (!setup_dbus_test_full(&dt, extra_args)) {
1165 + goto cleanup;
1166 + }
1167 + }
1168 +
1169 + proxy = create_server_proxy(dt.bus_conn, &err);
1170 + g_assert_no_error(err);
1171 + g_assert_nonnull(proxy);
1172 +
1173 + g_assert_cmpstr(qemu_vnc1_server_get_auth(proxy), ==, "vencrypt");
1174 + g_assert_cmpstr(qemu_vnc1_server_get_vencrypt_sub_auth(proxy), ==,
1175 + "x509-none");
1176 +
1177 + /*
1178 + * With verify-peer=no, a client without a certificate should
1179 + * be able to connect successfully through TLS.
1180 + */
1181 + {
1182 + g_autofree char *ca_path =
1183 + g_strdup_printf("%s/ca-cert.pem", tls_dir);
1184 + gnutls_certificate_credentials_t cred;
1185 + int fd;
1186 +
1187 + g_assert_cmpint(
1188 + gnutls_certificate_allocate_credentials(&cred), >=, 0);
1189 + g_assert_cmpint(
1190 + gnutls_certificate_set_x509_trust_file(
1191 + cred, ca_path, GNUTLS_X509_FMT_PEM), >=, 0);
1192 +
1193 + fd = wait_for_vnc_socket(dt.vnc_sock_path, VNC_TEST_TIMEOUT_MS);
1194 + g_assert(fd >= 0);
1195 + g_assert_true(try_raw_tls_connect(fd, cred));
1196 + close(fd);
1197 +
1198 + gnutls_certificate_free_credentials(cred);
1199 + }
1200 +
1201 +cleanup:
1202 + g_clear_object(&proxy);
1203 + cleanup_dbus_test(&dt);
1204 + cleanup_tls_certs(tls_dir, keyfile);
1205 +}
1206 +
1207 +static void
1208 +test_dbus_vnc_tls_authz(void)
1209 +{
1210 + DbusTest dt = { 0 };
1211 + g_autofree char *tls_dir = NULL;
1212 + g_autofree char *keyfile = NULL;
1213 + g_autofree char *ca_path = NULL;
1214 +
1215 + if (!g_getenv("QTEST_QEMU_VNC_BINARY")) {
1216 + g_test_skip("QTEST_QEMU_VNC_BINARY not set");
1217 + return;
1218 + }
1219 +
1220 + tls_dir = g_dir_make_tmp("dbus-vnc-tls-XXXXXX", NULL);
1221 + g_assert_nonnull(tls_dir);
1222 + keyfile = create_tls_certs(tls_dir);
1223 +
1224 + /*
1225 + * The client cert has CN=qemu-vnc-test, so the DN string
1226 + * reported by GnuTLS is "CN=qemu-vnc-test". Configure
1227 + * authz-simple to accept exactly that identity.
1228 + */
1229 + {
1230 + g_autofree char *identity =
1231 + g_strdup_printf("CN=%s", CLIENT_CERT_CN);
1232 + const char *extra_args[] = {
1233 + "--tls-creds", tls_dir,
1234 + "--object",
1235 + NULL, /* filled below */
1236 + "--tls-authz", "authz0",
1237 + NULL
1238 + };
1239 + g_autofree char *object_arg =
1240 + g_strdup_printf("authz-simple,id=authz0,identity=%s", identity);
1241 + extra_args[3] = object_arg;
1242 +
1243 + if (!setup_dbus_test_full(&dt, extra_args)) {
1244 + goto cleanup;
1245 + }
1246 + }
1247 +
1248 + ca_path = g_strdup_printf("%s/ca-cert.pem", tls_dir);
1249 +
1250 + /*
1251 + * Connect without a client certificate.
1252 + * With verify-peer=yes the TLS handshake must fail.
1253 + */
1254 + {
1255 + gnutls_certificate_credentials_t cred;
1256 + int fd;
1257 +
1258 + g_assert_cmpint(
1259 + gnutls_certificate_allocate_credentials(&cred), >=, 0);
1260 + g_assert_cmpint(
1261 + gnutls_certificate_set_x509_trust_file(
1262 + cred, ca_path, GNUTLS_X509_FMT_PEM), >=, 0);
1263 +
1264 + fd = wait_for_vnc_socket(dt.vnc_sock_path, VNC_TEST_TIMEOUT_MS);
1265 + g_assert(fd >= 0);
1266 + g_assert_false(try_raw_tls_connect(fd, cred));
1267 + close(fd);
1268 +
1269 + gnutls_certificate_free_credentials(cred);
1270 + }
1271 +
1272 + /*
1273 + * Connect with a valid client certificate whose DN
1274 + * matches the authz-simple identity. This must succeed.
1275 + */
1276 + {
1277 + g_autofree char *cert_path =
1278 + g_strdup_printf("%s/client-cert.pem", tls_dir);
1279 + g_autofree char *key_path =
1280 + g_strdup_printf("%s/key.pem", tls_dir);
1281 + gnutls_certificate_credentials_t cred;
1282 + int fd;
1283 +
1284 + g_assert_cmpint(
1285 + gnutls_certificate_allocate_credentials(&cred), >=, 0);
1286 + g_assert_cmpint(
1287 + gnutls_certificate_set_x509_trust_file(
1288 + cred, ca_path, GNUTLS_X509_FMT_PEM), >=, 0);
1289 + g_assert_cmpint(
1290 + gnutls_certificate_set_x509_key_file(
1291 + cred, cert_path, key_path, GNUTLS_X509_FMT_PEM), >=, 0);
1292 +
1293 + fd = wait_for_vnc_socket(dt.vnc_sock_path, VNC_TEST_TIMEOUT_MS);
1294 + g_assert(fd >= 0);
1295 + g_assert_true(try_raw_tls_connect(fd, cred));
1296 + close(fd);
1297 +
1298 + gnutls_certificate_free_credentials(cred);
1299 + }
1300 +
1301 +cleanup:
1302 + cleanup_dbus_test(&dt);
1303 + cleanup_tls_certs(tls_dir, keyfile);
1304 +}
1305 +#endif /* CONFIG_TASN1 */
1306 +
1307 +int
1308 +main(int argc, char **argv)
1309 +{
1310 + g_log_set_always_fatal(G_LOG_LEVEL_WARNING | G_LOG_LEVEL_CRITICAL);
1311 +
1312 + if (getenv("GTK_VNC_DEBUG")) {
1313 + vnc_util_set_debug(true);
1314 + }
1315 +
1316 + g_test_init(&argc, &argv, NULL);
1317 +
1318 + qtest_add_func("/dbus-vnc/basic", test_dbus_vnc_basic);
1319 + qtest_add_func("/dbus-vnc/server-props", test_dbus_vnc_server_props);
1320 + qtest_add_func("/dbus-vnc/client-lifecycle",
1321 + test_dbus_vnc_client_lifecycle);
1322 + qtest_add_func("/dbus-vnc/no-password", test_dbus_vnc_no_password);
1323 + qtest_add_func("/dbus-vnc/password-auth", test_dbus_vnc_password_auth);
1324 + qtest_add_func("/dbus-vnc/sasl-authz-no-sasl",
1325 + test_dbus_vnc_sasl_authz_no_sasl);
1326 +#ifdef CONFIG_VNC_SASL
1327 + qtest_add_func("/dbus-vnc/sasl-server-props",
1328 + test_dbus_vnc_sasl_server_props);
1329 + qtest_add_func("/dbus-vnc/sasl-auth",
1330 + test_dbus_vnc_sasl_auth);
1331 + qtest_add_func("/dbus-vnc/sasl-auth-bad-password",
1332 + test_dbus_vnc_sasl_auth_bad_password);
1333 + qtest_add_func("/dbus-vnc/sasl-authz-denied",
1334 + test_dbus_vnc_sasl_authz_denied);
1335 +#endif
1336 + qtest_add_func("/dbus-vnc/tls-authz-no-creds",
1337 + test_dbus_vnc_tls_authz_no_creds);
1338 +#ifdef CONFIG_TASN1
1339 + qtest_add_func("/dbus-vnc/tls-server-props",
1340 + test_dbus_vnc_tls_server_props);
1341 + qtest_add_func("/dbus-vnc/tls-authz",
1342 + test_dbus_vnc_tls_authz);
1343 +#endif
1344 +
1345 + return g_test_run();
1346 +}
tests/qtest/meson.build
+13
@@ -414,6 +414,15 @@ if vnc.found()
414 if gvnc.found()
415 qtests += {'vnc-display-test': [gvnc, keymap_targets]}
416 qtests_generic += [ 'vnc-display-test' ]
417 + if have_qemu_vnc and dbus_display and config_all_devices.has_key('CONFIG_VGA')
418 + dbus_vnc_test_deps = [dbus_display1, qemu_vnc1, gio, gvnc, keymap_targets]
419 + if gnutls.found() and tasn1.found()
420 + dbus_vnc_test_deps += [files('../unit/crypto-tls-x509-helpers.c'),
421 + gnutls, tasn1]
422 + endif
423 + qtests += {'dbus-vnc-test': dbus_vnc_test_deps}
424 + qtests_x86_64 += ['dbus-vnc-test']
425 + endif
426 endif
427 endif
428
@@ -445,6 +454,10 @@ foreach dir : target_dirs
454 qtest_env.set('QTEST_QEMU_STORAGE_DAEMON_BINARY', './storage-daemon/qemu-storage-daemon')
455 test_deps += [qsd]
456 endif
457 + if have_qemu_vnc
458 + qtest_env.set('QTEST_QEMU_VNC_BINARY', './tools/qemu-vnc/qemu-vnc')
459 + test_deps += [qemu_vnc]
460 + endif
461
462 qtest_env.set('PYTHON', python.full_path())
463
tools/qemu-vnc/audio.c new
+308
@@ -0,0 +1,308 @@
1 +/*
2 + * Standalone VNC server connecting to QEMU via D-Bus display interface.
3 + * Audio support. Only one audio stream is tracked.
4 + * Mixing/resampling to be added, if needed.
5 + *
6 + * Copyright (C) 2026 Red Hat, Inc.
7 + *
8 + * SPDX-License-Identifier: GPL-2.0-or-later
9 + */
10 +
11 +#include "qemu/osdep.h"
12 +
13 +#include "qemu/audio.h"
14 +#include "qemu/audio-capture.h"
15 +#include "qemu/sockets.h"
16 +#include "qemu/error-report.h"
17 +#include "ui/dbus-display1.h"
18 +#include "trace.h"
19 +#include "qemu-vnc.h"
20 +
21 +struct CaptureVoiceOut {
22 + struct audsettings as;
23 + struct audio_capture_ops ops;
24 + void *opaque;
25 + QLIST_ENTRY(CaptureVoiceOut) entries;
26 +};
27 +
28 +typedef struct AudioOut {
29 + guint64 id;
30 + struct audsettings as;
31 +} AudioOut;
32 +
33 +static QLIST_HEAD(, CaptureVoiceOut) capture_list =
34 + QLIST_HEAD_INITIALIZER(capture_list);
35 +static GDBusConnection *audio_listener_conn;
36 +static AudioOut audio_out;
37 +
38 +static bool audsettings_eq(const struct audsettings *a,
39 + const struct audsettings *b)
40 +{
41 + return a->freq == b->freq &&
42 + a->nchannels == b->nchannels &&
43 + a->fmt == b->fmt &&
44 + a->big_endian == b->big_endian;
45 +}
46 +
47 +static gboolean
48 +on_audio_out_init(QemuDBusDisplay1AudioOutListener *listener,
49 + GDBusMethodInvocation *invocation,
50 + guint64 id, guchar bits, gboolean is_signed,
51 + gboolean is_float, guint freq, guchar nchannels,
52 + guint bytes_per_frame, guint bytes_per_second,
53 + gboolean be, gpointer user_data)
54 +{
55 + AudioFormat fmt;
56 +
57 + switch (bits) {
58 + case 8:
59 + fmt = is_signed ? AUDIO_FORMAT_S8 : AUDIO_FORMAT_U8;
60 + break;
61 + case 16:
62 + fmt = is_signed ? AUDIO_FORMAT_S16 : AUDIO_FORMAT_U16;
63 + break;
64 + case 32:
65 + fmt = is_float ? AUDIO_FORMAT_F32 :
66 + is_signed ? AUDIO_FORMAT_S32 : AUDIO_FORMAT_U32;
67 + break;
68 + default:
69 + g_return_val_if_reached(DBUS_METHOD_INVOCATION_HANDLED);
70 + }
71 +
72 + struct audsettings as = {
73 + .freq = freq,
74 + .nchannels = nchannels,
75 + .fmt = fmt,
76 + .big_endian = be,
77 + };
78 + audio_out = (AudioOut) {
79 + .id = id,
80 + .as = as,
81 + };
82 +
83 + trace_qemu_vnc_audio_out_init(id, freq, nchannels, bits);
84 +
85 + qemu_dbus_display1_audio_out_listener_complete_init(
86 + listener, invocation);
87 + return DBUS_METHOD_INVOCATION_HANDLED;
88 +}
89 +
90 +static gboolean
91 +on_audio_out_fini(QemuDBusDisplay1AudioOutListener *listener,
92 + GDBusMethodInvocation *invocation,
93 + guint64 id, gpointer user_data)
94 +{
95 + trace_qemu_vnc_audio_out_fini(id);
96 +
97 + qemu_dbus_display1_audio_out_listener_complete_fini(
98 + listener, invocation);
99 + return DBUS_METHOD_INVOCATION_HANDLED;
100 +}
101 +
102 +static gboolean
103 +on_audio_out_set_enabled(QemuDBusDisplay1AudioOutListener *listener,
104 + GDBusMethodInvocation *invocation,
105 + guint64 id, gboolean enabled,
106 + gpointer user_data)
107 +{
108 + CaptureVoiceOut *cap;
109 +
110 + trace_qemu_vnc_audio_out_set_enabled(id, enabled);
111 +
112 + if (id == audio_out.id) {
113 + QLIST_FOREACH(cap, &capture_list, entries) {
114 + cap->ops.notify(cap->opaque,
115 + enabled ? AUD_CNOTIFY_ENABLE
116 + : AUD_CNOTIFY_DISABLE);
117 + }
118 + }
119 +
120 + qemu_dbus_display1_audio_out_listener_complete_set_enabled(
121 + listener, invocation);
122 + return DBUS_METHOD_INVOCATION_HANDLED;
123 +}
124 +
125 +static gboolean
126 +on_audio_out_set_volume(QemuDBusDisplay1AudioOutListener *listener,
127 + GDBusMethodInvocation *invocation,
128 + guint64 id, gboolean mute,
129 + GVariant *volume, gpointer user_data)
130 +{
131 + qemu_dbus_display1_audio_out_listener_complete_set_volume(
132 + listener, invocation);
133 + return DBUS_METHOD_INVOCATION_HANDLED;
134 +}
135 +
136 +static gboolean
137 +on_audio_out_write(QemuDBusDisplay1AudioOutListener *listener,
138 + GDBusMethodInvocation *invocation,
139 + guint64 id, GVariant *data,
140 + gpointer user_data)
141 +{
142 + CaptureVoiceOut *cap;
143 + gsize size;
144 + const void *buf;
145 +
146 + if (id == audio_out.id) {
147 + buf = g_variant_get_fixed_array(data, &size, 1);
148 +
149 + trace_qemu_vnc_audio_out_write(id, size);
150 +
151 + QLIST_FOREACH(cap, &capture_list, entries) {
152 + /* we don't handle audio resampling/format conversion */
153 + if (audsettings_eq(&cap->as, &audio_out.as)) {
154 + cap->ops.capture(cap->opaque, buf, size);
155 + }
156 + }
157 + }
158 +
159 + qemu_dbus_display1_audio_out_listener_complete_write(
160 + listener, invocation);
161 + return DBUS_METHOD_INVOCATION_HANDLED;
162 +}
163 +
164 +CaptureVoiceOut *audio_be_add_capture(
165 + AudioBackend *be,
166 + const struct audsettings *as,
167 + const struct audio_capture_ops *ops,
168 + void *opaque)
169 +{
170 + CaptureVoiceOut *cap;
171 +
172 + if (!audio_listener_conn) {
173 + return NULL;
174 + }
175 +
176 + cap = g_new0(CaptureVoiceOut, 1);
177 + cap->ops = *ops;
178 + cap->opaque = opaque;
179 + cap->as = *as;
180 + QLIST_INSERT_HEAD(&capture_list, cap, entries);
181 +
182 + return cap;
183 +}
184 +
185 +void audio_be_del_capture(
186 + AudioBackend *be,
187 + CaptureVoiceOut *cap,
188 + void *cb_opaque)
189 +{
190 + if (!cap) {
191 + return;
192 + }
193 +
194 + cap->ops.destroy(cap->opaque);
195 + QLIST_REMOVE(cap, entries);
196 + g_free(cap);
197 +}
198 +
199 +/*
200 + * Dummy audio backend — the VNC server only needs a non-NULL pointer
201 + * so that audio capture registration doesn't bail out. The pointer
202 + * is never dereferenced by our code (audio_be_add_capture ignores it).
203 + */
204 +static AudioBackend dummy_audio_be;
205 +
206 +AudioBackend *audio_get_default_audio_be(Error **errp)
207 +{
208 + return &dummy_audio_be;
209 +}
210 +
211 +AudioBackend *audio_be_by_name(const char *name, Error **errp)
212 +{
213 + return NULL;
214 +}
215 +
216 +static void
217 +on_register_audio_listener_finished(GObject *source_object,
218 + GAsyncResult *res,
219 + gpointer user_data)
220 +{
221 + GThread *thread = user_data;
222 + g_autoptr(GError) err = NULL;
223 + g_autoptr(GDBusObjectSkeleton) obj = NULL;
224 + GDBusObjectManagerServer *server;
225 + QemuDBusDisplay1AudioOutListener *audio_skel;
226 +
227 + qemu_dbus_display1_audio_call_register_out_listener_finish(
228 + QEMU_DBUS_DISPLAY1_AUDIO(source_object),
229 + NULL, res, &err);
230 +
231 + if (err) {
232 + error_report("RegisterOutListener failed: %s", err->message);
233 + g_thread_join(thread);
234 + return;
235 + }
236 +
237 + audio_listener_conn = g_thread_join(thread);
238 + if (!audio_listener_conn) {
239 + return;
240 + }
241 +
242 + server = g_dbus_object_manager_server_new(DBUS_DISPLAY1_ROOT);
243 + obj = g_dbus_object_skeleton_new(
244 + DBUS_DISPLAY1_ROOT "/AudioOutListener");
245 +
246 + audio_skel = qemu_dbus_display1_audio_out_listener_skeleton_new();
247 + g_object_connect(audio_skel,
248 + "signal::handle-init",
249 + on_audio_out_init, NULL,
250 + "signal::handle-fini",
251 + on_audio_out_fini, NULL,
252 + "signal::handle-set-enabled",
253 + on_audio_out_set_enabled, NULL,
254 + "signal::handle-set-volume",
255 + on_audio_out_set_volume, NULL,
256 + "signal::handle-write",
257 + on_audio_out_write, NULL,
258 + NULL);
259 + g_dbus_object_skeleton_add_interface(
260 + obj, G_DBUS_INTERFACE_SKELETON(audio_skel));
261 +
262 + g_dbus_object_manager_server_export(server, obj);
263 + g_dbus_object_manager_server_set_connection(
264 + server, audio_listener_conn);
265 +
266 + g_dbus_connection_start_message_processing(audio_listener_conn);
267 +}
268 +
269 +void audio_setup(GDBusObjectManager *manager)
270 +{
271 + g_autoptr(GError) err = NULL;
272 + g_autoptr(GUnixFDList) fd_list = NULL;
273 + g_autoptr(GDBusInterface) iface = NULL;
274 + GThread *thread;
275 + int pair[2];
276 + int idx;
277 +
278 + iface = g_dbus_object_manager_get_interface(
279 + manager, DBUS_DISPLAY1_ROOT "/Audio",
280 + "org.qemu.Display1.Audio");
281 + if (!iface) {
282 + return;
283 + }
284 +
285 + if (qemu_socketpair(AF_UNIX, SOCK_STREAM, 0, pair) < 0) {
286 + error_report("audio socketpair failed: %s", strerror(errno));
287 + return;
288 + }
289 +
290 + fd_list = g_unix_fd_list_new();
291 + idx = g_unix_fd_list_append(fd_list, pair[1], &err);
292 + close(pair[1]);
293 + if (idx < 0) {
294 + close(pair[0]);
295 + error_report("Failed to append fd: %s", err->message);
296 + return;
297 + }
298 +
299 + thread = p2p_dbus_thread_new(pair[0]);
300 +
301 + qemu_dbus_display1_audio_call_register_out_listener(
302 + QEMU_DBUS_DISPLAY1_AUDIO(iface),
303 + g_variant_new_handle(idx),
304 + G_DBUS_CALL_FLAGS_NONE, -1,
305 + fd_list, NULL,
306 + on_register_audio_listener_finished,
307 + thread);
308 +}
tools/qemu-vnc/chardev.c new
+148
@@ -0,0 +1,148 @@
1 +/*
2 + * Standalone VNC server connecting to QEMU via D-Bus display interface.
3 + *
4 + * Copyright (C) 2026 Red Hat, Inc.
5 + *
6 + * SPDX-License-Identifier: GPL-2.0-or-later
7 + */
8 +
9 +#include "qemu/osdep.h"
10 +
11 +#include "qemu/sockets.h"
12 +#include "qemu/error-report.h"
13 +#include "qapi/util.h"
14 +#include "qapi-types-char.h"
15 +#include "ui/dbus-display1.h"
16 +#include "trace.h"
17 +#include "qemu-vnc.h"
18 +
19 +typedef struct ChardevRegisterData {
20 + QemuDBusDisplay1Chardev *proxy;
21 + int local_fd;
22 + char *name;
23 + bool echo;
24 + ChardevVCEncoding encoding;
25 +} ChardevRegisterData;
26 +
27 +static void
28 +on_chardev_register_finished(GObject *source_object,
29 + GAsyncResult *res,
30 + gpointer user_data)
31 +{
32 + ChardevRegisterData *data = user_data;
33 + g_autoptr(GError) err = NULL;
34 + QemuTextConsole *tc;
35 +
36 + if (!qemu_dbus_display1_chardev_call_register_finish(
37 + data->proxy, NULL, res, &err)) {
38 + error_report("Chardev Register failed for %s: %s",
39 + data->name, err->message);
40 + close(data->local_fd);
41 + goto out;
42 + }
43 +
44 + tc = qemu_vnc_text_console_new(data->name, data->local_fd, data->echo,
45 + data->encoding);
46 + if (!tc) {
47 + close(data->local_fd);
48 + goto out;
49 + }
50 +
51 + trace_qemu_vnc_chardev_connected(data->name);
52 +
53 +out:
54 + g_object_unref(data->proxy);
55 + g_free(data->name);
56 + g_free(data);
57 +}
58 +
59 +/* Default chardevs to expose as VNC text consoles */
60 +static const char * const default_names[] = {
61 + "org.qemu.console.serial.0",
62 + "org.qemu.monitor.hmp.0",
63 + NULL,
64 +};
65 +
66 +/* Active chardev names list (points to CLI args or default_names) */
67 +static const char * const *names;
68 +
69 +static void
70 +chardev_register(QemuDBusDisplay1Chardev *proxy,
71 + ChardevVCEncoding encoding)
72 +{
73 + g_autoptr(GUnixFDList) fd_list = NULL;
74 + ChardevRegisterData *data;
75 + const char *name;
76 + int pair[2];
77 + int idx;
78 +
79 + name = qemu_dbus_display1_chardev_get_name(proxy);
80 + if (!name || !g_strv_contains(names, name)) {
81 + return;
82 + }
83 +
84 + if (qemu_socketpair(AF_UNIX, SOCK_STREAM, 0, pair) < 0) {
85 + error_report("chardev socketpair failed: %s", strerror(errno));
86 + return;
87 + }
88 +
89 + fd_list = g_unix_fd_list_new();
90 + idx = g_unix_fd_list_append(fd_list, pair[1], NULL);
91 + close(pair[1]);
92 +
93 + data = g_new0(ChardevRegisterData, 1);
94 + data->proxy = g_object_ref(proxy);
95 + data->local_fd = pair[0];
96 + data->name = g_strdup(name);
97 + data->echo = qemu_dbus_display1_chardev_get_echo(proxy);
98 + data->encoding = encoding;
99 +
100 + qemu_dbus_display1_chardev_call_register(
101 + proxy, g_variant_new_handle(idx),
102 + G_DBUS_CALL_FLAGS_NONE, -1,
103 + fd_list, NULL,
104 + on_chardev_register_finished, data);
105 +}
106 +
107 +void chardev_setup(const char * const *chardev_names,
108 + GDBusObjectManager *manager)
109 +{
110 + GList *objects, *l;
111 +
112 + names = chardev_names ? chardev_names : default_names;
113 +
114 + objects = g_dbus_object_manager_get_objects(manager);
115 + for (l = objects; l; l = l->next) {
116 + GDBusObject *obj = l->data;
117 + const char *path = g_dbus_object_get_object_path(obj);
118 + g_autoptr(GDBusInterface) iface = NULL;
119 + g_autoptr(GDBusInterface) enc_iface = NULL;
120 + ChardevVCEncoding encoding = CHARDEV_VC_ENCODING_UTF8;
121 +
122 + if (!g_str_has_prefix(path, DBUS_DISPLAY1_ROOT "/Chardev_")) {
123 + continue;
124 + }
125 +
126 + iface = g_dbus_object_get_interface(
127 + obj, "org.qemu.Display1.Chardev");
128 + if (!iface) {
129 + continue;
130 + }
131 +
132 + enc_iface = g_dbus_object_get_interface(
133 + obj, "org.qemu.Display1.Chardev.VCEncoding");
134 + if (enc_iface) {
135 + const char *enc_str =
136 + qemu_dbus_display1_chardev_vcencoding_get_encoding(
137 + QEMU_DBUS_DISPLAY1_CHARDEV_VCENCODING(enc_iface));
138 + int enc = qapi_enum_parse(&ChardevVCEncoding_lookup,
139 + enc_str, -1, NULL);
140 + if (enc >= 0) {
141 + encoding = enc;
142 + }
143 + }
144 +
145 + chardev_register(QEMU_DBUS_DISPLAY1_CHARDEV(iface), encoding);
146 + }
147 + g_list_free_full(objects, g_object_unref);
148 +}
tools/qemu-vnc/clipboard.c new
+376
@@ -0,0 +1,376 @@
1 +/*
2 + * Standalone VNC server connecting to QEMU via D-Bus display interface.
3 + *
4 + * Copyright (C) 2026 Red Hat, Inc.
5 + *
6 + * SPDX-License-Identifier: GPL-2.0-or-later
7 + */
8 +
9 +#include "qemu/osdep.h"
10 +
11 +#include "qemu/error-report.h"
12 +#include "ui/clipboard.h"
13 +#include "ui/dbus-display1.h"
14 +#include "trace.h"
15 +#include "qemu-vnc.h"
16 +
17 +#define MIME_TEXT_PLAIN_UTF8 "text/plain;charset=utf-8"
18 +
19 +typedef struct {
20 + GDBusMethodInvocation *invocation;
21 + QemuClipboardType type;
22 + guint timeout_id;
23 +} VncDBusClipboardRequest;
24 +
25 +static QemuDBusDisplay1Clipboard *clipboard_proxy;
26 +static QemuDBusDisplay1Clipboard *clipboard_skel;
27 +static QemuClipboardPeer clipboard_peer;
28 +static uint32_t clipboard_serial;
29 +static VncDBusClipboardRequest
30 + clipboard_request[QEMU_CLIPBOARD_SELECTION__COUNT];
31 +
32 +static void
33 +vnc_dbus_clipboard_complete_request(
34 + GDBusMethodInvocation *invocation,
35 + QemuClipboardInfo *info,
36 + QemuClipboardType type)
37 +{
38 + GVariant *v_data = g_variant_new_from_data(
39 + G_VARIANT_TYPE("ay"),
40 + info->types[type].data,
41 + info->types[type].size,
42 + TRUE,
43 + (GDestroyNotify)qemu_clipboard_info_unref,
44 + qemu_clipboard_info_ref(info));
45 +
46 + qemu_dbus_display1_clipboard_complete_request(
47 + clipboard_skel, invocation,
48 + MIME_TEXT_PLAIN_UTF8, v_data);
49 +}
50 +
51 +static void
52 +vnc_dbus_clipboard_request_cancelled(VncDBusClipboardRequest *req)
53 +{
54 + if (!req->invocation) {
55 + return;
56 + }
57 +
58 + g_dbus_method_invocation_return_error(
59 + req->invocation,
60 + G_DBUS_ERROR,
61 + G_DBUS_ERROR_FAILED,
62 + "Cancelled clipboard request");
63 +
64 + g_clear_object(&req->invocation);
65 + g_clear_handle_id(&req->timeout_id, g_source_remove);;
66 +}
67 +
68 +static gboolean
69 +vnc_dbus_clipboard_request_timeout(gpointer user_data)
70 +{
71 + vnc_dbus_clipboard_request_cancelled(user_data);
72 + return G_SOURCE_REMOVE;
73 +}
74 +
75 +static void
76 +vnc_dbus_clipboard_request(QemuClipboardInfo *info,
77 + QemuClipboardType type)
78 +{
79 + g_autofree char *mime = NULL;
80 + g_autoptr(GVariant) v_data = NULL;
81 + g_autoptr(GError) err = NULL;
82 + const char *data = NULL;
83 + const char *mimes[] = { MIME_TEXT_PLAIN_UTF8, NULL };
84 + size_t n;
85 +
86 + if (type != QEMU_CLIPBOARD_TYPE_TEXT) {
87 + return;
88 + }
89 +
90 + if (!clipboard_proxy) {
91 + return;
92 + }
93 +
94 + if (!qemu_dbus_display1_clipboard_call_request_sync(
95 + clipboard_proxy,
96 + info->selection,
97 + mimes,
98 + G_DBUS_CALL_FLAGS_NONE, -1, &mime, &v_data, NULL, &err)) {
99 + error_report("Failed to request clipboard: %s", err->message);
100 + return;
101 + }
102 +
103 + if (!g_str_equal(mime, MIME_TEXT_PLAIN_UTF8)) {
104 + error_report("Unsupported returned MIME: %s", mime);
105 + return;
106 + }
107 +
108 + data = g_variant_get_fixed_array(v_data, &n, 1);
109 + qemu_clipboard_set_data(&clipboard_peer, info, type,
110 + n, data, true);
111 +}
112 +
113 +static void
114 +vnc_dbus_clipboard_update_info(QemuClipboardInfo *info)
115 +{
116 + bool self_update = info->owner == &clipboard_peer;
117 + const char *mime[QEMU_CLIPBOARD_TYPE__COUNT + 1] = { 0, };
118 + VncDBusClipboardRequest *req;
119 + int i = 0;
120 +
121 + if (info->owner == NULL) {
122 + if (clipboard_proxy) {
123 + qemu_dbus_display1_clipboard_call_release(
124 + clipboard_proxy,
125 + info->selection,
126 + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
127 + }
128 + return;
129 + }
130 +
131 + if (self_update) {
132 + return;
133 + }
134 +
135 + req = &clipboard_request[info->selection];
136 + if (req->invocation && info->types[req->type].data) {
137 + vnc_dbus_clipboard_complete_request(
138 + req->invocation, info, req->type);
139 + g_clear_object(&req->invocation);
140 + g_clear_handle_id(&req->timeout_id, g_source_remove);;
141 + return;
142 + }
143 +
144 + if (info->types[QEMU_CLIPBOARD_TYPE_TEXT].available) {
145 + mime[i++] = MIME_TEXT_PLAIN_UTF8;
146 + }
147 +
148 + if (i > 0 && clipboard_proxy) {
149 + uint32_t serial = info->has_serial ?
150 + info->serial : ++clipboard_serial;
151 + qemu_dbus_display1_clipboard_call_grab(
152 + clipboard_proxy,
153 + info->selection,
154 + serial,
155 + mime,
156 + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
157 + }
158 +}
159 +
160 +static void
161 +vnc_dbus_clipboard_notify(Notifier *notifier, void *data)
162 +{
163 + QemuClipboardNotify *notify = data;
164 +
165 + switch (notify->type) {
166 + case QEMU_CLIPBOARD_UPDATE_INFO:
167 + vnc_dbus_clipboard_update_info(notify->info);
168 + return;
169 + case QEMU_CLIPBOARD_RESET_SERIAL:
170 + if (clipboard_proxy) {
171 + qemu_dbus_display1_clipboard_call_register(
172 + clipboard_proxy,
173 + G_DBUS_CALL_FLAGS_NONE,
174 + -1, NULL, NULL, NULL);
175 + }
176 + return;
177 + }
178 +}
179 +
180 +static gboolean
181 +on_clipboard_register(QemuDBusDisplay1Clipboard *clipboard,
182 + GDBusMethodInvocation *invocation,
183 + gpointer user_data)
184 +{
185 + clipboard_serial = 0;
186 + qemu_clipboard_reset_serial();
187 +
188 + qemu_dbus_display1_clipboard_complete_register(
189 + clipboard, invocation);
190 + return DBUS_METHOD_INVOCATION_HANDLED;
191 +}
192 +
193 +static gboolean
194 +on_clipboard_unregister(QemuDBusDisplay1Clipboard *clipboard,
195 + GDBusMethodInvocation *invocation,
196 + gpointer user_data)
197 +{
198 + int i;
199 +
200 + for (i = 0; i < G_N_ELEMENTS(clipboard_request); ++i) {
201 + vnc_dbus_clipboard_request_cancelled(&clipboard_request[i]);
202 + }
203 +
204 + qemu_dbus_display1_clipboard_complete_unregister(
205 + clipboard, invocation);
206 + return DBUS_METHOD_INVOCATION_HANDLED;
207 +}
208 +
209 +static gboolean
210 +on_clipboard_grab(QemuDBusDisplay1Clipboard *clipboard,
211 + GDBusMethodInvocation *invocation,
212 + gint arg_selection,
213 + guint arg_serial,
214 + const gchar *const *arg_mimes,
215 + gpointer user_data)
216 +{
217 + QemuClipboardSelection s = arg_selection;
218 + g_autoptr(QemuClipboardInfo) info = NULL;
219 +
220 + if (s >= QEMU_CLIPBOARD_SELECTION__COUNT) {
221 + g_dbus_method_invocation_return_error(
222 + invocation,
223 + G_DBUS_ERROR,
224 + G_DBUS_ERROR_FAILED,
225 + "Invalid clipboard selection: %d", arg_selection);
226 + return DBUS_METHOD_INVOCATION_HANDLED;
227 + }
228 +
229 + trace_qemu_vnc_clipboard_grab(arg_selection, arg_serial);
230 +
231 + info = qemu_clipboard_info_new(&clipboard_peer, s);
232 + if (g_strv_contains(arg_mimes, MIME_TEXT_PLAIN_UTF8)) {
233 + info->types[QEMU_CLIPBOARD_TYPE_TEXT].available = true;
234 + }
235 + info->serial = arg_serial;
236 + info->has_serial = true;
237 + if (qemu_clipboard_check_serial(info, true)) {
238 + qemu_clipboard_update(info);
239 + }
240 +
241 + qemu_dbus_display1_clipboard_complete_grab(
242 + clipboard, invocation);
243 + return DBUS_METHOD_INVOCATION_HANDLED;
244 +}
245 +
246 +static gboolean
247 +on_clipboard_release(QemuDBusDisplay1Clipboard *clipboard,
248 + GDBusMethodInvocation *invocation,
249 + gint arg_selection,
250 + gpointer user_data)
251 +{
252 + trace_qemu_vnc_clipboard_release(arg_selection);
253 +
254 + qemu_clipboard_peer_release(&clipboard_peer, arg_selection);
255 +
256 + qemu_dbus_display1_clipboard_complete_release(
257 + clipboard, invocation);
258 + return DBUS_METHOD_INVOCATION_HANDLED;
259 +}
260 +
261 +static gboolean
262 +on_clipboard_request(QemuDBusDisplay1Clipboard *clipboard,
263 + GDBusMethodInvocation *invocation,
264 + gint arg_selection,
265 + const gchar *const *arg_mimes,
266 + gpointer user_data)
267 +{
268 + QemuClipboardSelection s = arg_selection;
269 + QemuClipboardType type = QEMU_CLIPBOARD_TYPE_TEXT;
270 + QemuClipboardInfo *info = NULL;
271 +
272 + trace_qemu_vnc_clipboard_request(arg_selection);
273 +
274 + if (s >= QEMU_CLIPBOARD_SELECTION__COUNT) {
275 + g_dbus_method_invocation_return_error(
276 + invocation,
277 + G_DBUS_ERROR,
278 + G_DBUS_ERROR_FAILED,
279 + "Invalid clipboard selection: %d", arg_selection);
280 + return DBUS_METHOD_INVOCATION_HANDLED;
281 + }
282 +
283 + if (clipboard_request[s].invocation) {
284 + g_dbus_method_invocation_return_error(
285 + invocation,
286 + G_DBUS_ERROR,
287 + G_DBUS_ERROR_FAILED,
288 + "Pending request");
289 + return DBUS_METHOD_INVOCATION_HANDLED;
290 + }
291 +
292 + info = qemu_clipboard_info(s);
293 + if (!info || !info->owner || info->owner == &clipboard_peer) {
294 + g_dbus_method_invocation_return_error(
295 + invocation,
296 + G_DBUS_ERROR,
297 + G_DBUS_ERROR_FAILED,
298 + "Empty clipboard");
299 + return DBUS_METHOD_INVOCATION_HANDLED;
300 + }
301 +
302 + if (!g_strv_contains(arg_mimes, MIME_TEXT_PLAIN_UTF8) ||
303 + !info->types[type].available) {
304 + g_dbus_method_invocation_return_error(
305 + invocation,
306 + G_DBUS_ERROR,
307 + G_DBUS_ERROR_FAILED,
308 + "Unhandled MIME types requested");
309 + return DBUS_METHOD_INVOCATION_HANDLED;
310 + }
311 +
312 + if (info->types[type].data) {
313 + vnc_dbus_clipboard_complete_request(invocation, info, type);
314 + } else {
315 + qemu_clipboard_request(info, type);
316 +
317 + clipboard_request[s].invocation = g_object_ref(invocation);
318 + clipboard_request[s].type = type;
319 + clipboard_request[s].timeout_id =
320 + g_timeout_add_seconds(5,
321 + vnc_dbus_clipboard_request_timeout,
322 + &clipboard_request[s]);
323 + }
324 +
325 + return DBUS_METHOD_INVOCATION_HANDLED;
326 +}
327 +
328 +void clipboard_setup(GDBusObjectManager *manager, GDBusConnection *bus)
329 +{
330 + g_autoptr(GError) err = NULL;
331 + g_autoptr(GDBusInterface) iface = NULL;
332 +
333 + iface = g_dbus_object_manager_get_interface(
334 + manager, DBUS_DISPLAY1_ROOT "/Clipboard",
335 + "org.qemu.Display1.Clipboard");
336 + if (!iface) {
337 + return;
338 + }
339 +
340 + clipboard_proxy = g_object_ref(QEMU_DBUS_DISPLAY1_CLIPBOARD(iface));
341 +
342 + clipboard_skel = qemu_dbus_display1_clipboard_skeleton_new();
343 + g_object_connect(clipboard_skel,
344 + "signal::handle-register",
345 + on_clipboard_register, NULL,
346 + "signal::handle-unregister",
347 + on_clipboard_unregister, NULL,
348 + "signal::handle-grab",
349 + on_clipboard_grab, NULL,
350 + "signal::handle-release",
351 + on_clipboard_release, NULL,
352 + "signal::handle-request",
353 + on_clipboard_request, NULL,
354 + NULL);
355 +
356 + if (!g_dbus_interface_skeleton_export(
357 + G_DBUS_INTERFACE_SKELETON(clipboard_skel),
358 + bus,
359 + DBUS_DISPLAY1_ROOT "/Clipboard",
360 + &err)) {
361 + error_report("Failed to export clipboard: %s", err->message);
362 + g_clear_object(&clipboard_skel);
363 + g_clear_object(&clipboard_proxy);
364 + return;
365 + }
366 +
367 + clipboard_peer.name = "dbus";
368 + clipboard_peer.notifier.notify = vnc_dbus_clipboard_notify;
369 + clipboard_peer.request = vnc_dbus_clipboard_request;
370 + qemu_clipboard_peer_register(&clipboard_peer);
371 +
372 + qemu_dbus_display1_clipboard_call_register(
373 + clipboard_proxy,
374 + G_DBUS_CALL_FLAGS_NONE,
375 + -1, NULL, NULL, NULL);
376 +}
tools/qemu-vnc/console.c new
+170
@@ -0,0 +1,170 @@
1 +/*
2 + * Minimal QemuConsole helpers for the standalone qemu-vnc binary.
3 + *
4 + * Copyright (C) 2026 Red Hat, Inc.
5 + *
6 + * SPDX-License-Identifier: GPL-2.0-or-later
7 + */
8 +
9 +#include "qemu/osdep.h"
10 +
11 +#include "ui/console.h"
12 +#include "ui/console-priv.h"
13 +#include "ui/vt100.h"
14 +#include "qapi-types-char.h"
15 +#include "qemu-vnc.h"
16 +#include "trace.h"
17 +
18 +/*
19 + * Our own QemuTextConsole definition — the one in console-vc.c uses
20 + * a Chardev* backend which is not available in the standalone binary.
21 + * Here we drive the VT100 emulator directly over a raw file descriptor.
22 + */
23 +typedef struct QemuTextConsole {
24 + QemuConsole parent;
25 + QemuVT100 vt;
26 + int chardev_fd;
27 + guint io_watch_id;
28 + char *name;
29 +} QemuTextConsole;
30 +
31 +typedef QemuConsoleClass QemuTextConsoleClass;
32 +
33 +OBJECT_DEFINE_TYPE(QemuTextConsole, qemu_text_console,
34 + QEMU_TEXT_CONSOLE, QEMU_CONSOLE)
35 +
36 +static void qemu_text_console_class_init(ObjectClass *oc, const void *data)
37 +{
38 +}
39 +
40 +static void text_console_invalidate(void *opaque)
41 +{
42 + QemuTextConsole *s = QEMU_TEXT_CONSOLE(opaque);
43 +
44 + vt100_set_image(&s->vt, QEMU_CONSOLE(s)->surface->image);
45 + vt100_refresh(&s->vt);
46 +}
47 +
48 +static const GraphicHwOps text_console_ops = {
49 + .invalidate = text_console_invalidate,
50 +};
51 +
52 +static void qemu_text_console_init(Object *obj)
53 +{
54 + QemuTextConsole *c = QEMU_TEXT_CONSOLE(obj);
55 +
56 + QEMU_CONSOLE(c)->hw_ops = &text_console_ops;
57 + QEMU_CONSOLE(c)->hw = c;
58 +}
59 +
60 +static void qemu_text_console_finalize(Object *obj)
61 +{
62 + QemuTextConsole *tc = QEMU_TEXT_CONSOLE(obj);
63 +
64 + vt100_fini(&tc->vt);
65 + if (tc->io_watch_id) {
66 + g_source_remove(tc->io_watch_id);
67 + }
68 + if (tc->chardev_fd >= 0) {
69 + close(tc->chardev_fd);
70 + }
71 + g_free(tc->name);
72 +}
73 +
74 +
75 +static void text_console_out_flush(QemuVT100 *vt)
76 +{
77 + QemuTextConsole *tc = container_of(vt, QemuTextConsole, vt);
78 + const uint8_t *data;
79 + uint32_t len;
80 +
81 + while (!fifo8_is_empty(&vt->out_fifo)) {
82 + ssize_t ret;
83 +
84 + data = fifo8_pop_bufptr(&vt->out_fifo,
85 + fifo8_num_used(&vt->out_fifo), &len);
86 + ret = write(tc->chardev_fd, data, len);
87 + if (ret < 0) {
88 + trace_qemu_vnc_console_io_error(tc->name);
89 + break;
90 + }
91 + }
92 +}
93 +
94 +static void text_console_image_update(QemuVT100 *vt, int x, int y, int w, int h)
95 +{
96 + QemuTextConsole *tc = container_of(vt, QemuTextConsole, vt);
97 + QemuConsole *con = QEMU_CONSOLE(tc);
98 +
99 + qemu_console_update(con, x, y, w, h);
100 +}
101 +
102 +static gboolean text_console_io_cb(GIOChannel *source,
103 + GIOCondition cond, gpointer data)
104 +{
105 + QemuTextConsole *tc = data;
106 + uint8_t buf[4096];
107 + ssize_t n;
108 +
109 + if (cond & (G_IO_HUP | G_IO_ERR)) {
110 + tc->io_watch_id = 0;
111 + return G_SOURCE_REMOVE;
112 + }
113 +
114 + n = read(tc->chardev_fd, buf, sizeof(buf));
115 + if (n <= 0) {
116 + trace_qemu_vnc_console_io_error(tc->name);
117 + tc->io_watch_id = 0;
118 + return G_SOURCE_REMOVE;
119 + }
120 +
121 + vt100_input(&tc->vt, buf, n);
122 + return G_SOURCE_CONTINUE;
123 +}
124 +
125 +QemuTextConsole *qemu_vnc_text_console_new(const char *name,
126 + int fd, bool echo,
127 + ChardevVCEncoding encoding)
128 +{
129 + int w = TEXT_COLS * TEXT_FONT_WIDTH;
130 + int h = TEXT_ROWS * TEXT_FONT_HEIGHT;
131 + QemuTextConsole *tc;
132 + QemuConsole *con;
133 + pixman_image_t *image;
134 + GIOChannel *chan;
135 +
136 + tc = QEMU_TEXT_CONSOLE(object_new(TYPE_QEMU_TEXT_CONSOLE));
137 + con = QEMU_CONSOLE(tc);
138 +
139 + tc->name = g_strdup(name);
140 + tc->chardev_fd = fd;
141 +
142 + image = pixman_image_create_bits(PIXMAN_x8r8g8b8, w, h, NULL, 0);
143 + con->surface = qemu_create_displaysurface_pixman(image);
144 + con->scanout.kind = SCANOUT_SURFACE;
145 + qemu_pixman_image_unref(image);
146 +
147 + vt100_init(&tc->vt, con->surface->image, encoding,
148 + text_console_image_update, text_console_out_flush);
149 + tc->vt.echo = echo;
150 + vt100_refresh(&tc->vt);
151 +
152 + chan = g_io_channel_unix_new(fd);
153 + g_io_channel_set_encoding(chan, NULL, NULL);
154 + tc->io_watch_id = g_io_add_watch(chan,
155 + G_IO_IN | G_IO_HUP | G_IO_ERR,
156 + text_console_io_cb, tc);
157 + g_io_channel_unref(chan);
158 +
159 + return tc;
160 +}
161 +
162 +void qemu_text_console_handle_keysym(QemuTextConsole *s, int keysym)
163 +{
164 + vt100_keysym(&s->vt, keysym);
165 +}
166 +
167 +void qemu_text_console_update_size(QemuTextConsole *c)
168 +{
169 + qemu_console_text_resize(QEMU_CONSOLE(c), c->vt.width, c->vt.height);
170 +}
tools/qemu-vnc/dbus.c new
+474
@@ -0,0 +1,474 @@
1 +/*
2 + * D-Bus interface for qemu-vnc standalone VNC server.
3 + *
4 + * Copyright (C) 2026 Red Hat, Inc.
5 + *
6 + * SPDX-License-Identifier: GPL-2.0-or-later
7 + */
8 +
9 +#include "qemu/osdep.h"
10 +
11 +#include "qemu/cutils.h"
12 +#include "qapi-types-trace.h"
13 +#include "system/system.h"
14 +#include "qapi/qapi-types-ui.h"
15 +#include "qapi/qapi-commands-ui.h"
16 +#include "qemu-vnc.h"
17 +#include "qemu-vnc1.h"
18 +#include "qapi/qapi-emit-events.h"
19 +#include "qobject/qdict.h"
20 +#include "ui/vnc.h"
21 +#include "trace.h"
22 +
23 +typedef struct VncDbusClient {
24 + QemuVnc1ClientSkeleton *skeleton;
25 + char *path;
26 + char *host;
27 + char *service;
28 + unsigned int id;
29 + QTAILQ_ENTRY(VncDbusClient) next;
30 +} VncDbusClient;
31 +
32 +static QemuVnc1ServerSkeleton *server_skeleton;
33 +static GDBusObjectManagerServer *obj_manager;
34 +static unsigned int next_client_id;
35 +
36 +static QTAILQ_HEAD(, VncDbusClient)
37 + dbus_clients = QTAILQ_HEAD_INITIALIZER(dbus_clients);
38 +
39 +static VncDbusClient *vnc_dbus_find_client(const char *host,
40 + const char *service)
41 +{
42 + VncDbusClient *c;
43 +
44 + QTAILQ_FOREACH(c, &dbus_clients, next) {
45 + if (g_str_equal(c->host, host) &&
46 + g_str_equal(c->service, service)) {
47 + return c;
48 + }
49 + }
50 + return NULL;
51 +}
52 +
53 +static void vnc_dbus_update_clients_property(void)
54 +{
55 + VncDbusClient *c;
56 + GPtrArray *paths;
57 + const char **strv;
58 +
59 + paths = g_ptr_array_new();
60 + QTAILQ_FOREACH(c, &dbus_clients, next) {
61 + g_ptr_array_add(paths, c->path);
62 + }
63 + g_ptr_array_add(paths, NULL);
64 +
65 + strv = (const char **)paths->pdata;
66 + qemu_vnc1_server_set_clients(QEMU_VNC1_SERVER(server_skeleton), strv);
67 + g_ptr_array_free(paths, TRUE);
68 +}
69 +
70 +void vnc_dbus_client_connected(const char *host, const char *service,
71 + const char *family, bool websocket)
72 +{
73 + VncDbusClient *c;
74 + g_autoptr(GDBusObjectSkeleton) obj = NULL;
75 +
76 + if (!server_skeleton) {
77 + return;
78 + }
79 +
80 + c = g_new0(VncDbusClient, 1);
81 + c->id = next_client_id++;
82 + c->host = g_strdup(host);
83 + c->service = g_strdup(service);
84 + c->path = g_strdup_printf("/org/qemu/Vnc1/Client_%u", c->id);
85 +
86 + c->skeleton = QEMU_VNC1_CLIENT_SKELETON(qemu_vnc1_client_skeleton_new());
87 + qemu_vnc1_client_set_host(QEMU_VNC1_CLIENT(c->skeleton), host);
88 + qemu_vnc1_client_set_service(QEMU_VNC1_CLIENT(c->skeleton), service);
89 + qemu_vnc1_client_set_family(QEMU_VNC1_CLIENT(c->skeleton), family);
90 + qemu_vnc1_client_set_web_socket(QEMU_VNC1_CLIENT(c->skeleton), websocket);
91 + qemu_vnc1_client_set_x509_dname(QEMU_VNC1_CLIENT(c->skeleton), "");
92 + qemu_vnc1_client_set_sasl_username(QEMU_VNC1_CLIENT(c->skeleton), "");
93 +
94 + obj = g_dbus_object_skeleton_new(c->path);
95 + g_dbus_object_skeleton_add_interface(
96 + obj, G_DBUS_INTERFACE_SKELETON(c->skeleton));
97 + g_dbus_object_manager_server_export(obj_manager, obj);
98 +
99 + QTAILQ_INSERT_TAIL(&dbus_clients, c, next);
100 + vnc_dbus_update_clients_property();
101 +
102 + qemu_vnc1_server_emit_client_connected(
103 + QEMU_VNC1_SERVER(server_skeleton), c->path);
104 +}
105 +
106 +void vnc_dbus_client_initialized(const char *host, const char *service,
107 + const char *x509_dname,
108 + const char *sasl_username)
109 +{
110 + VncDbusClient *c;
111 +
112 + if (!server_skeleton) {
113 + return;
114 + }
115 +
116 + c = vnc_dbus_find_client(host, service);
117 + if (!c) {
118 + trace_qemu_vnc_client_not_found(host, service);
119 + return;
120 + }
121 +
122 + if (x509_dname) {
123 + qemu_vnc1_client_set_x509_dname(
124 + QEMU_VNC1_CLIENT(c->skeleton), x509_dname);
125 + }
126 + if (sasl_username) {
127 + qemu_vnc1_client_set_sasl_username(
128 + QEMU_VNC1_CLIENT(c->skeleton), sasl_username);
129 + }
130 +
131 + qemu_vnc1_server_emit_client_initialized(
132 + QEMU_VNC1_SERVER(server_skeleton), c->path);
133 +}
134 +
135 +void vnc_dbus_client_disconnected(const char *host, const char *service)
136 +{
137 + VncDbusClient *c;
138 +
139 + if (!server_skeleton) {
140 + return;
141 + }
142 +
143 + c = vnc_dbus_find_client(host, service);
144 + if (!c) {
145 + trace_qemu_vnc_client_not_found(host, service);
146 + return;
147 + }
148 +
149 + qemu_vnc1_server_emit_client_disconnected(
150 + QEMU_VNC1_SERVER(server_skeleton), c->path);
151 +
152 + g_dbus_object_manager_server_unexport(obj_manager, c->path);
153 + QTAILQ_REMOVE(&dbus_clients, c, next);
154 + vnc_dbus_update_clients_property();
155 +
156 + g_object_unref(c->skeleton);
157 + g_free(c->path);
158 + g_free(c->host);
159 + g_free(c->service);
160 + g_free(c);
161 +}
162 +
163 +static gboolean
164 +on_set_password(QemuVnc1Server *iface,
165 + GDBusMethodInvocation *invocation,
166 + const gchar *password,
167 + gpointer user_data)
168 +{
169 + Error *err = NULL;
170 +
171 + if (vnc_display_password("default", password, &err) < 0) {
172 + g_dbus_method_invocation_return_error(
173 + invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
174 + "%s", error_get_pretty(err));
175 + error_free(err);
176 + return TRUE;
177 + }
178 +
179 + qemu_vnc1_server_complete_set_password(iface, invocation);
180 + return TRUE;
181 +}
182 +
183 +static gboolean
184 +on_expire_password(QemuVnc1Server *iface,
185 + GDBusMethodInvocation *invocation,
186 + const gchar *time_str,
187 + gpointer user_data)
188 +{
189 + time_t when;
190 +
191 + if (g_str_equal(time_str, "now")) {
192 + when = 0;
193 + } else if (g_str_equal(time_str, "never")) {
194 + when = TIME_MAX;
195 + } else if (time_str[0] == '+') {
196 + int seconds;
197 + if (qemu_strtoi(time_str + 1, NULL, 10, &seconds) < 0) {
198 + g_dbus_method_invocation_return_error(
199 + invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
200 + "Invalid time format: %s", time_str);
201 + return TRUE;
202 + }
203 + when = time(NULL) + seconds;
204 + } else {
205 + int64_t epoch;
206 + if (qemu_strtoi64(time_str, NULL, 10, &epoch) < 0) {
207 + g_dbus_method_invocation_return_error(
208 + invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
209 + "Invalid time format: %s", time_str);
210 + return TRUE;
211 + }
212 + when = epoch;
213 + }
214 +
215 + if (vnc_display_pw_expire("default", when) < 0) {
216 + g_dbus_method_invocation_return_error(
217 + invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
218 + "Failed to set password expiry");
219 + return TRUE;
220 + }
221 +
222 + qemu_vnc1_server_complete_expire_password(iface, invocation);
223 + return TRUE;
224 +}
225 +
226 +static gboolean
227 +on_reload_certificates(QemuVnc1Server *iface,
228 + GDBusMethodInvocation *invocation,
229 + gpointer user_data)
230 +{
231 + Error *err = NULL;
232 +
233 + if (!vnc_display_reload_certs("default", &err)) {
234 + g_dbus_method_invocation_return_error(
235 + invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
236 + "%s", error_get_pretty(err));
237 + error_free(err);
238 + return TRUE;
239 + }
240 +
241 + qemu_vnc1_server_complete_reload_certificates(iface, invocation);
242 + return TRUE;
243 +}
244 +
245 +static gboolean
246 +on_add_client(QemuVnc1Server *iface,
247 + GDBusMethodInvocation *invocation,
248 + GUnixFDList *fd_list,
249 + GVariant *arg_socket,
250 + gboolean skipauth,
251 + gpointer user_data)
252 +{
253 + gint32 handle = g_variant_get_handle(arg_socket);
254 + g_autoptr(GError) err = NULL;
255 + int fd;
256 +
257 + fd = g_unix_fd_list_get(fd_list, handle, &err);
258 + if (fd < 0) {
259 + g_dbus_method_invocation_return_error(
260 + invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
261 + "Failed to get fd: %s", err->message);
262 + return TRUE;
263 + }
264 +
265 + vnc_display_add_client("default", fd, skipauth);
266 +
267 + qemu_vnc1_server_complete_add_client(iface, invocation, NULL);
268 + return TRUE;
269 +}
270 +
271 +static void vnc_dbus_add_listeners(VncInfo2 *info)
272 +{
273 + GVariantBuilder builder;
274 + VncServerInfo2List *entry;
275 +
276 + g_variant_builder_init(&builder, G_VARIANT_TYPE("aa{sv}"));
277 +
278 + for (entry = info->server; entry; entry = entry->next) {
279 + VncServerInfo2 *s = entry->value;
280 + const char *vencrypt_str = "";
281 +
282 + if (s->has_vencrypt) {
283 + vencrypt_str = VncVencryptSubAuth_str(s->vencrypt);
284 + }
285 +
286 + g_variant_builder_open(&builder, G_VARIANT_TYPE("a{sv}"));
287 + g_variant_builder_add(&builder, "{sv}", "Host",
288 + g_variant_new_string(s->host));
289 + g_variant_builder_add(&builder, "{sv}", "Service",
290 + g_variant_new_string(s->service));
291 + g_variant_builder_add(&builder, "{sv}", "Family",
292 + g_variant_new_string(
293 + NetworkAddressFamily_str(s->family)));
294 + g_variant_builder_add(&builder, "{sv}", "WebSocket",
295 + g_variant_new_boolean(s->websocket));
296 + g_variant_builder_add(&builder, "{sv}", "Auth",
297 + g_variant_new_string(
298 + VncPrimaryAuth_str(s->auth)));
299 + g_variant_builder_add(&builder, "{sv}", "VencryptSubAuth",
300 + g_variant_new_string(vencrypt_str));
301 + g_variant_builder_close(&builder);
302 + }
303 +
304 + qemu_vnc1_server_set_listeners(
305 + QEMU_VNC1_SERVER(server_skeleton),
306 + g_variant_builder_end(&builder));
307 +}
308 +
309 +void vnc_dbus_setup(GDBusConnection *bus)
310 +{
311 + g_autoptr(GDBusObjectSkeleton) server_obj = NULL;
312 + g_autoptr(VncInfo2List) info_list = NULL;
313 + Error *err = NULL;
314 + const char *auth_str = "none";
315 + const char *vencrypt_str = "";
316 +
317 + obj_manager = g_dbus_object_manager_server_new("/org/qemu/Vnc1");
318 +
319 + server_skeleton = QEMU_VNC1_SERVER_SKELETON(
320 + qemu_vnc1_server_skeleton_new());
321 +
322 + qemu_vnc1_server_set_name(QEMU_VNC1_SERVER(server_skeleton),
323 + qemu_name ? qemu_name : "");
324 + qemu_vnc1_server_set_clients(QEMU_VNC1_SERVER(server_skeleton), NULL);
325 +
326 + /* Query auth info from the VNC display */
327 + info_list = qmp_query_vnc_servers(&err);
328 + if (info_list) {
329 + VncInfo2 *info = info_list->value;
330 + auth_str = VncPrimaryAuth_str(info->auth);
331 + if (info->has_vencrypt) {
332 + vencrypt_str = VncVencryptSubAuth_str(info->vencrypt);
333 + }
334 + vnc_dbus_add_listeners(info);
335 + }
336 +
337 + qemu_vnc1_server_set_auth(QEMU_VNC1_SERVER(server_skeleton), auth_str);
338 + qemu_vnc1_server_set_vencrypt_sub_auth(
339 + QEMU_VNC1_SERVER(server_skeleton), vencrypt_str);
340 +
341 + g_signal_connect(server_skeleton, "handle-set-password",
342 + G_CALLBACK(on_set_password), NULL);
343 + g_signal_connect(server_skeleton, "handle-expire-password",
344 + G_CALLBACK(on_expire_password), NULL);
345 + g_signal_connect(server_skeleton, "handle-reload-certificates",
346 + G_CALLBACK(on_reload_certificates), NULL);
347 + g_signal_connect(server_skeleton, "handle-add-client",
348 + G_CALLBACK(on_add_client), NULL);
349 +
350 + server_obj = g_dbus_object_skeleton_new("/org/qemu/Vnc1/Server");
351 + g_dbus_object_skeleton_add_interface(
352 + server_obj, G_DBUS_INTERFACE_SKELETON(server_skeleton));
353 + g_dbus_object_manager_server_export(obj_manager, server_obj);
354 +
355 + g_dbus_object_manager_server_set_connection(obj_manager, bus);
356 +
357 + if (g_dbus_connection_get_flags(bus)
358 + & G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION) {
359 + g_bus_own_name_on_connection(
360 + bus, "org.qemu.vnc",
361 + G_BUS_NAME_OWNER_FLAGS_NONE,
362 + NULL, NULL, NULL, NULL);
363 + }
364 +}
365 +
366 +void vnc_action_shutdown(VncState *vs)
367 +{
368 + VncDbusClient *c;
369 +
370 + c = vnc_dbus_find_client(vs->info->host, vs->info->service);
371 + if (!c) {
372 + trace_qemu_vnc_client_not_found(vs->info->host, vs->info->service);
373 + return;
374 + }
375 +
376 + qemu_vnc1_client_emit_shutdown_request(QEMU_VNC1_CLIENT(c->skeleton));
377 +}
378 +
379 +void vnc_action_reset(VncState *vs)
380 +{
381 + VncDbusClient *c;
382 +
383 + c = vnc_dbus_find_client(vs->info->host, vs->info->service);
384 + if (!c) {
385 + trace_qemu_vnc_client_not_found(vs->info->host, vs->info->service);
386 + return;
387 + }
388 +
389 + qemu_vnc1_client_emit_reset_request(QEMU_VNC1_CLIENT(c->skeleton));
390 +}
391 +
392 +/*
393 + * Override the stub qapi_event_emit() to capture VNC events
394 + * and forward them to the D-Bus interface.
395 + */
396 +void qapi_event_emit(QAPIEvent event, QDict *qdict)
397 +{
398 + QDict *data, *client;
399 + const char *host, *service, *family;
400 + bool websocket;
401 +
402 + if (event != QAPI_EVENT_VNC_CONNECTED &&
403 + event != QAPI_EVENT_VNC_INITIALIZED &&
404 + event != QAPI_EVENT_VNC_DISCONNECTED) {
405 + return;
406 + }
407 +
408 + data = qdict_get_qdict(qdict, "data");
409 + if (!data) {
410 + return;
411 + }
412 +
413 + client = qdict_get_qdict(data, "client");
414 + if (!client) {
415 + return;
416 + }
417 +
418 + host = qdict_get_str(client, "host");
419 + service = qdict_get_str(client, "service");
420 + family = qdict_get_str(client, "family");
421 + websocket = qdict_get_bool(client, "websocket");
422 +
423 + switch (event) {
424 + case QAPI_EVENT_VNC_CONNECTED:
425 + vnc_dbus_client_connected(host, service, family, websocket);
426 + break;
427 + case QAPI_EVENT_VNC_INITIALIZED: {
428 + const char *x509_dname = NULL;
429 + const char *sasl_username = NULL;
430 +
431 + if (qdict_haskey(client, "x509_dname")) {
432 + x509_dname = qdict_get_str(client, "x509_dname");
433 + }
434 + if (qdict_haskey(client, "sasl_username")) {
435 + sasl_username = qdict_get_str(client, "sasl_username");
436 + }
437 + vnc_dbus_client_initialized(host, service,
438 + x509_dname, sasl_username);
439 + break;
440 + }
441 + case QAPI_EVENT_VNC_DISCONNECTED:
442 + vnc_dbus_client_disconnected(host, service);
443 + break;
444 + default:
445 + break;
446 + }
447 +}
448 +
449 +void vnc_dbus_emit_leaving(const char *reason)
450 +{
451 + if (!server_skeleton) {
452 + return;
453 + }
454 +
455 + qemu_vnc1_server_emit_leaving(QEMU_VNC1_SERVER(server_skeleton), reason);
456 +}
457 +
458 +void vnc_dbus_cleanup(void)
459 +{
460 + VncDbusClient *c, *next;
461 +
462 + QTAILQ_FOREACH_SAFE(c, &dbus_clients, next, next) {
463 + g_dbus_object_manager_server_unexport(obj_manager, c->path);
464 + QTAILQ_REMOVE(&dbus_clients, c, next);
465 + g_object_unref(c->skeleton);
466 + g_free(c->path);
467 + g_free(c->host);
468 + g_free(c->service);
469 + g_free(c);
470 + }
471 +
472 + g_clear_object(&server_skeleton);
473 + g_clear_object(&obj_manager);
474 +}
tools/qemu-vnc/display.c new
+456
@@ -0,0 +1,456 @@
1 +/*
2 + * D-Bus display listener — scanout, update and cursor handling.
3 + *
4 + * Copyright (C) 2026 Red Hat, Inc.
5 + *
6 + * SPDX-License-Identifier: GPL-2.0-or-later
7 + */
8 +
9 +#include "qemu/osdep.h"
10 +
11 +#include "qemu/sockets.h"
12 +#include "qemu/error-report.h"
13 +#include "ui/console-priv.h"
14 +#include "ui/dbus-display1.h"
15 +#include "ui/surface.h"
16 +#include "trace.h"
17 +#include "qemu-vnc.h"
18 +
19 +typedef struct ConsoleData {
20 + QemuDBusDisplay1Console *console_proxy;
21 + QemuDBusDisplay1Keyboard *keyboard_proxy;
22 + QemuDBusDisplay1Mouse *mouse_proxy;
23 + QemuGraphicConsole *gfx_con;
24 + GDBusConnection *listener_conn;
25 + /*
26 + * When true the surface is backed by a read-only mmap (ScanoutMap path)
27 + * and Update messages must be rejected because compositing into the
28 + * surface is not possible. The plain Scanout path provides a writable
29 + * copy and clears this flag.
30 + */
31 + bool read_only;
32 +} ConsoleData;
33 +
34 +static void display_ui_info(void *opaque, uint32_t head, QemuUIInfo *info)
35 +{
36 + ConsoleData *cd = opaque;
37 + g_autoptr(GError) err = NULL;
38 +
39 + if (!cd || !cd->console_proxy) {
40 + return;
41 + }
42 +
43 + qemu_dbus_display1_console_call_set_uiinfo_sync(
44 + cd->console_proxy,
45 + info->width_mm, info->height_mm,
46 + info->xoff, info->yoff,
47 + info->width, info->height,
48 + G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err);
49 + if (err) {
50 + error_report("SetUIInfo failed: %s", err->message);
51 + }
52 +}
53 +
54 +static void
55 +scanout_image_destroy(pixman_image_t *image, void *data)
56 +{
57 + g_variant_unref(data);
58 +}
59 +
60 +typedef struct {
61 + void *addr;
62 + size_t len;
63 +} ScanoutMapData;
64 +
65 +static void
66 +scanout_map_destroy(pixman_image_t *image, void *data)
67 +{
68 + ScanoutMapData *map = data;
69 + munmap(map->addr, map->len);
70 + g_free(map);
71 +}
72 +
73 +static gboolean
74 +on_scanout(QemuDBusDisplay1Listener *listener,
75 + GDBusMethodInvocation *invocation,
76 + guint width, guint height, guint stride,
77 + guint pixman_format, GVariant *data,
78 + gpointer user_data)
79 +{
80 + ConsoleData *cd = user_data;
81 + QemuConsole *con = QEMU_CONSOLE(cd->gfx_con);
82 + gsize size;
83 + const uint8_t *pixels;
84 + pixman_image_t *image;
85 + DisplaySurface *surface;
86 +
87 + trace_qemu_vnc_scanout(width, height, stride, pixman_format);
88 +
89 + pixels = g_variant_get_fixed_array(data, &size, 1);
90 +
91 + image = pixman_image_create_bits((pixman_format_code_t)pixman_format,
92 + width, height, (uint32_t *)pixels, stride);
93 + assert(image);
94 +
95 + g_variant_ref(data);
96 + pixman_image_set_destroy_function(image, scanout_image_destroy, data);
97 +
98 + cd->read_only = false;
99 + surface = qemu_create_displaysurface_pixman(image);
100 + qemu_console_set_surface(con, surface);
101 +
102 + qemu_dbus_display1_listener_complete_scanout(listener, invocation);
103 + return DBUS_METHOD_INVOCATION_HANDLED;
104 +}
105 +
106 +static gboolean
107 +on_update(QemuDBusDisplay1Listener *listener,
108 + GDBusMethodInvocation *invocation,
109 + gint x, gint y, gint w, gint h,
110 + guint stride, guint pixman_format, GVariant *data,
111 + gpointer user_data)
112 +{
113 + ConsoleData *cd = user_data;
114 + QemuConsole *con = QEMU_CONSOLE(cd->gfx_con);
115 + DisplaySurface *surface = qemu_console_surface(con);
116 + gsize size;
117 + const uint8_t *pixels;
118 + pixman_image_t *src;
119 +
120 + trace_qemu_vnc_update(x, y, w, h, stride, pixman_format);
121 + if (!surface || cd->read_only) {
122 + g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
123 + G_DBUS_ERROR_FAILED, "No active or writable console");
124 + return DBUS_METHOD_INVOCATION_HANDLED;
125 + }
126 +
127 + pixels = g_variant_get_fixed_array(data, &size, 1);
128 + src = pixman_image_create_bits((pixman_format_code_t)pixman_format,
129 + w, h, (uint32_t *)pixels, stride);
130 + assert(src);
131 + pixman_image_composite(PIXMAN_OP_SRC, src, NULL,
132 + surface->image,
133 + 0, 0, 0, 0, x, y, w, h);
134 + pixman_image_unref(src);
135 +
136 + qemu_console_update(con, x, y, w, h);
137 +
138 + qemu_dbus_display1_listener_complete_update(listener, invocation);
139 + return DBUS_METHOD_INVOCATION_HANDLED;
140 +}
141 +
142 +static gboolean
143 +on_scanout_map(QemuDBusDisplay1ListenerUnixMap *listener,
144 + GDBusMethodInvocation *invocation,
145 + GUnixFDList *fd_list,
146 + GVariant *arg_handle,
147 + guint offset, guint width, guint height,
148 + guint stride, guint pixman_format,
149 + gpointer user_data)
150 +{
151 + ConsoleData *cd = user_data;
152 + gint32 handle = g_variant_get_handle(arg_handle);
153 + g_autoptr(GError) err = NULL;
154 + DisplaySurface *surface;
155 + int fd;
156 + void *addr;
157 + size_t len = (size_t)height * stride;
158 + pixman_image_t *image;
159 +
160 + trace_qemu_vnc_scanout_map(width, height, stride, pixman_format, offset);
161 +
162 + fd = g_unix_fd_list_get(fd_list, handle, &err);
163 + if (fd < 0) {
164 + g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
165 + G_DBUS_ERROR_FAILED, "Failed to get fd: %s", err->message);
166 + return DBUS_METHOD_INVOCATION_HANDLED;
167 + }
168 +
169 + /* MAP_PRIVATE: we only read; avoid propagating writes back to QEMU */
170 + addr = mmap(NULL, len, PROT_READ, MAP_PRIVATE, fd, offset);
171 + close(fd);
172 + if (addr == MAP_FAILED) {
173 + g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
174 + G_DBUS_ERROR_FAILED, "mmap failed: %s", g_strerror(errno));
175 + return DBUS_METHOD_INVOCATION_HANDLED;
176 + }
177 +
178 + image = pixman_image_create_bits((pixman_format_code_t)pixman_format,
179 + width, height, addr, stride);
180 + assert(image);
181 + {
182 + ScanoutMapData *map = g_new0(ScanoutMapData, 1);
183 + map->addr = addr;
184 + map->len = len;
185 + pixman_image_set_destroy_function(image, scanout_map_destroy, map);
186 + }
187 +
188 + cd->read_only = true;
189 + surface = qemu_create_displaysurface_pixman(image);
190 + qemu_console_set_surface(QEMU_CONSOLE(cd->gfx_con), surface);
191 +
192 + qemu_dbus_display1_listener_unix_map_complete_scanout_map(
193 + listener, invocation, NULL);
194 + return DBUS_METHOD_INVOCATION_HANDLED;
195 +}
196 +
197 +static gboolean
198 +on_update_map(QemuDBusDisplay1ListenerUnixMap *listener,
199 + GDBusMethodInvocation *invocation,
200 + guint x, guint y, guint w, guint h,
201 + gpointer user_data)
202 +{
203 + ConsoleData *cd = user_data;
204 +
205 + trace_qemu_vnc_update_map(x, y, w, h);
206 +
207 + qemu_console_update(QEMU_CONSOLE(cd->gfx_con), x, y, w, h);
208 +
209 + qemu_dbus_display1_listener_unix_map_complete_update_map(
210 + listener, invocation);
211 + return DBUS_METHOD_INVOCATION_HANDLED;
212 +}
213 +
214 +static gboolean
215 +on_cursor_define(QemuDBusDisplay1Listener *listener,
216 + GDBusMethodInvocation *invocation,
217 + gint width, gint height,
218 + gint hot_x, gint hot_y,
219 + GVariant *data,
220 + gpointer user_data)
221 +{
222 + ConsoleData *cd = user_data;
223 + gsize size;
224 + const uint8_t *pixels;
225 + QEMUCursor *c;
226 +
227 + trace_qemu_vnc_cursor_define(width, height, hot_x, hot_y);
228 +
229 + c = cursor_alloc(width, height);
230 + if (!c) {
231 + qemu_dbus_display1_listener_complete_cursor_define(
232 + listener, invocation);
233 + return DBUS_METHOD_INVOCATION_HANDLED;
234 + }
235 +
236 + c->hot_x = hot_x;
237 + c->hot_y = hot_y;
238 +
239 + pixels = g_variant_get_fixed_array(data, &size, 1);
240 + memcpy(c->data, pixels, MIN(size, (gsize)width * height * 4));
241 +
242 + qemu_console_set_cursor(QEMU_CONSOLE(cd->gfx_con), c);
243 + cursor_unref(c);
244 +
245 + qemu_dbus_display1_listener_complete_cursor_define(
246 + listener, invocation);
247 + return DBUS_METHOD_INVOCATION_HANDLED;
248 +}
249 +
250 +typedef struct {
251 + GMainLoop *loop;
252 + GThread *thread;
253 + GDBusConnection *listener_conn;
254 +} ListenerSetupData;
255 +
256 +static void
257 +on_register_listener_finished(GObject *source_object,
258 + GAsyncResult *res,
259 + gpointer user_data)
260 +{
261 + ListenerSetupData *data = user_data;
262 + g_autoptr(GError) err = NULL;
263 +
264 + qemu_dbus_display1_console_call_register_listener_finish(
265 + QEMU_DBUS_DISPLAY1_CONSOLE(source_object),
266 + NULL,
267 + res, &err);
268 +
269 + if (err) {
270 + error_report("RegisterListener failed: %s", err->message);
271 + g_main_loop_quit(data->loop);
272 + return;
273 + }
274 +
275 + data->listener_conn = g_thread_join(data->thread);
276 + g_main_loop_quit(data->loop);
277 +}
278 +
279 +static GDBusConnection *
280 +console_register_display_listener(QemuDBusDisplay1Console *console)
281 +{
282 + g_autoptr(GError) err = NULL;
283 + g_autoptr(GMainLoop) loop = NULL;
284 + g_autoptr(GUnixFDList) fd_list = NULL;
285 + ListenerSetupData data = { 0 };
286 + int pair[2];
287 + int idx;
288 +
289 + if (qemu_socketpair(AF_UNIX, SOCK_STREAM, 0, pair) < 0) {
290 + error_report("socketpair failed: %s", strerror(errno));
291 + return NULL;
292 + }
293 +
294 + fd_list = g_unix_fd_list_new();
295 + idx = g_unix_fd_list_append(fd_list, pair[1], &err);
296 + close(pair[1]);
297 + if (idx < 0) {
298 + close(pair[0]);
299 + error_report("Failed to append fd: %s", err->message);
300 + return NULL;
301 + }
302 +
303 + loop = g_main_loop_new(NULL, FALSE);
304 + data.loop = loop;
305 + data.thread = p2p_dbus_thread_new(pair[0]);
306 +
307 + qemu_dbus_display1_console_call_register_listener(
308 + console,
309 + g_variant_new_handle(idx),
310 + G_DBUS_CALL_FLAGS_NONE,
311 + -1,
312 + fd_list,
313 + NULL,
314 + on_register_listener_finished,
315 + &data);
316 +
317 + g_main_loop_run(loop);
318 +
319 + return data.listener_conn;
320 +}
321 +
322 +static void
323 +setup_display_listener(ConsoleData *cd)
324 +{
325 + g_autoptr(GDBusObjectSkeleton) obj = NULL;
326 + GDBusObjectManagerServer *server;
327 + QemuDBusDisplay1Listener *iface;
328 + QemuDBusDisplay1ListenerUnixMap *iface_map;
329 +
330 + server = g_dbus_object_manager_server_new(DBUS_DISPLAY1_ROOT);
331 + obj = g_dbus_object_skeleton_new(DBUS_DISPLAY1_ROOT "/Listener");
332 +
333 + /* Main listener interface */
334 + iface = qemu_dbus_display1_listener_skeleton_new();
335 + g_object_connect(iface,
336 + "signal::handle-scanout", on_scanout, cd,
337 + "signal::handle-update", on_update, cd,
338 + "signal::handle-cursor-define", on_cursor_define, cd,
339 + NULL);
340 + g_dbus_object_skeleton_add_interface(obj,
341 + G_DBUS_INTERFACE_SKELETON(iface));
342 +
343 + /* Unix shared memory map interface */
344 + iface_map = qemu_dbus_display1_listener_unix_map_skeleton_new();
345 + g_object_connect(iface_map,
346 + "signal::handle-scanout-map", on_scanout_map, cd,
347 + "signal::handle-update-map", on_update_map, cd,
348 + NULL);
349 + g_dbus_object_skeleton_add_interface(obj,
350 + G_DBUS_INTERFACE_SKELETON(iface_map));
351 +
352 + {
353 + const gchar *ifaces[] = {
354 + "org.qemu.Display1.Listener.Unix.Map", NULL
355 + };
356 + g_object_set(iface, "interfaces", ifaces, NULL);
357 + }
358 +
359 + g_dbus_object_manager_server_export(server, obj);
360 + g_dbus_object_manager_server_set_connection(server,
361 + cd->listener_conn);
362 +
363 + g_dbus_connection_start_message_processing(cd->listener_conn);
364 +}
365 +
366 +static const GraphicHwOps vnc_hw_ops = {
367 + .ui_info = display_ui_info,
368 +};
369 +
370 +bool console_setup(GDBusConnection *bus, const char *bus_name,
371 + const char *console_path)
372 +{
373 + g_autoptr(GError) err = NULL;
374 + ConsoleData *cd;
375 + QemuConsole *con;
376 +
377 + cd = g_new0(ConsoleData, 1);
378 +
379 + cd->console_proxy = qemu_dbus_display1_console_proxy_new_sync(
380 + bus, G_DBUS_PROXY_FLAGS_NONE, bus_name,
381 + console_path, NULL, &err);
382 + if (!cd->console_proxy) {
383 + error_report("Failed to create console proxy for %s: %s",
384 + console_path, err->message);
385 + g_free(cd);
386 + return false;
387 + }
388 +
389 + cd->keyboard_proxy = QEMU_DBUS_DISPLAY1_KEYBOARD(
390 + qemu_dbus_display1_keyboard_proxy_new_sync(
391 + bus, G_DBUS_PROXY_FLAGS_NONE, bus_name,
392 + console_path, NULL, &err));
393 + if (!cd->keyboard_proxy) {
394 + error_report("Failed to create keyboard proxy for %s: %s",
395 + console_path, err->message);
396 + g_object_unref(cd->console_proxy);
397 + g_free(cd);
398 + return false;
399 + }
400 +
401 + g_clear_error(&err);
402 + cd->mouse_proxy = QEMU_DBUS_DISPLAY1_MOUSE(
403 + qemu_dbus_display1_mouse_proxy_new_sync(
404 + bus, G_DBUS_PROXY_FLAGS_NONE, bus_name,
405 + console_path, NULL, &err));
406 + if (!cd->mouse_proxy) {
407 + error_report("Failed to create mouse proxy for %s: %s",
408 + console_path, err->message);
409 + g_object_unref(cd->keyboard_proxy);
410 + g_object_unref(cd->console_proxy);
411 + g_free(cd);
412 + return false;
413 + }
414 +
415 + con = qemu_graphic_console_create(NULL, 0, &vnc_hw_ops, cd);
416 + cd->gfx_con = QEMU_GRAPHIC_CONSOLE(con);
417 +
418 + cd->listener_conn = console_register_display_listener(
419 + cd->console_proxy);
420 + if (!cd->listener_conn) {
421 + error_report("Failed to setup D-Bus listener for %s",
422 + console_path);
423 + g_object_unref(cd->mouse_proxy);
424 + g_object_unref(cd->keyboard_proxy);
425 + g_object_unref(cd->console_proxy);
426 + g_free(cd);
427 + return false;
428 + }
429 +
430 + setup_display_listener(cd);
431 + input_setup(cd->keyboard_proxy, cd->mouse_proxy);
432 +
433 + return true;
434 +}
435 +
436 +QemuDBusDisplay1Keyboard *console_get_keyboard(QemuConsole *con)
437 +{
438 + ConsoleData *cd;
439 +
440 + if (!QEMU_IS_GRAPHIC_CONSOLE(con)) {
441 + return NULL;
442 + }
443 + cd = con->hw;
444 + return cd ? cd->keyboard_proxy : NULL;
445 +}
446 +
447 +QemuDBusDisplay1Mouse *console_get_mouse(QemuConsole *con)
448 +{
449 + ConsoleData *cd;
450 +
451 + if (!QEMU_IS_GRAPHIC_CONSOLE(con)) {
452 + return NULL;
453 + }
454 + cd = con->hw;
455 + return cd ? cd->mouse_proxy : NULL;
456 +}
tools/qemu-vnc/input.c new
+239
@@ -0,0 +1,239 @@
1 +/*
2 + * Keyboard and mouse input dispatch via D-Bus.
3 + *
4 + * Copyright (C) 2026 Red Hat, Inc.
5 + *
6 + * SPDX-License-Identifier: GPL-2.0-or-later
7 + */
8 +
9 +#include "qemu/osdep.h"
10 +
11 +#include "ui/dbus-display1.h"
12 +#include "ui/input.h"
13 +#include "trace.h"
14 +#include "qemu-vnc.h"
15 +
16 +struct QEMUPutLEDEntry {
17 + QEMUPutLEDEvent *put_led;
18 + void *opaque;
19 + QTAILQ_ENTRY(QEMUPutLEDEntry) next;
20 +};
21 +
22 +static NotifierList mouse_mode_notifiers =
23 + NOTIFIER_LIST_INITIALIZER(mouse_mode_notifiers);
24 +static QTAILQ_HEAD(, QEMUPutLEDEntry) led_handlers =
25 + QTAILQ_HEAD_INITIALIZER(led_handlers);
26 +
27 +/* Track the target console for pending mouse events (used by sync) */
28 +static QemuConsole *mouse_target;
29 +
30 +QEMUPutLEDEntry *qemu_add_led_event_handler(QEMUPutLEDEvent *func,
31 + void *opaque)
32 +{
33 + QEMUPutLEDEntry *s;
34 +
35 + s = g_new0(QEMUPutLEDEntry, 1);
36 + s->put_led = func;
37 + s->opaque = opaque;
38 + QTAILQ_INSERT_TAIL(&led_handlers, s, next);
39 + return s;
40 +}
41 +
42 +void qemu_remove_led_event_handler(QEMUPutLEDEntry *entry)
43 +{
44 + if (!entry) {
45 + return;
46 + }
47 + QTAILQ_REMOVE(&led_handlers, entry, next);
48 + g_free(entry);
49 +}
50 +
51 +static void
52 +on_keyboard_modifiers_changed(GObject *gobject, GParamSpec *pspec,
53 + gpointer user_data)
54 +{
55 + guint modifiers;
56 + QEMUPutLEDEntry *cursor;
57 +
58 + modifiers = qemu_dbus_display1_keyboard_get_modifiers(
59 + QEMU_DBUS_DISPLAY1_KEYBOARD(gobject));
60 +
61 + /*
62 + * The D-Bus Keyboard.Modifiers property uses the same
63 + * bit layout as QEMU's LED constants.
64 + */
65 + QTAILQ_FOREACH(cursor, &led_handlers, next) {
66 + cursor->put_led(cursor->opaque, modifiers);
67 + }
68 +}
69 +
70 +void qemu_add_mouse_mode_change_notifier(Notifier *notify)
71 +{
72 + notifier_list_add(&mouse_mode_notifiers, notify);
73 +}
74 +
75 +void qemu_remove_mouse_mode_change_notifier(Notifier *notify)
76 +{
77 + notifier_remove(notify);
78 +}
79 +
80 +void qemu_input_event_send_key_delay(uint32_t delay_ms)
81 +{
82 +}
83 +
84 +void qemu_input_event_send_key_qcode(QemuConsole *src, QKeyCode q, bool down)
85 +{
86 + QemuDBusDisplay1Keyboard *kbd;
87 + guint qnum;
88 +
89 + trace_qemu_vnc_key_event(q, down);
90 +
91 + if (!src) {
92 + return;
93 + }
94 + kbd = console_get_keyboard(src);
95 + if (!kbd) {
96 + return;
97 + }
98 +
99 + if (q >= qemu_input_map_qcode_to_qnum_len) {
100 + return;
101 + }
102 + qnum = qemu_input_map_qcode_to_qnum[q];
103 +
104 + if (down) {
105 + qemu_dbus_display1_keyboard_call_press(
106 + kbd, qnum,
107 + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
108 + } else {
109 + qemu_dbus_display1_keyboard_call_release(
110 + kbd, qnum,
111 + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
112 + }
113 +}
114 +
115 +static guint abs_x, abs_y;
116 +static bool abs_pending;
117 +static gint rel_dx, rel_dy;
118 +static bool rel_pending;
119 +
120 +void qemu_input_queue_abs(QemuConsole *src, InputAxis axis,
121 + int value, int min_in, int max_in)
122 +{
123 + if (axis == INPUT_AXIS_X) {
124 + abs_x = value;
125 + } else if (axis == INPUT_AXIS_Y) {
126 + abs_y = value;
127 + }
128 + abs_pending = true;
129 + mouse_target = src;
130 +}
131 +
132 +void qemu_input_queue_rel(QemuConsole *src, InputAxis axis, int value)
133 +{
134 + if (axis == INPUT_AXIS_X) {
135 + rel_dx += value;
136 + } else if (axis == INPUT_AXIS_Y) {
137 + rel_dy += value;
138 + }
139 + rel_pending = true;
140 + mouse_target = src;
141 +}
142 +
143 +void qemu_input_event_sync(void)
144 +{
145 + QemuDBusDisplay1Mouse *mouse;
146 +
147 + if (!mouse_target) {
148 + return;
149 + }
150 +
151 + mouse = console_get_mouse(mouse_target);
152 + if (!mouse) {
153 + abs_pending = false;
154 + rel_pending = false;
155 + return;
156 + }
157 +
158 + if (abs_pending) {
159 + trace_qemu_vnc_input_abs(abs_x, abs_y);
160 + abs_pending = false;
161 + qemu_dbus_display1_mouse_call_set_abs_position(
162 + mouse, abs_x, abs_y,
163 + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
164 + }
165 +
166 + if (rel_pending) {
167 + trace_qemu_vnc_input_rel(rel_dx, rel_dy);
168 + rel_pending = false;
169 + qemu_dbus_display1_mouse_call_rel_motion(
170 + mouse, rel_dx, rel_dy,
171 + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
172 + rel_dx = 0;
173 + rel_dy = 0;
174 + }
175 +}
176 +
177 +bool qemu_input_is_absolute(QemuConsole *con)
178 +{
179 + QemuDBusDisplay1Mouse *mouse;
180 +
181 + if (!con) {
182 + return false;
183 + }
184 + mouse = console_get_mouse(con);
185 +
186 + if (!mouse) {
187 + return false;
188 + }
189 + return qemu_dbus_display1_mouse_get_is_absolute(mouse);
190 +}
191 +
192 +static void
193 +on_mouse_is_absolute_changed(GObject *gobject, GParamSpec *pspec,
194 + gpointer user_data)
195 +{
196 + notifier_list_notify(&mouse_mode_notifiers, NULL);
197 +}
198 +
199 +void qemu_input_update_buttons(QemuConsole *src, uint32_t *button_map,
200 + uint32_t button_old, uint32_t button_new)
201 +{
202 + QemuDBusDisplay1Mouse *mouse;
203 + uint32_t changed;
204 + int i;
205 +
206 + if (!src) {
207 + return;
208 + }
209 + mouse = console_get_mouse(src);
210 + if (!mouse) {
211 + return;
212 + }
213 +
214 + changed = button_old ^ button_new;
215 + for (i = 0; i < 32; i++) {
216 + if (!(changed & (1u << i))) {
217 + continue;
218 + }
219 + trace_qemu_vnc_input_btn(i, !!(button_new & (1u << i)));
220 + if (button_new & (1u << i)) {
221 + qemu_dbus_display1_mouse_call_press(
222 + mouse, i,
223 + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
224 + } else {
225 + qemu_dbus_display1_mouse_call_release(
226 + mouse, i,
227 + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
228 + }
229 + }
230 +}
231 +
232 +void input_setup(QemuDBusDisplay1Keyboard *kbd,
233 + QemuDBusDisplay1Mouse *mouse)
234 +{
235 + g_signal_connect(kbd, "notify::modifiers",
236 + G_CALLBACK(on_keyboard_modifiers_changed), NULL);
237 + g_signal_connect(mouse, "notify::is-absolute",
238 + G_CALLBACK(on_mouse_is_absolute_changed), NULL);
239 +}
tools/qemu-vnc/meson.build new
+26
@@ -0,0 +1,26 @@
1 +vnca = vnc_ss.apply({}, strict: false)
2 +
3 +qemu_vnc1 = custom_target('qemu-vnc1 gdbus-codegen',
4 + output: ['qemu-vnc1.h', 'qemu-vnc1.c'],
5 + input: files('qemu-vnc1.xml'),
6 + command: [gdbus_codegen, '@INPUT@',
7 + '--glib-min-required', '2.64',
8 + '--output-directory', meson.current_build_dir(),
9 + '--interface-prefix', 'org.qemu.',
10 + '--c-namespace', 'Qemu',
11 + '--generate-c-code', '@BASENAME@'])
12 +
13 +qemu_vnc = executable('qemu-vnc',
14 + sources: ['qemu-vnc.c', 'display.c', 'input.c',
15 + 'audio.c', 'chardev.c', 'clipboard.c', 'console.c',
16 + 'dbus.c', 'stubs.c', 'utils.c',
17 + vnca.sources(), dbus_display1, qemu_vnc1],
18 + dependencies: [vnca.dependencies(), io, crypto, qemuutil, gio, ui])
19 +
20 +# The executable lives in a subdirectory of the build tree, but
21 +# get_relocated_path() looks for qemu-bundle relative to the binary.
22 +# Create a symlink so that firmware/keymap lookup works during development.
23 +run_command('ln', '-sfn',
24 + '../../qemu-bundle',
25 + meson.current_build_dir() / 'qemu-bundle',
26 + check: false)
tools/qemu-vnc/qemu-vnc.c new
+581
@@ -0,0 +1,581 @@
1 +/*
2 + * Standalone VNC server connecting to QEMU via D-Bus display interface.
3 + *
4 + * Copyright (C) 2026 Red Hat, Inc.
5 + *
6 + * SPDX-License-Identifier: GPL-2.0-or-later
7 + */
8 +
9 +#include "qemu/osdep.h"
10 +
11 +#include "qemu/cutils.h"
12 +#include "qemu/datadir.h"
13 +#include "qemu/error-report.h"
14 +#include "qemu/config-file.h"
15 +#include "qemu/option.h"
16 +#include "qemu/log.h"
17 +#include "qemu/main-loop.h"
18 +#include "qemu-version.h"
19 +#include "ui/vnc.h"
20 +#include "crypto/secret.h"
21 +#include "crypto/tlscredsx509.h"
22 +#include "qom/object_interfaces.h"
23 +#include "trace.h"
24 +#include "qemu-vnc.h"
25 +
26 +const char *qemu_name;
27 +const char *keyboard_layout;
28 +
29 +typedef struct {
30 + GDBusConnection *bus;
31 + const char *bus_name;
32 + const char * const *chardev_names;
33 + char *terminate_reason;
34 + bool no_vt;
35 + bool terminate;
36 + bool owner_seen;
37 + bool wait_for_owner;
38 +} QemuVncState;
39 +
40 +static GType
41 +dbus_display_get_proxy_type(GDBusObjectManagerClient *manager,
42 + const gchar *object_path,
43 + const gchar *interface_name,
44 + gpointer user_data)
45 +{
46 + static const struct {
47 + const char *iface;
48 + GType (*get_type)(void);
49 + } types[] = {
50 + { "org.qemu.Display1.Clipboard",
51 + qemu_dbus_display1_clipboard_proxy_get_type },
52 + { "org.qemu.Display1.Audio",
53 + qemu_dbus_display1_audio_proxy_get_type },
54 + { "org.qemu.Display1.Chardev",
55 + qemu_dbus_display1_chardev_proxy_get_type },
56 + { "org.qemu.Display1.Chardev.VCEncoding",
57 + qemu_dbus_display1_chardev_vcencoding_proxy_get_type },
58 + };
59 +
60 + if (!interface_name) {
61 + return G_TYPE_DBUS_OBJECT_PROXY;
62 + }
63 +
64 + for (int i = 0; i < G_N_ELEMENTS(types); i++) {
65 + if (g_str_equal(interface_name, types[i].iface)) {
66 + return types[i].get_type();
67 + }
68 + }
69 +
70 + return G_TYPE_DBUS_PROXY;
71 +}
72 +
73 +static void
74 +on_bus_closed(GDBusConnection *connection,
75 + gboolean remote_peer_vanished,
76 + GError *error,
77 + gpointer user_data)
78 +{
79 + QemuVncState *state = user_data;
80 +
81 + state->terminate_reason = g_strdup("D-Bus connection closed");
82 + state->terminate = true;
83 + qemu_notify_event();
84 +}
85 +
86 +static void
87 +on_manager_ready(GObject *source_object,
88 + GAsyncResult *res,
89 + gpointer user_data)
90 +{
91 + QemuVncState *state = user_data;
92 + g_autoptr(GError) err = NULL;
93 + g_autoptr(GDBusObjectManager) manager = NULL;
94 + GList *objects, *l;
95 + g_autoptr(GPtrArray) console_paths = NULL;
96 + bool found = false;
97 + Error *local_err = NULL;
98 +
99 + manager = G_DBUS_OBJECT_MANAGER(
100 + g_dbus_object_manager_client_new_finish(res, &err));
101 + if (!manager) {
102 + error_report("Failed to create object manager: %s",
103 + err->message);
104 + g_assert_not_reached();
105 + return;
106 + }
107 +
108 + /*
109 + * Discover all Console objects and sort them so that console
110 + * indices are assigned in a predictable order matching QEMU's.
111 + */
112 + console_paths = g_ptr_array_new_with_free_func(g_free);
113 + objects = g_dbus_object_manager_get_objects(manager);
114 + for (l = objects; l; l = l->next) {
115 + GDBusObject *obj = l->data;
116 + const char *path = g_dbus_object_get_object_path(obj);
117 +
118 + if (g_str_has_prefix(path, DBUS_DISPLAY1_ROOT "/Console_")) {
119 + g_ptr_array_add(console_paths, g_strdup(path));
120 + }
121 + }
122 + g_list_free_full(objects, g_object_unref);
123 +
124 + g_ptr_array_sort(console_paths, (GCompareFunc)qemu_pstrcmp0);
125 +
126 + for (guint i = 0; i < console_paths->len; i++) {
127 + const char *path = g_ptr_array_index(console_paths, i);
128 +
129 + if (!console_setup(state->bus, state->bus_name, path)) {
130 + error_report("Failed to setup console %s", path);
131 + continue;
132 + }
133 + found = true;
134 + }
135 +
136 + if (!found) {
137 + error_report("No consoles found");
138 + state->terminate_reason = g_strdup("No consoles found");
139 + state->terminate = true;
140 + qemu_notify_event();
141 + return;
142 + }
143 +
144 + /*
145 + * Create the VNC display now that consoles exist, so that the
146 + * display change listener is registered against a valid console.
147 + */
148 + if (!vnc_display_new("default", &local_err)) {
149 + error_report("Failed to create VNC display: %s",
150 + error_get_pretty(local_err));
151 + g_assert_not_reached();
152 + return;
153 + }
154 +
155 + vnc_dbus_setup(state->bus);
156 +
157 + clipboard_setup(manager, state->bus);
158 + audio_setup(manager);
159 + if (!state->no_vt) {
160 + chardev_setup(state->chardev_names, manager);
161 + }
162 +}
163 +
164 +static void
165 +start_display_setup(QemuVncState *state)
166 +{
167 + g_autoptr(QemuDBusDisplay1VMProxy) vm_proxy =
168 + QEMU_DBUS_DISPLAY1_VM_PROXY(
169 + qemu_dbus_display1_vm_proxy_new_sync(
170 + state->bus, G_DBUS_PROXY_FLAGS_NONE,
171 + state->bus_name,
172 + DBUS_DISPLAY1_ROOT "/VM", NULL, NULL));
173 + if (vm_proxy) {
174 + qemu_name = g_strdup(qemu_dbus_display1_vm_get_name(
175 + QEMU_DBUS_DISPLAY1_VM(vm_proxy)));
176 + }
177 +
178 + g_dbus_object_manager_client_new(
179 + state->bus,
180 + G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_NONE,
181 + state->bus_name, DBUS_DISPLAY1_ROOT,
182 + dbus_display_get_proxy_type,
183 + NULL, NULL, NULL,
184 + on_manager_ready, state);
185 +}
186 +
187 +static void
188 +on_owner_appeared(GDBusConnection *connection,
189 + const gchar *name,
190 + const gchar *name_owner,
191 + gpointer user_data)
192 +{
193 + QemuVncState *state = user_data;
194 +
195 + if (state->owner_seen) {
196 + return;
197 + }
198 +
199 + info_report("D-Bus name %s appeared.", name);
200 + state->owner_seen = true;
201 + trace_qemu_vnc_owner_appeared(name);
202 + start_display_setup(state);
203 +}
204 +
205 +static void
206 +on_owner_vanished(GDBusConnection *connection,
207 + const gchar *name,
208 + gpointer user_data)
209 +{
210 + QemuVncState *state = user_data;
211 +
212 + trace_qemu_vnc_owner_vanished(name);
213 +
214 + if (!state->owner_seen) {
215 + if (state->wait_for_owner) {
216 + return;
217 + }
218 + error_report("D-Bus name %s not found. "
219 + "Is QEMU running? "
220 + "Use --wait to wait for it to appear.", name);
221 + state->terminate_reason =
222 + g_strdup_printf("D-Bus name %s not found", name);
223 + } else {
224 + error_report("D-Bus peer %s vanished, terminating", name);
225 + state->terminate_reason =
226 + g_strdup_printf("D-Bus peer %s vanished", name);
227 + }
228 +
229 + state->terminate = true;
230 + qemu_notify_event();
231 +}
232 +
233 +static GDBusConnection *
234 +setup_dbus_connection(int dbus_p2p_fd, const char *dbus_address,
235 + char **bus_name)
236 +{
237 + g_autoptr(GError) err = NULL;
238 + GDBusConnection *bus;
239 +
240 + if (dbus_p2p_fd >= 0) {
241 + g_autoptr(GSocket) socket = NULL;
242 + g_autoptr(GSocketConnection) socketc = NULL;
243 +
244 + if (*bus_name) {
245 + error_report("--bus-name is not supported with --dbus-p2p-fd");
246 + return NULL;
247 + }
248 +
249 + socket = g_socket_new_from_fd(dbus_p2p_fd, &err);
250 + if (!socket) {
251 + error_report("Failed to create socket from fd %d: %s",
252 + dbus_p2p_fd, err->message);
253 + return NULL;
254 + }
255 +
256 + socketc = g_socket_connection_factory_create_connection(socket);
257 + if (!socketc) {
258 + error_report("Failed to create socket connection");
259 + return NULL;
260 + }
261 +
262 + bus = g_dbus_connection_new_sync(
263 + G_IO_STREAM(socketc), NULL,
264 + G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT,
265 + NULL, NULL, &err);
266 + } else if (dbus_address) {
267 + GDBusConnectionFlags flags =
268 + G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT;
269 + if (*bus_name) {
270 + flags |= G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION;
271 + }
272 + bus = g_dbus_connection_new_for_address_sync(
273 + dbus_address, flags, NULL, NULL, &err);
274 + } else {
275 + bus = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &err);
276 + if (!*bus_name) {
277 + *bus_name = g_strdup("org.qemu");
278 + }
279 + }
280 +
281 + if (!bus) {
282 + error_report("Failed to connect to D-Bus: %s", err->message);
283 + }
284 +
285 + return bus;
286 +}
287 +
288 +static bool
289 +setup_credentials(const char *tls_creds_dir, const char *tls_authz,
290 + bool *has_vnc_password)
291 +{
292 + Error *local_err = NULL;
293 + const char *creds_dir;
294 +
295 + /*
296 + * Set up TLS credentials if requested. The object must exist
297 + * before vnc_display_open() which looks it up by ID.
298 + */
299 + if (tls_creds_dir) {
300 + if (!object_new_with_props(TYPE_QCRYPTO_TLS_CREDS_X509,
301 + object_get_objects_root(),
302 + "tlscreds0",
303 + &local_err,
304 + "endpoint", "server",
305 + "dir", tls_creds_dir,
306 + "verify-peer", tls_authz ? "yes" : "no",
307 + NULL)) {
308 + error_report_err(local_err);
309 + return false;
310 + }
311 + }
312 +
313 + /*
314 + * Check for systemd credentials: if a vnc-password credential
315 + * file exists, create a QCryptoSecret and enable VNC password auth.
316 + */
317 + creds_dir = g_getenv("CREDENTIALS_DIRECTORY");
318 + if (creds_dir) {
319 + g_autofree char *password_path =
320 + g_build_filename(creds_dir, "vnc-password", NULL);
321 + if (g_file_test(password_path, G_FILE_TEST_EXISTS)) {
322 + if (!object_new_with_props(TYPE_QCRYPTO_SECRET,
323 + object_get_objects_root(),
324 + "vncsecret0",
325 + &local_err,
326 + "file", password_path,
327 + NULL)) {
328 + error_report_err(local_err);
329 + return false;
330 + }
331 + *has_vnc_password = true;
332 + }
333 + }
334 +
335 + return true;
336 +}
337 +
338 +static bool
339 +setup_vnc_opts(const char *vnc_addr, const char *tls_creds_dir,
340 + const char *tls_authz, bool sasl, const char *sasl_authz,
341 + bool has_vnc_password, const char *ws_addr,
342 + const char *share, bool password, bool lossy,
343 + bool non_adaptive)
344 +{
345 + g_autoptr(GString) opts_str = g_string_new(vnc_addr);
346 + QemuOptsList *olist = qemu_find_opts("vnc");
347 + QemuOpts *opts;
348 +
349 + if (tls_creds_dir) {
350 + g_string_append(opts_str, ",tls-creds=tlscreds0");
351 + }
352 + if (tls_authz) {
353 + g_string_append_printf(opts_str, ",tls-authz=%s", tls_authz);
354 + }
355 + if (sasl) {
356 + g_string_append(opts_str, ",sasl=on");
357 + }
358 + if (sasl_authz) {
359 + g_string_append_printf(opts_str, ",sasl-authz=%s", sasl_authz);
360 + }
361 + if (has_vnc_password) {
362 + g_string_append(opts_str, ",password-secret=vncsecret0");
363 + }
364 + if (ws_addr) {
365 + g_string_append_printf(opts_str, ",websocket=%s", ws_addr);
366 + }
367 + if (share) {
368 + g_string_append_printf(opts_str, ",share=%s", share);
369 + }
370 + if (password && !has_vnc_password) {
371 + g_string_append(opts_str, ",password=on");
372 + }
373 + if (lossy) {
374 + g_string_append(opts_str, ",lossy=on");
375 + }
376 + if (non_adaptive) {
377 + g_string_append(opts_str, ",non-adaptive=on");
378 + }
379 +
380 + opts = qemu_opts_parse_noisily(olist, opts_str->str, true);
381 + if (!opts) {
382 + return false;
383 + }
384 + qemu_opts_set_id(opts, g_strdup("default"));
385 + return true;
386 +}
387 +
388 +int
389 +main(int argc, char *argv[])
390 +{
391 + g_autoptr(GError) err = NULL;
392 + g_autoptr(GDBusConnection) bus = NULL;
393 + g_autofree char *dbus_address = NULL;
394 + g_autofree char *bus_name = NULL;
395 + int dbus_p2p_fd = -1;
396 + g_autofree char *vnc_addr = NULL;
397 + g_autofree char *ws_addr = NULL;
398 + g_autofree char *share = NULL;
399 + g_autofree char *tls_creds_dir = NULL;
400 + g_autofree char *tls_authz = NULL;
401 + g_autofree char *sasl_authz = NULL;
402 + g_autofree char *trace_opt = NULL;
403 + g_auto(GStrv) chardev_names = NULL;
404 + g_auto(GStrv) object_strs = NULL;
405 + QemuVncState state = { 0 };
406 + bool has_vnc_password = false;
407 + bool show_version = false;
408 + bool no_vt = false;
409 + bool wait_for_owner = false;
410 + bool password = false;
411 + bool sasl = false;
412 + bool lossy = false;
413 + bool non_adaptive = false;
414 + g_autoptr(GOptionContext) context = NULL;
415 + GOptionEntry entries[] = {
416 + { "dbus-address", 'a', 0, G_OPTION_ARG_STRING, &dbus_address,
417 + "D-Bus address to connect to (default: session bus)", "ADDRESS" },
418 + { "dbus-p2p-fd", 'p', 0, G_OPTION_ARG_INT, &dbus_p2p_fd,
419 + "D-Bus peer-to-peer socket file descriptor", "FD" },
420 + { "bus-name", 'n', 0, G_OPTION_ARG_STRING, &bus_name,
421 + "D-Bus bus name (default: org.qemu)", "NAME" },
422 + { "wait", 'W', 0, G_OPTION_ARG_NONE, &wait_for_owner,
423 + "Wait for the D-Bus name to appear", NULL },
424 + { "vnc-addr", 'l', 0, G_OPTION_ARG_STRING, &vnc_addr,
425 + "VNC display address (default localhost:0, \"none\" to disable)",
426 + "ADDR" },
427 + { "websocket", 'w', 0, G_OPTION_ARG_STRING, &ws_addr,
428 + "WebSocket address (e.g. port number or addr:port)", "ADDR" },
429 + { "share", 's', 0, G_OPTION_ARG_STRING, &share,
430 + "Display sharing policy "
431 + "(allow-exclusive|force-shared|ignore)", "POLICY" },
432 + { "tls-creds", 't', 0, G_OPTION_ARG_STRING, &tls_creds_dir,
433 + "TLS x509 credentials directory", "DIR" },
434 + { "tls-authz", 0, 0, G_OPTION_ARG_STRING, &tls_authz,
435 + "ID of a QAuthZ object for TLS client certificate "
436 + "authorization", "ID" },
437 + { "object", 'O', 0, G_OPTION_ARG_STRING_ARRAY, &object_strs,
438 + "QEMU user-creatable object "
439 + "(e.g. authz-list-file,id=auth0,filename=acl.json)", "OBJDEF" },
440 + { "vt-chardev", 'C', 0, G_OPTION_ARG_STRING_ARRAY, &chardev_names,
441 + "Chardev type names to expose as text console (repeatable, "
442 + "default: serial & hmp)", "NAME" },
443 + { "no-vt", 'N', 0, G_OPTION_ARG_NONE, &no_vt,
444 + "Do not expose any chardevs as text consoles", NULL },
445 + { "keyboard-layout", 'k', 0, G_OPTION_ARG_STRING, &keyboard_layout,
446 + "Keyboard layout", "LAYOUT" },
447 + { "trace", 'T', 0, G_OPTION_ARG_STRING, &trace_opt,
448 + "Trace options (same as QEMU -trace)", "PATTERN" },
449 + { "version", 'V', 0, G_OPTION_ARG_NONE, &show_version,
450 + "Print version information and exit", NULL },
451 + { "password", 0, 0, G_OPTION_ARG_NONE, &password,
452 + "Require password authentication (use D-Bus SetPassword to set)",
453 + NULL },
454 + { "lossy", 0, 0, G_OPTION_ARG_NONE, &lossy,
455 + "Enable lossy compression", NULL },
456 + { "non-adaptive", 0, 0, G_OPTION_ARG_NONE, &non_adaptive,
457 + "Disable adaptive encodings", NULL },
458 + { "sasl", 0, 0, G_OPTION_ARG_NONE, &sasl,
459 + "Enable SASL authentication", NULL },
460 + { "sasl-authz", 0, 0, G_OPTION_ARG_STRING, &sasl_authz,
461 + "ID of a QAuthZ object for SASL username "
462 + "authorization", "ID" },
463 + { NULL }
464 + };
465 +
466 + qemu_init_exec_dir(argv[0]);
467 + qemu_add_data_dir(g_strdup(CONFIG_QEMU_DATADIR));
468 + qemu_add_data_dir(get_relocated_path(CONFIG_QEMU_DATADIR));
469 +
470 + module_call_init(MODULE_INIT_TRACE);
471 + module_call_init(MODULE_INIT_QOM);
472 + module_call_init(MODULE_INIT_OPTS);
473 + qemu_add_opts(&qemu_trace_opts);
474 +
475 + context = g_option_context_new(NULL);
476 + g_option_context_set_summary(context,
477 + "Standalone VNC server connecting to a QEMU instance via the\n"
478 + "D-Bus display interface (org.qemu.Display1).");
479 + g_option_context_add_main_entries(context, entries, NULL);
480 + if (!g_option_context_parse(context, &argc, &argv, &err)) {
481 + error_report("Option parsing failed: %s", err->message);
482 + return 1;
483 + }
484 +
485 + if (show_version) {
486 + printf("qemu-vnc " QEMU_FULL_VERSION "\n");
487 + return 0;
488 + }
489 +
490 + if (trace_opt) {
491 + trace_opt_parse(trace_opt);
492 + qemu_set_log(LOG_TRACE, &error_fatal);
493 + }
494 + trace_init_file();
495 +
496 + qemu_init_main_loop(&error_fatal);
497 +
498 + if (!vnc_addr) {
499 + vnc_addr = g_strdup("localhost:0");
500 + }
501 +
502 + if (object_strs) {
503 + for (int i = 0; object_strs[i]; i++) {
504 + user_creatable_process_cmdline(object_strs[i]);
505 + }
506 + }
507 +
508 + if (tls_authz && !tls_creds_dir) {
509 + error_report("--tls-authz requires --tls-creds");
510 + return 1;
511 + }
512 +
513 + if (sasl_authz && !sasl) {
514 + error_report("--sasl-authz requires --sasl");
515 + return 1;
516 + }
517 +
518 + if (dbus_p2p_fd >= 0 && dbus_address) {
519 + error_report("--dbus-p2p-fd and --dbus-address are"
520 + " mutually exclusive");
521 + return 1;
522 + }
523 +
524 + if (wait_for_owner && dbus_p2p_fd >= 0) {
525 + error_report("--wait is not supported with --dbus-p2p-fd");
526 + return 1;
527 + }
528 +
529 + bus = setup_dbus_connection(dbus_p2p_fd, dbus_address, &bus_name);
530 + if (!bus) {
531 + return 1;
532 + }
533 +
534 + if (wait_for_owner && !bus_name) {
535 + error_report("--wait requires a D-Bus bus name (--bus-name)");
536 + return 1;
537 + }
538 +
539 + if (!setup_credentials(tls_creds_dir, tls_authz, &has_vnc_password)) {
540 + return 1;
541 + }
542 +
543 + if (!setup_vnc_opts(vnc_addr, tls_creds_dir, tls_authz, sasl, sasl_authz,
544 + has_vnc_password, ws_addr, share, password, lossy,
545 + non_adaptive)) {
546 + return 1;
547 + }
548 +
549 + state.bus = bus;
550 + state.bus_name = bus_name;
551 + state.chardev_names = (const char * const *)chardev_names;
552 + state.no_vt = no_vt;
553 + state.wait_for_owner = wait_for_owner;
554 +
555 + g_signal_connect(bus, "closed", G_CALLBACK(on_bus_closed), &state);
556 +
557 + if (bus_name) {
558 + if (wait_for_owner) {
559 + info_report("Waiting for D-Bus name %s to appear...", bus_name);
560 + }
561 + g_bus_watch_name_on_connection(bus, bus_name,
562 + G_BUS_NAME_WATCHER_FLAGS_NONE,
563 + on_owner_appeared,
564 + on_owner_vanished,
565 + &state, NULL);
566 + } else {
567 + state.owner_seen = true;
568 + start_display_setup(&state);
569 + }
570 +
571 + while (!state.terminate) {
572 + main_loop_wait(false);
573 + }
574 +
575 + vnc_dbus_emit_leaving(state.terminate_reason ?: "Shutting down");
576 + vnc_dbus_cleanup();
577 + vnc_cleanup();
578 + g_free(state.terminate_reason);
579 +
580 + return 0;
581 +}
tools/qemu-vnc/qemu-vnc.h new
+49
@@ -0,0 +1,49 @@
1 +/*
2 + * SPDX-License-Identifier: GPL-2.0-or-later
3 + */
4 +#ifndef TOOLS_QEMU_VNC_H
5 +#define TOOLS_QEMU_VNC_H
6 +
7 +#include "qemu/osdep.h"
8 +
9 +#include <gio/gunixfdlist.h>
10 +#include "qemu/dbus.h"
11 +#include "qapi-types-char.h"
12 +#include "ui/console.h"
13 +#include "ui/dbus-display1.h"
14 +
15 +#define TEXT_COLS 80
16 +#define TEXT_ROWS 24
17 +#define TEXT_FONT_WIDTH 8
18 +#define TEXT_FONT_HEIGHT 16
19 +
20 +
21 +QemuTextConsole *qemu_vnc_text_console_new(const char *name,
22 + int fd, bool echo,
23 + ChardevVCEncoding encoding);
24 +
25 +void input_setup(QemuDBusDisplay1Keyboard *kbd,
26 + QemuDBusDisplay1Mouse *mouse);
27 +bool console_setup(GDBusConnection *bus, const char *bus_name,
28 + const char *console_path);
29 +QemuDBusDisplay1Keyboard *console_get_keyboard(QemuConsole *con);
30 +QemuDBusDisplay1Mouse *console_get_mouse(QemuConsole *con);
31 +
32 +void audio_setup(GDBusObjectManager *manager);
33 +void clipboard_setup(GDBusObjectManager *manager, GDBusConnection *bus);
34 +void chardev_setup(const char * const *chardev_names,
35 + GDBusObjectManager *manager);
36 +
37 +GThread *p2p_dbus_thread_new(int fd);
38 +
39 +void vnc_dbus_setup(GDBusConnection *bus);
40 +void vnc_dbus_emit_leaving(const char *reason);
41 +void vnc_dbus_cleanup(void);
42 +void vnc_dbus_client_connected(const char *host, const char *service,
43 + const char *family, bool websocket);
44 +void vnc_dbus_client_initialized(const char *host, const char *service,
45 + const char *x509_dname,
46 + const char *sasl_username);
47 +void vnc_dbus_client_disconnected(const char *host, const char *service);
48 +
49 +#endif /* TOOLS_QEMU_VNC_H */
tools/qemu-vnc/qemu-vnc1.xml new
+201
@@ -0,0 +1,201 @@
1 +<?xml version="1.0" encoding="utf-8"?>
2 +<!--
3 + SPDX-License-Identifier: GPL-2.0-or-later
4 + -->
5 +<node>
6 + <!--
7 + org.qemu.Vnc1.Server:
8 +
9 + This interface is implemented on ``/org/qemu/Vnc1/Server``.
10 + It provides management and monitoring of the VNC server.
11 + -->
12 + <interface name="org.qemu.Vnc1.Server">
13 + <!--
14 + Name:
15 +
16 + The VM name.
17 + -->
18 + <property name="Name" type="s" access="read"/>
19 +
20 + <!--
21 + Auth:
22 +
23 + Primary authentication method (none, vnc, vencrypt, sasl, etc.).
24 + -->
25 + <property name="Auth" type="s" access="read"/>
26 +
27 + <!--
28 + VencryptSubAuth:
29 +
30 + VEncrypt sub-authentication method, if applicable.
31 + Empty string otherwise.
32 + -->
33 + <property name="VencryptSubAuth" type="s" access="read"/>
34 +
35 + <!--
36 + Clients:
37 +
38 + Object paths of connected VNC clients.
39 + -->
40 + <property name="Clients" type="ao" access="read"/>
41 +
42 + <!--
43 + Listeners:
44 +
45 + List of listening sockets. Each entry is a dictionary with keys:
46 + ``Host`` (s), ``Service`` (s), ``Family`` (s),
47 + ``WebSocket`` (b), ``Auth`` (s), ``VencryptSubAuth`` (s).
48 + -->
49 + <property name="Listeners" type="aa{sv}" access="read"/>
50 +
51 + <!--
52 + SetPassword:
53 + @password: The new VNC password.
54 +
55 + Change the VNC password. Existing clients are unaffected.
56 + -->
57 + <method name="SetPassword">
58 + <arg type="s" name="password" direction="in"/>
59 + </method>
60 +
61 + <!--
62 + ExpirePassword:
63 + @time: Expiry specification.
64 +
65 + Set password expiry. Values: ``"now"``, ``"never"``,
66 + ``"+N"`` (seconds from now), ``"N"`` (absolute epoch seconds).
67 + -->
68 + <method name="ExpirePassword">
69 + <arg type="s" name="time" direction="in"/>
70 + </method>
71 +
72 + <!--
73 + ReloadCertificates:
74 +
75 + Reload TLS certificates from disk.
76 + -->
77 + <method name="ReloadCertificates"/>
78 +
79 + <!--
80 + AddClient:
81 + @socket: file descriptor of a connected socket.
82 + @skipauth: whether to skip VNC authentication.
83 +
84 + Add a VNC client from an already-connected socket.
85 + -->
86 + <method name="AddClient">
87 + <arg type="h" name="socket" direction="in"/>
88 + <arg type="b" name="skipauth" direction="in"/>
89 + </method>
90 +
91 + <!--
92 + ClientConnected:
93 + @client: Object path of the new client.
94 +
95 + Emitted when a VNC client TCP connection is established
96 + (before authentication).
97 + -->
98 + <signal name="ClientConnected">
99 + <arg type="o" name="client"/>
100 + </signal>
101 +
102 + <!--
103 + ClientInitialized:
104 + @client: Object path of the client.
105 +
106 + Emitted when a VNC client has completed authentication
107 + and is active.
108 + -->
109 + <signal name="ClientInitialized">
110 + <arg type="o" name="client"/>
111 + </signal>
112 +
113 + <!--
114 + ClientDisconnected:
115 + @client: Object path of the client.
116 +
117 + Emitted when a VNC client disconnects.
118 + -->
119 + <signal name="ClientDisconnected">
120 + <arg type="o" name="client"/>
121 + </signal>
122 +
123 + <!--
124 + Leaving:
125 + @reason: A human-readable reason for shutting down (e.g.
126 + "D-Bus peer org.qemu vanished").
127 +
128 + Emitted when the VNC server is shutting down cleanly.
129 + Clients should expect the connection to close shortly after.
130 + -->
131 + <signal name="Leaving">
132 + <arg type="s" name="reason"/>
133 + </signal>
134 + </interface>
135 +
136 + <!--
137 + org.qemu.Vnc1.Client:
138 +
139 + This interface is implemented on ``/org/qemu/Vnc1/Client_$id``.
140 + It exposes information about a connected VNC client.
141 + -->
142 + <interface name="org.qemu.Vnc1.Client">
143 + <!--
144 + Host:
145 +
146 + Client IP address.
147 + -->
148 + <property name="Host" type="s" access="read"/>
149 +
150 + <!--
151 + Service:
152 +
153 + Client port or service name. This may depend on the host system’s
154 + service database so symbolic names should not be relied on.
155 + -->
156 + <property name="Service" type="s" access="read"/>
157 +
158 + <!--
159 + Family:
160 +
161 + Address family (ipv4, ipv6, unix).
162 + -->
163 + <property name="Family" type="s" access="read"/>
164 +
165 + <!--
166 + WebSocket:
167 +
168 + Whether this is a WebSocket connection.
169 + -->
170 + <property name="WebSocket" type="b" access="read"/>
171 +
172 + <!--
173 + X509Dname:
174 +
175 + X.509 distinguished name (empty if not applicable).
176 + -->
177 + <property name="X509Dname" type="s" access="read"/>
178 +
179 + <!--
180 + SaslUsername:
181 +
182 + SASL username (empty if not applicable).
183 + -->
184 + <property name="SaslUsername" type="s" access="read"/>
185 +
186 + <!--
187 + ShutdownRequest:
188 +
189 + Emitted when the VNC client requests a guest shutdown.
190 + -->
191 + <signal name="ShutdownRequest"/>
192 +
193 + <!--
194 + ResetRequest:
195 +
196 + Emitted when the VNC client requests a guest reset.
197 + -->
198 + <signal name="ResetRequest"/>
199 + </interface>
200 +
201 +</node>
tools/qemu-vnc/stubs.c new
+62
@@ -0,0 +1,62 @@
1 +/*
2 + * Stubs for qemu-vnc standalone binary.
3 + *
4 + * SPDX-License-Identifier: GPL-2.0-or-later
5 + */
6 +
7 +#include "qemu/osdep.h"
8 +
9 +#include "system/runstate.h"
10 +#include "hw/core/qdev.h"
11 +#include "monitor/monitor.h"
12 +#include "migration/vmstate.h"
13 +
14 +bool runstate_is_running(void)
15 +{
16 + return true;
17 +}
18 +
19 +bool phase_check(MachineInitPhase phase)
20 +{
21 + return true;
22 +}
23 +
24 +DeviceState *qdev_find_recursive(BusState *bus, const char *id)
25 +{
26 + return NULL;
27 +}
28 +
29 +/*
30 + * Provide the monitor stubs locally so that the linker does not
31 + * pull stubs/monitor-core.c.o from libqemuutil.a (which would
32 + * bring a conflicting qapi_event_emit definition).
33 + */
34 +Monitor *monitor_cur(void)
35 +{
36 + return NULL;
37 +}
38 +
39 +bool monitor_cur_is_qmp(void)
40 +{
41 + return false;
42 +}
43 +
44 +Monitor *monitor_set_cur(Coroutine *co, Monitor *mon)
45 +{
46 + return NULL;
47 +}
48 +
49 +int monitor_vprintf(Monitor *mon, const char *fmt, va_list ap)
50 +{
51 + return -1;
52 +}
53 +
54 +/*
55 + * Link-time stubs for VMState symbols referenced by VNC code.
56 + * The standalone binary never performs migration, so these are
57 + * never actually used at runtime.
58 + */
59 +const VMStateInfo vmstate_info_bool = {};
60 +const VMStateInfo vmstate_info_int32 = {};
61 +const VMStateInfo vmstate_info_uint32 = {};
62 +const VMStateInfo vmstate_info_buffer = {};
tools/qemu-vnc/trace-events new
+21
@@ -0,0 +1,21 @@
1 +qemu_vnc_audio_out_fini(uint64_t id) "id=%" PRIu64
2 +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"
3 +qemu_vnc_audio_out_set_enabled(uint64_t id, bool enabled) "id=%" PRIu64 " enabled=%d"
4 +qemu_vnc_audio_out_write(uint64_t id, size_t size) "id=%" PRIu64 " size=%zu"
5 +qemu_vnc_chardev_connected(const char *name) "name=%s"
6 +qemu_vnc_clipboard_grab(int selection, uint32_t serial) "selection=%d serial=%u"
7 +qemu_vnc_clipboard_release(int selection) "selection=%d"
8 +qemu_vnc_clipboard_request(int selection) "selection=%d"
9 +qemu_vnc_client_not_found(const char *host, const char *service) "host=%s service=%s"
10 +qemu_vnc_console_io_error(const char *name) "name=%s"
11 +qemu_vnc_cursor_define(int width, int height, int hot_x, int hot_y) "w=%d h=%d hot=%d,%d"
12 +qemu_vnc_input_abs(uint32_t x, uint32_t y) "x=%u y=%u"
13 +qemu_vnc_input_btn(int button, bool press) "button=%d press=%d"
14 +qemu_vnc_input_rel(int dx, int dy) "dx=%d dy=%d"
15 +qemu_vnc_key_event(int qcode, bool down) "qcode=%d down=%d"
16 +qemu_vnc_owner_appeared(const char *name) "peer=%s"
17 +qemu_vnc_owner_vanished(const char *name) "peer=%s"
18 +qemu_vnc_scanout(uint32_t width, uint32_t height, uint32_t stride, uint32_t format) "w=%u h=%u stride=%u fmt=0x%x"
19 +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"
20 +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"
21 +qemu_vnc_update_map(uint32_t x, uint32_t y, uint32_t w, uint32_t h) "x=%u y=%u w=%u h=%u"
tools/qemu-vnc/trace.h new
+4
@@ -0,0 +1,4 @@
1 +/*
2 + * SPDX-License-Identifier: GPL-2.0-or-later
3 + */
4 +#include "trace/trace-tools_qemu_vnc.h"
tools/qemu-vnc/utils.c new
+59
@@ -0,0 +1,59 @@
1 +/*
2 + * Standalone VNC server connecting to QEMU via D-Bus display interface.
3 + *
4 + * Copyright (C) 2026 Red Hat, Inc.
5 + *
6 + * SPDX-License-Identifier: GPL-2.0-or-later
7 + */
8 +
9 +#include "qemu/osdep.h"
10 +
11 +#include "qemu/error-report.h"
12 +#include "qemu-vnc.h"
13 +
14 +static GDBusConnection *
15 +dbus_p2p_from_fd(int fd)
16 +{
17 + g_autoptr(GError) err = NULL;
18 + g_autoptr(GSocket) socket = NULL;
19 + g_autoptr(GSocketConnection) socketc = NULL;
20 + GDBusConnection *conn;
21 +
22 + socket = g_socket_new_from_fd(fd, &err);
23 + if (!socket) {
24 + error_report("Failed to create socket: %s", err->message);
25 + return NULL;
26 + }
27 +
28 + socketc = g_socket_connection_factory_create_connection(socket);
29 + if (!socketc) {
30 + error_report("Failed to create socket connection");
31 + return NULL;
32 + }
33 +
34 + conn = g_dbus_connection_new_sync(
35 + G_IO_STREAM(socketc), NULL,
36 + G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT |
37 + G_DBUS_CONNECTION_FLAGS_DELAY_MESSAGE_PROCESSING,
38 + NULL, NULL, &err);
39 + if (!conn) {
40 + error_report("Failed to create D-Bus connection: %s", err->message);
41 + return NULL;
42 + }
43 +
44 + return conn;
45 +}
46 +
47 +static gpointer
48 +p2p_server_setup_thread(gpointer data)
49 +{
50 + return dbus_p2p_from_fd(GPOINTER_TO_INT(data));
51 +}
52 +
53 +GThread *
54 +p2p_dbus_thread_new(int fd)
55 +{
56 + return g_thread_new("p2p-server-setup",
57 + p2p_server_setup_thread,
58 + GINT_TO_POINTER(fd));
59 +}