From: Marc-André Lureau <marcandre.lureau@redhat.com> Add helpers to manage the standalone qemu-vnc VNC server process. The qemu-vnc server connects to QEMU via the D-Bus display interface, providing VNC access decoupled from the QEMU process. The helper handles process lifecycle (start/stop), D-Bus name watching, and provides D-Bus methods for password management, certificate reloading, and client connections. Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com> --- po/POTFILES | 1 + src/qemu/meson.build | 1 + src/qemu/qemu_vnc.c | 407 +++++++++++++++++++++++++++++++++++++++++++ src/qemu/qemu_vnc.h | 41 +++++ 4 files changed, 450 insertions(+) create mode 100644 src/qemu/qemu_vnc.c create mode 100644 src/qemu/qemu_vnc.h diff --git a/po/POTFILES b/po/POTFILES index a5f8705eb8..a4e3ed6f25 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -208,6 +208,7 @@ src/qemu/qemu_validate.c src/qemu/qemu_vhost_user.c src/qemu/qemu_vhost_user_gpu.c src/qemu/qemu_virtiofs.c +src/qemu/qemu_vnc.c src/remote/libvirtd.policy.in src/remote/remote_daemon.c src/remote/remote_daemon_config.c diff --git a/src/qemu/meson.build b/src/qemu/meson.build index b4fb62f14f..9737e50734 100644 --- a/src/qemu/meson.build +++ b/src/qemu/meson.build @@ -43,6 +43,7 @@ qemu_driver_sources = [ 'qemu_vhost_user.c', 'qemu_vhost_user_gpu.c', 'qemu_virtiofs.c', + 'qemu_vnc.c', ] driver_source_files += files(qemu_driver_sources) diff --git a/src/qemu/qemu_vnc.c b/src/qemu/qemu_vnc.c new file mode 100644 index 0000000000..4afdf6aa79 --- /dev/null +++ b/src/qemu/qemu_vnc.c @@ -0,0 +1,407 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include <config.h> + +#include <gio/gio.h> + +#include "qemu_dbus.h" +#include "qemu_extdevice.h" +#include "qemu_security.h" +#include "qemu_vnc.h" +#include "virerror.h" +#include "virlog.h" +#include "virpidfile.h" +#include "virgdbus.h" + +#define VIR_FROM_THIS VIR_FROM_NONE + +VIR_LOG_INIT("qemu.vnc"); + +#define ORG_QEMU_VNC "org.qemu.vnc" +#define ORG_QEMU_VNC_PATH "/org/qemu/Vnc1/Server" +#define ORG_QEMU_VNC_IFACE "org.qemu.Vnc1.Server" + + +void +qemuVncFree(qemuVnc *vnc) +{ + if (!vnc) + return; + + g_free(vnc); +} + + +qemuVnc * +qemuVncNew(void) +{ + g_autoptr(qemuVnc) vnc = g_new0(qemuVnc, 1); + + vnc->pid = -1; + + return g_steal_pointer(&vnc); +} + + +static char * +qemuVncCreatePidFilename(virDomainObj *vm) +{ + qemuDomainObjPrivate *priv = vm->privateData; + virQEMUDriver *driver = priv->driver; + g_autofree char *shortName = virDomainDefGetShortName(vm->def); + g_autoptr(virQEMUDriverConfig) cfg = virQEMUDriverGetConfig(driver); + g_autofree char *name = NULL; + + name = g_strdup_printf("%s-vnc", shortName); + + return virPidFileBuildPath(cfg->vncStateDir, name); +} + + +void +qemuVncStop(virDomainObj *vm, virDomainGraphicsDef *gfx) +{ + qemuDomainObjPrivate *priv = vm->privateData; + qemuDomainGraphicsPrivate *gfxpriv = QEMU_DOMAIN_GRAPHICS_PRIVATE(gfx); + qemuVnc *vnc = gfxpriv->vnc; + g_autofree char *pidfile = qemuVncCreatePidFilename(vm); + virErrorPtr orig_err; + + if (!vnc) + return; + + if (vnc->leaving_id) { + g_dbus_connection_signal_unsubscribe(priv->dbusConnection, vnc->leaving_id); + vnc->leaving_id = 0; + } + g_clear_handle_id(&vnc->name_watch, g_bus_unwatch_name); + + virErrorPreserveLast(&orig_err); + + if (virPidFileForceCleanupPath(pidfile) < 0) { + VIR_WARN("Unable to kill qemu-vnc process"); + } else { + vnc->pid = -1; + } + + virErrorRestore(&orig_err); +} + + +int +qemuVncSetupCgroup(qemuVnc *vnc, + virCgroup *cgroup) +{ + return virCgroupAddProcess(cgroup, vnc->pid); +} + + +static void +on_leaving_signal(GDBusConnection *connection, + const gchar *sender_name G_GNUC_UNUSED, + const gchar *object_path G_GNUC_UNUSED, + const gchar *interface_name G_GNUC_UNUSED, + const gchar *signal_name G_GNUC_UNUSED, + GVariant *parameters, + gpointer user_data) +{ + qemuVnc *vnc = user_data; + const gchar *reason; + + g_variant_get(parameters, "(&s)", &reason); + VIR_DEBUG("%s.Leaving reason: '%s'", ORG_QEMU_VNC_IFACE, reason); + g_dbus_connection_signal_unsubscribe(connection, vnc->leaving_id); + vnc->leaving_id = 0; +} + + +static void +name_appeared_cb(GDBusConnection *connection, + const gchar *name G_GNUC_UNUSED, + const gchar *name_owner G_GNUC_UNUSED, + gpointer user_data) +{ + qemuVnc *vnc = user_data; + + VIR_DEBUG("'%s' appeared", name); + vnc->name_appeared = true; + + if (!vnc->leaving_id) { + vnc->leaving_id = g_dbus_connection_signal_subscribe( + connection, + ORG_QEMU_VNC, + ORG_QEMU_VNC_IFACE, + "Leaving", + ORG_QEMU_VNC_PATH, + NULL, + G_DBUS_SIGNAL_FLAGS_NONE, + on_leaving_signal, + vnc, + NULL); + } +} + + +static void +name_vanished_cb(GDBusConnection *connection G_GNUC_UNUSED, + const gchar *name, + gpointer user_data) +{ + qemuVnc *vnc = user_data; + + if (vnc->name_appeared && vnc->leaving_id) { + virReportError(VIR_ERR_INTERNAL_ERROR, _("'%1$s' vanished unexpectedly"), name); + } +} + + +int +qemuVncStart(virDomainObj *vm, virDomainGraphicsDef *gfx) +{ + qemuDomainObjPrivate *priv = vm->privateData; + virQEMUDriver *driver = priv->driver; + qemuDomainGraphicsPrivate *gfxpriv = QEMU_DOMAIN_GRAPHICS_PRIVATE(gfx); + qemuVnc *vnc = gfxpriv->vnc; + virDomainGraphicsListenDef *glisten = NULL; + g_autoptr(virQEMUDriverConfig) cfg = virQEMUDriverGetConfig(driver); + g_autoptr(virCommand) cmd = NULL; + g_autofree char *shortName = virDomainDefGetShortName(vm->def); + g_autofree char *pidfile = NULL; + g_autofree char *logname = NULL; + g_autofree char *dbus_addr = qemuDBusGetAddress(driver, vm); + g_auto(virBuffer) vnc_addr = VIR_BUFFER_INITIALIZER; + pid_t pid = -1; + int logfd = -1; + g_autoptr(domainLogContext) logContext = NULL; + + if (vnc->pid != -1) { + return 0; + } + + if (!dbus_addr) { + virReportError(VIR_ERR_INTERNAL_ERROR, "%s", _("no D-Bus address")); + return -1; + } + + if (!(pidfile = qemuVncCreatePidFilename(vm))) + return -1; + + logname = g_strdup_printf("%s-qemu-vnc", shortName); + if (!(logContext = domainLogContextNew(cfg->stdioLogD, cfg->logDir, + QEMU_DRIVER_NAME, + vm, driver->privileged, + logname))) { + virLastErrorPrefixMessage("%s", _("can't open log context")); + return -1; + } + + logfd = domainLogContextGetWriteFD(logContext); + + cmd = virCommandNew(cfg->qemuVncName); + virCommandClearCaps(cmd); + virCommandSetPidFile(cmd, pidfile); + virCommandSetOutputFD(cmd, &logfd); + virCommandSetErrorFD(cmd, &logfd); + virCommandDaemonize(cmd); + virCommandAddArgPair(cmd, "--dbus-address", dbus_addr); + virCommandAddArgPair(cmd, "--bus-name", "org.qemu"); + virCommandAddArg(cmd, "--wait"); + + /* Build VNC listen address */ + if ((glisten = virDomainGraphicsGetListen(gfx, 0))) { + switch (glisten->type) { + case VIR_DOMAIN_GRAPHICS_LISTEN_TYPE_ADDRESS: + case VIR_DOMAIN_GRAPHICS_LISTEN_TYPE_NETWORK: + if (glisten->address) { + bool escapeAddr = strchr(glisten->address, ':') != NULL; + if (escapeAddr) + virBufferAsprintf(&vnc_addr, "[%s]", glisten->address); + else + virBufferAdd(&vnc_addr, glisten->address, -1); + } else { + virBufferAddLit(&vnc_addr, "localhost"); + } + virBufferAsprintf(&vnc_addr, ":%d", + gfx->data.vnc.port > 0 + ? gfx->data.vnc.port - 5900 : 0); + break; + case VIR_DOMAIN_GRAPHICS_LISTEN_TYPE_SOCKET: + virBufferAsprintf(&vnc_addr, "unix:%s", glisten->socket); + break; + case VIR_DOMAIN_GRAPHICS_LISTEN_TYPE_NONE: + virBufferAddLit(&vnc_addr, "none"); + break; + case VIR_DOMAIN_GRAPHICS_LISTEN_TYPE_LAST: + default: + virBufferAddLit(&vnc_addr, "localhost:0"); + break; + } + + virCommandAddArg(cmd, "--vnc-addr"); + virCommandAddArgBuffer(cmd, &vnc_addr); + } + + if (gfx->data.vnc.websocket > 0) { + g_autofree char *ws = g_strdup_printf("%d", gfx->data.vnc.websocket); + virCommandAddArgPair(cmd, "--websocket", ws); + } + + if (gfx->data.vnc.sharePolicy != VIR_DOMAIN_GRAPHICS_VNC_SHARE_DEFAULT) { + virCommandAddArgPair(cmd, "--share", + virDomainGraphicsVNCSharePolicyTypeToString( + gfx->data.vnc.sharePolicy)); + } + + if (gfx->data.vnc.keymap) + virCommandAddArgPair(cmd, "--keyboard-layout", gfx->data.vnc.keymap); + + if (gfx->data.vnc.auth.passwd || cfg->vncPassword) + virCommandAddArg(cmd, "--password"); + + if (cfg->vncTLS && cfg->vncTLSx509certdir) + virCommandAddArgPair(cmd, "--tls-creds", cfg->vncTLSx509certdir); + + if (cfg->vncSASL) { + virCommandAddArg(cmd, "--sasl"); + + if (cfg->vncSASLdir) + virCommandAddEnvPair(cmd, "SASL_CONF_PATH", cfg->vncSASLdir); + } + + vnc->name_watch = g_bus_watch_name_on_connection(priv->dbusConnection, + ORG_QEMU_VNC, + G_BUS_NAME_WATCHER_FLAGS_NONE, + name_appeared_cb, + name_vanished_cb, + vnc, + NULL); + + if (qemuExtDeviceLogCommand(driver, vm, cmd, "qemu-vnc") < 0) + goto error; + + if (qemuSecurityCommandRun(driver, vm, cmd, -1, -1, false, NULL) < 0) + goto error; + + if (virPidFileReadPath(pidfile, &pid) < 0) { + virReportError(VIR_ERR_INTERNAL_ERROR, + _("Unable to read qemu-vnc pidfile '%1$s'"), + pidfile); + goto error; + } + + if (virProcessKill(pid, 0) != 0) { + g_autofree char *msg = NULL; + + if (domainLogContextReadFiltered(logContext, &msg, 1024) < 0) + VIR_WARN("Unable to read from qemu-vnc log"); + + virReportError(VIR_ERR_OPERATION_FAILED, + _("qemu-vnc died and reported:\n%1$s"), + NULLSTR(msg)); + goto error; + } + + vnc->pid = pid; + + return 0; + + error: + g_clear_handle_id(&vnc->name_watch, g_bus_unwatch_name); + qemuVncStop(vm, gfx); + return -1; +} + + +int +qemuVncSetPassword(virDomainObj *vm, + const char *password) +{ + qemuDomainObjPrivate *priv = vm->privateData; + g_autoptr(GVariant) args = NULL; + + args = g_variant_new("(s)", password); + + return virGDBusCallMethod(priv->dbusConnection, + NULL, + G_VARIANT_TYPE("()"), + NULL, + ORG_QEMU_VNC, + ORG_QEMU_VNC_PATH, + ORG_QEMU_VNC_IFACE, + "SetPassword", + args); +} + + +int +qemuVncReloadCertificates(virDomainObj *vm) +{ + qemuDomainObjPrivate *priv = vm->privateData; + + return virGDBusCallMethod(priv->dbusConnection, + NULL, + G_VARIANT_TYPE("()"), + NULL, + ORG_QEMU_VNC, + ORG_QEMU_VNC_PATH, + ORG_QEMU_VNC_IFACE, + "ReloadCertificates", + NULL); +} + + +int +qemuVncAddClient(virDomainObj *vm, + int fd, + bool skipauth) +{ + qemuDomainObjPrivate *priv = vm->privateData; + g_autoptr(GUnixFDList) fdlist = NULL; + g_autoptr(GVariant) args = NULL; + gint fd_idx; + + fdlist = g_unix_fd_list_new(); + fd_idx = g_unix_fd_list_append(fdlist, fd, NULL); + if (fd_idx < 0) { + virReportError(VIR_ERR_INTERNAL_ERROR, "%s", + _("failed to append FD to list")); + return -1; + } + + args = g_variant_new("(hb)", fd_idx, skipauth); + + return virGDBusCallMethodWithFD(priv->dbusConnection, + NULL, + G_VARIANT_TYPE("()"), + NULL, + NULL, + ORG_QEMU_VNC, + ORG_QEMU_VNC_PATH, + ORG_QEMU_VNC_IFACE, + "AddClient", + args, + fdlist); +} + + +/** + * qemuVncAvailable: + * @helper: name (or path to) 'qemu-vnc' binary + * + * Returns whether 'qemu-vnc' is available. + * + * Important: + * This function is called from 'virQEMUDriverGetDomainCapabilities'. It must + * not report any errors and must not add any additional checks. + * + * This function is mocked from 'tests/testutilsqemu.c' + */ +bool +qemuVncAvailable(const char *helper) +{ + g_autofree char *helperPath = NULL; + + return !!(helperPath = virFindFileInPath(helper)); +} diff --git a/src/qemu/qemu_vnc.h b/src/qemu/qemu_vnc.h new file mode 100644 index 0000000000..6a0d2124c5 --- /dev/null +++ b/src/qemu/qemu_vnc.h @@ -0,0 +1,41 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#include "qemu_conf.h" + +typedef struct _qemuVnc qemuVnc; +struct _qemuVnc { + pid_t pid; + guint name_watch; + bool name_appeared; + guint leaving_id; +}; + +bool qemuVncAvailable(const char *helper); + +qemuVnc *qemuVncNew(void); + +void qemuVncFree(qemuVnc *vnc); + +int qemuVncStart(virDomainObj *vm, + virDomainGraphicsDef *gfx); + +void qemuVncStop(virDomainObj *vm, + virDomainGraphicsDef *gfx); + +int qemuVncSetupCgroup(qemuVnc *vnc, + virCgroup *cgroup); + +int qemuVncSetPassword(virDomainObj *vm, + const char *password); + +int qemuVncReloadCertificates(virDomainObj *vm); + +int qemuVncAddClient(virDomainObj *vm, + int fd, + bool skipauth); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(qemuVnc, qemuVncFree); -- 2.53.0