master
c 581 lines 18.1 KB
Raw
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 }