blob: 8953a4b6265d94be514540d623e180a2de20830e [file]
/*
* D-Bus VNC server (qemu-vnc) end-to-end test
*
* Copyright (c) 2026 Red Hat, Inc.
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "qemu/osdep.h"
#include <gio/gio.h>
#include <gvnc.h>
#include <sys/un.h>
#include "qemu/sockets.h"
#include "libqtest.h"
#include "qemu-vnc1.h"
#ifdef CONFIG_TASN1
#include "tests/unit/crypto-tls-x509-helpers.h"
#endif
#define VNC_TEST_TIMEOUT_MS 10000
typedef struct DbusTest {
QTestState *qts;
GSubprocess *vnc_subprocess;
GTestDBus *bus;
GDBusConnection *bus_conn;
GMainLoop *loop;
char *vnc_sock_path;
char *tmp_dir;
char *bus_addr;
} DbusTest;
typedef struct LifecycleData {
DbusTest *dt;
QemuVnc1Server *server_proxy;
VncConnection *conn;
char *client_path;
gboolean got_connected;
gboolean got_initialized;
gboolean got_disconnected;
} LifecycleData;
static QemuVnc1Server *
create_server_proxy(GDBusConnection *bus_conn, GError **errp)
{
return qemu_vnc1_server_proxy_new_sync(
bus_conn,
G_DBUS_PROXY_FLAGS_NONE,
"org.qemu.vnc",
"/org/qemu/Vnc1/Server",
NULL, errp);
}
static void
on_vnc_error(VncConnection *self, const char *msg)
{
g_error("vnc-error: %s", msg);
}
static void
on_vnc_auth_failure(VncConnection *self, const char *msg)
{
g_error("vnc-auth-failure: %s", msg);
}
static void
on_vnc_initialized(VncConnection *self, GMainLoop *loop)
{
const char *name = vnc_connection_get_name(self);
g_assert_cmpstr(name, ==, "QEMU (dbus-vnc-test)");
g_main_loop_quit(loop);
}
static gboolean
timeout_cb(gpointer data)
{
g_error("test timed out");
return G_SOURCE_REMOVE;
}
static int
connect_unix_socket(const char *path)
{
int fd;
struct sockaddr_un addr = { .sun_family = AF_UNIX };
fd = socket(AF_UNIX, SOCK_STREAM, 0);
g_assert(fd >= 0);
snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", path);
if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
close(fd);
return -1;
}
return fd;
}
static int
wait_for_vnc_socket(const char *path, int timeout_ms)
{
int elapsed = 0;
const int interval = 50;
while (elapsed < timeout_ms) {
int fd = connect_unix_socket(path);
if (fd >= 0) {
return fd;
}
g_usleep(interval * 1000);
elapsed += interval;
}
return -1;
}
static GSubprocess *
spawn_qemu_vnc(int dbus_fd, const char *sock_path)
{
const char *binary;
g_autoptr(GError) err = NULL;
g_autoptr(GSubprocessLauncher) launcher = NULL;
GSubprocess *proc;
g_autofree char *fd_str = NULL;
g_autofree char *vnc_addr = NULL;
binary = g_getenv("QTEST_QEMU_VNC_BINARY");
g_assert(binary != NULL);
fd_str = g_strdup_printf("%d", dbus_fd);
vnc_addr = g_strdup_printf("unix:%s", sock_path);
launcher = g_subprocess_launcher_new(G_SUBPROCESS_FLAGS_NONE);
g_subprocess_launcher_take_fd(launcher, dbus_fd, dbus_fd);
proc = g_subprocess_launcher_spawn(launcher, &err,
binary,
"--dbus-p2p-fd", fd_str,
"--vnc-addr", vnc_addr,
NULL);
g_assert_no_error(err);
g_assert(proc != NULL);
return proc;
}
static GSubprocess *
spawn_qemu_vnc_bus_full(const char *dbus_addr, const char *sock_path,
const char *const *extra_args)
{
const char *binary;
g_autoptr(GError) err = NULL;
g_autoptr(GSubprocessLauncher) launcher = NULL;
g_autoptr(GPtrArray) argv = NULL;
GSubprocess *proc;
g_autofree char *vnc_addr = NULL;
binary = g_getenv("QTEST_QEMU_VNC_BINARY");
g_assert(binary != NULL);
vnc_addr = g_strdup_printf("unix:%s", sock_path);
argv = g_ptr_array_new();
g_ptr_array_add(argv, (gpointer)binary);
g_ptr_array_add(argv, (gpointer)"--dbus-address");
g_ptr_array_add(argv, (gpointer)dbus_addr);
g_ptr_array_add(argv, (gpointer)"--bus-name");
g_ptr_array_add(argv, (gpointer)"org.qemu");
g_ptr_array_add(argv, (gpointer)"--vnc-addr");
g_ptr_array_add(argv, (gpointer)vnc_addr);
if (extra_args) {
for (int i = 0; extra_args[i]; i++) {
g_ptr_array_add(argv, (gpointer)extra_args[i]);
}
}
g_ptr_array_add(argv, NULL);
launcher = g_subprocess_launcher_new(G_SUBPROCESS_FLAGS_NONE);
proc = g_subprocess_launcher_spawnv(launcher,
(const char *const *)argv->pdata, &err);
g_assert_no_error(err);
g_assert(proc != NULL);
return proc;
}
static void
name_appeared_cb(GDBusConnection *connection,
const gchar *name,
const gchar *name_owner,
gpointer user_data)
{
gboolean *appeared = user_data;
*appeared = TRUE;
}
static bool
setup_dbus_test_full(DbusTest *dt, const char *const *vnc_extra_args)
{
g_autoptr(GError) err = NULL;
g_auto(GStrv) addr_parts = NULL;
g_autofree char *qemu_args = NULL;
if (!g_getenv("QTEST_QEMU_VNC_BINARY")) {
g_test_skip("QTEST_QEMU_VNC_BINARY not set");
return false;
}
dt->bus = g_test_dbus_new(G_TEST_DBUS_NONE);
g_test_dbus_up(dt->bus);
/* remove ,guid=foo part */
addr_parts = g_strsplit(g_test_dbus_get_bus_address(dt->bus), ",", 2);
dt->bus_addr = g_strdup(addr_parts[0]);
dt->bus_conn = g_dbus_connection_new_for_address_sync(
g_test_dbus_get_bus_address(dt->bus),
G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT |
G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION,
NULL, NULL, &err);
g_assert_no_error(err);
qemu_args = g_strdup_printf("-display dbus,addr=%s "
"-name dbus-vnc-test", dt->bus_addr);
dt->qts = qtest_init(qemu_args);
dt->tmp_dir = g_dir_make_tmp("dbus-vnc-test-XXXXXX", NULL);
g_assert(dt->tmp_dir != NULL);
dt->vnc_sock_path = g_build_filename(dt->tmp_dir, "vnc.sock", NULL);
dt->vnc_subprocess = spawn_qemu_vnc_bus_full(dt->bus_addr,
dt->vnc_sock_path,
vnc_extra_args);
/*
* Wait for the org.qemu.vnc bus name to appear, which indicates
* qemu-vnc has fully initialized (connected to QEMU, set up the
* display, exported its D-Bus interfaces, and opened the VNC
* socket).
*/
{
guint watch_id, timeout_id;
gboolean appeared = FALSE;
watch_id = g_bus_watch_name_on_connection(
dt->bus_conn, "org.qemu.vnc",
G_BUS_NAME_WATCHER_FLAGS_NONE,
name_appeared_cb, NULL, &appeared, NULL);
timeout_id = g_timeout_add_seconds(10, timeout_cb, NULL);
while (!appeared) {
if (!g_main_context_iteration(NULL, TRUE)) {
break;
}
}
g_bus_unwatch_name(watch_id);
g_source_remove(timeout_id);
if (!appeared) {
g_test_fail();
g_test_message("Timed out waiting for org.qemu.vnc bus name");
return false;
}
}
return true;
}
static bool
setup_dbus_test(DbusTest *dt)
{
return setup_dbus_test_full(dt, NULL);
}
static void
cleanup_dbus_test(DbusTest *dt)
{
if (dt->bus_conn) {
g_dbus_connection_close_sync(dt->bus_conn, NULL, NULL);
g_object_unref(dt->bus_conn);
}
if (dt->vnc_subprocess) {
g_subprocess_force_exit(dt->vnc_subprocess);
g_subprocess_wait(dt->vnc_subprocess, NULL, NULL);
g_object_unref(dt->vnc_subprocess);
}
if (dt->vnc_sock_path) {
unlink(dt->vnc_sock_path);
g_free(dt->vnc_sock_path);
}
if (dt->tmp_dir) {
rmdir(dt->tmp_dir);
g_free(dt->tmp_dir);
}
if (dt->qts) {
qtest_quit(dt->qts);
}
if (dt->bus) {
g_test_dbus_down(dt->bus);
g_object_unref(dt->bus);
}
g_free(dt->bus_addr);
}
static void
test_dbus_vnc_basic(void)
{
DbusTest dt = { 0 };
VncConnection *conn = NULL;
GMainLoop *loop = NULL;
int pair[2];
int vnc_fd;
guint timeout_id;
if (!g_getenv("QTEST_QEMU_VNC_BINARY")) {
g_test_skip("QTEST_QEMU_VNC_BINARY not set");
return;
}
dt.qts = qtest_init("-display dbus,p2p=yes -name dbus-vnc-test");
g_assert_cmpint(qemu_socketpair(AF_UNIX, SOCK_STREAM, 0, pair), ==, 0);
qtest_qmp_add_client(dt.qts, "@dbus-display", pair[1]);
close(pair[1]);
dt.tmp_dir = g_dir_make_tmp("dbus-vnc-test-XXXXXX", NULL);
g_assert(dt.tmp_dir != NULL);
dt.vnc_sock_path = g_build_filename(dt.tmp_dir, "vnc.sock", NULL);
dt.vnc_subprocess = spawn_qemu_vnc(pair[0], dt.vnc_sock_path);
vnc_fd = wait_for_vnc_socket(dt.vnc_sock_path, VNC_TEST_TIMEOUT_MS);
g_assert(vnc_fd >= 0);
loop = g_main_loop_new(NULL, FALSE);
conn = vnc_connection_new();
g_signal_connect(conn, "vnc-error",
G_CALLBACK(on_vnc_error), NULL);
g_signal_connect(conn, "vnc-auth-failure",
G_CALLBACK(on_vnc_auth_failure), NULL);
g_signal_connect(conn, "vnc-initialized",
G_CALLBACK(on_vnc_initialized), loop);
vnc_connection_set_auth_type(conn, VNC_CONNECTION_AUTH_NONE);
vnc_connection_open_fd(conn, vnc_fd);
timeout_id = g_timeout_add_seconds(10, timeout_cb, NULL);
g_main_loop_run(loop);
g_source_remove(timeout_id);
if (conn) {
vnc_connection_shutdown(conn);
g_object_unref(conn);
}
g_clear_pointer(&loop, g_main_loop_unref);
cleanup_dbus_test(&dt);
}
static void
test_dbus_vnc_server_props(void)
{
DbusTest dt = { 0 };
QemuVnc1Server *proxy = NULL;
g_autoptr(GError) err = NULL;
const gchar *const *clients;
GVariant *listeners;
if (!setup_dbus_test(&dt)) {
goto cleanup;
}
proxy = create_server_proxy(dt.bus_conn, &err);
g_assert_no_error(err);
g_assert_nonnull(proxy);
g_assert_cmpstr(qemu_vnc1_server_get_name(proxy), ==,
"dbus-vnc-test");
g_assert_cmpstr(qemu_vnc1_server_get_auth(proxy), ==,
"none");
g_assert_cmpstr(qemu_vnc1_server_get_vencrypt_sub_auth(proxy), ==,
"");
clients = qemu_vnc1_server_get_clients(proxy);
g_assert_nonnull(clients);
g_assert_cmpint(g_strv_length((gchar **)clients), ==, 0);
listeners = qemu_vnc1_server_get_listeners(proxy);
g_assert_nonnull(listeners);
g_assert_cmpint(g_variant_n_children(listeners), >, 0);
cleanup:
g_clear_object(&proxy);
cleanup_dbus_test(&dt);
}
static void
on_client_connected(QemuVnc1Server *proxy,
const gchar *client_path,
LifecycleData *data)
{
data->got_connected = TRUE;
data->client_path = g_strdup(client_path);
}
static void
on_client_initialized(QemuVnc1Server *proxy,
const gchar *client_path,
LifecycleData *data)
{
data->got_initialized = TRUE;
g_main_loop_quit(data->dt->loop);
}
static void
on_client_disconnected(QemuVnc1Server *proxy,
const gchar *client_path,
LifecycleData *data)
{
data->got_disconnected = TRUE;
g_main_loop_quit(data->dt->loop);
}
static void
test_dbus_vnc_client_lifecycle(void)
{
DbusTest dt = { 0 };
QemuVnc1Server *server_proxy = NULL;
QemuVnc1Client *client_proxy = NULL;
g_autoptr(GError) err = NULL;
LifecycleData ldata = { 0 };
int vnc_fd;
guint timeout_id;
if (!setup_dbus_test(&dt)) {
goto cleanup;
}
server_proxy = create_server_proxy(dt.bus_conn, &err);
g_assert_no_error(err);
ldata.dt = &dt;
ldata.server_proxy = server_proxy;
g_signal_connect(server_proxy, "client-connected",
G_CALLBACK(on_client_connected), &ldata);
g_signal_connect(server_proxy, "client-initialized",
G_CALLBACK(on_client_initialized), &ldata);
g_signal_connect(server_proxy, "client-disconnected",
G_CALLBACK(on_client_disconnected), &ldata);
vnc_fd = wait_for_vnc_socket(dt.vnc_sock_path, VNC_TEST_TIMEOUT_MS);
g_assert(vnc_fd >= 0);
ldata.conn = vnc_connection_new();
g_signal_connect(ldata.conn, "vnc-error",
G_CALLBACK(on_vnc_error), NULL);
g_signal_connect(ldata.conn, "vnc-auth-failure",
G_CALLBACK(on_vnc_auth_failure), NULL);
vnc_connection_set_auth_type(ldata.conn, VNC_CONNECTION_AUTH_NONE);
vnc_connection_open_fd(ldata.conn, vnc_fd);
/* wait for ClientInitialized */
dt.loop = g_main_loop_new(NULL, FALSE);
timeout_id = g_timeout_add_seconds(10, timeout_cb, NULL);
g_main_loop_run(dt.loop);
g_source_remove(timeout_id);
g_assert_true(ldata.got_connected);
g_assert_true(ldata.got_initialized);
g_assert_nonnull(ldata.client_path);
/* Check client properties while still connected */
client_proxy = qemu_vnc1_client_proxy_new_sync(
dt.bus_conn,
G_DBUS_PROXY_FLAGS_NONE,
"org.qemu.vnc",
ldata.client_path,
NULL, &err);
g_assert_no_error(err);
g_assert_cmpstr(qemu_vnc1_client_get_family(client_proxy), ==,
"unix");
g_assert_false(qemu_vnc1_client_get_web_socket(client_proxy));
g_assert_cmpstr(qemu_vnc1_client_get_x509_dname(client_proxy), ==,
"");
g_assert_cmpstr(qemu_vnc1_client_get_sasl_username(client_proxy),
==, "");
/* disconnect and wait for ClientDisconnected */
vnc_connection_shutdown(ldata.conn);
timeout_id = g_timeout_add_seconds(10, timeout_cb, NULL);
g_main_loop_run(dt.loop);
g_source_remove(timeout_id);
g_assert_true(ldata.got_disconnected);
g_object_unref(ldata.conn);
g_main_loop_unref(dt.loop);
dt.loop = NULL;
g_free(ldata.client_path);
cleanup:
g_clear_object(&server_proxy);
g_clear_object(&client_proxy);
cleanup_dbus_test(&dt);
}
static void
test_dbus_vnc_no_password(void)
{
DbusTest dt = { 0 };
QemuVnc1Server *proxy = NULL;
g_autoptr(GError) err = NULL;
gboolean ret;
if (!setup_dbus_test(&dt)) {
goto cleanup;
}
proxy = create_server_proxy(dt.bus_conn, &err);
g_assert_no_error(err);
/*
* With default auth=none, SetPassword should return an error
* because VNC password authentication is not enabled.
*/
ret = qemu_vnc1_server_call_set_password_sync(
proxy, "secret",
G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err);
g_assert_false(ret);
g_assert_error(err, G_DBUS_ERROR, G_DBUS_ERROR_FAILED);
g_clear_error(&err);
ret = qemu_vnc1_server_call_expire_password_sync(
proxy, "never",
G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err);
g_assert_no_error(err);
g_assert_true(ret);
ret = qemu_vnc1_server_call_expire_password_sync(
proxy, "+3600",
G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err);
g_assert_no_error(err);
g_assert_true(ret);
cleanup:
g_clear_object(&proxy);
cleanup_dbus_test(&dt);
}
typedef struct PasswordData {
DbusTest *dt;
VncConnection *conn;
const char *password;
gboolean auth_succeeded;
gboolean auth_failed;
} PasswordData;
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
static void
on_pw_vnc_auth_credential(VncConnection *conn, GValueArray *creds,
PasswordData *data)
{
for (guint i = 0; i < creds->n_values; i++) {
int type = g_value_get_enum(g_value_array_get_nth(creds, i));
if (type == VNC_CONNECTION_CREDENTIAL_PASSWORD) {
vnc_connection_set_credential(conn, type, data->password);
}
}
}
G_GNUC_END_IGNORE_DEPRECATIONS
static void
on_pw_vnc_initialized(VncConnection *conn, PasswordData *data)
{
data->auth_succeeded = TRUE;
g_main_loop_quit(data->dt->loop);
}
static void
on_pw_vnc_auth_failure(VncConnection *conn, const char *msg,
PasswordData *data)
{
data->auth_failed = TRUE;
g_main_loop_quit(data->dt->loop);
}
static void
on_pw_vnc_error(VncConnection *conn, const char *msg,
PasswordData *data)
{
data->auth_failed = TRUE;
g_main_loop_quit(data->dt->loop);
}
static void
test_dbus_vnc_password_auth(void)
{
DbusTest dt = { 0 };
QemuVnc1Server *proxy = NULL;
g_autoptr(GError) err = NULL;
PasswordData pdata = { 0 };
const char *extra_args[] = { "--password", NULL };
int vnc_fd;
guint timeout_id;
gboolean ret;
if (!setup_dbus_test_full(&dt, extra_args)) {
goto cleanup;
}
proxy = create_server_proxy(dt.bus_conn, &err);
g_assert_no_error(err);
g_assert_cmpstr(qemu_vnc1_server_get_auth(proxy), ==, "vnc");
ret = qemu_vnc1_server_call_set_password_sync(
proxy, "testpass123",
G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err);
g_assert_no_error(err);
g_assert_true(ret);
vnc_fd = wait_for_vnc_socket(dt.vnc_sock_path, VNC_TEST_TIMEOUT_MS);
g_assert(vnc_fd >= 0);
pdata.dt = &dt;
pdata.password = "testpass123";
pdata.conn = vnc_connection_new();
g_signal_connect(pdata.conn, "vnc-error",
G_CALLBACK(on_pw_vnc_error), &pdata);
g_signal_connect(pdata.conn, "vnc-auth-failure",
G_CALLBACK(on_pw_vnc_auth_failure), &pdata);
g_signal_connect(pdata.conn, "vnc-auth-credential",
G_CALLBACK(on_pw_vnc_auth_credential), &pdata);
g_signal_connect(pdata.conn, "vnc-initialized",
G_CALLBACK(on_pw_vnc_initialized), &pdata);
vnc_connection_set_auth_type(pdata.conn, VNC_CONNECTION_AUTH_VNC);
vnc_connection_open_fd(pdata.conn, vnc_fd);
dt.loop = g_main_loop_new(NULL, FALSE);
timeout_id = g_timeout_add_seconds(10, timeout_cb, NULL);
g_main_loop_run(dt.loop);
g_source_remove(timeout_id);
g_assert_true(pdata.auth_succeeded);
g_assert_false(pdata.auth_failed);
vnc_connection_shutdown(pdata.conn);
g_object_unref(pdata.conn);
g_main_loop_unref(dt.loop);
dt.loop = NULL;
cleanup:
g_clear_object(&proxy);
cleanup_dbus_test(&dt);
}
static void
test_dbus_vnc_sasl_authz_no_sasl(void)
{
const char *binary;
g_autoptr(GError) err = NULL;
g_autoptr(GSubprocess) proc = NULL;
gboolean ok;
binary = g_getenv("QTEST_QEMU_VNC_BINARY");
if (!binary) {
g_test_skip("QTEST_QEMU_VNC_BINARY not set");
return;
}
proc = g_subprocess_new(G_SUBPROCESS_FLAGS_STDERR_SILENCE,
&err,
binary,
"--sasl-authz", "authz0",
NULL);
g_assert_no_error(err);
g_assert_nonnull(proc);
ok = g_subprocess_wait(proc, NULL, &err);
g_assert_no_error(err);
g_assert_true(ok);
g_assert_false(g_subprocess_get_successful(proc));
}
#ifdef CONFIG_VNC_SASL
static void
test_dbus_vnc_sasl_server_props(void)
{
DbusTest dt = { 0 };
QemuVnc1Server *proxy = NULL;
g_autoptr(GError) err = NULL;
const char *extra_args[] = { "--sasl", NULL };
if (!setup_dbus_test_full(&dt, extra_args)) {
goto cleanup;
}
proxy = create_server_proxy(dt.bus_conn, &err);
g_assert_no_error(err);
g_assert_nonnull(proxy);
g_assert_cmpstr(qemu_vnc1_server_get_auth(proxy), ==, "sasl");
cleanup:
g_clear_object(&proxy);
cleanup_dbus_test(&dt);
}
#define SASL_TEST_USER "testuser"
#define SASL_TEST_PASS "testpass123"
typedef struct SaslAuthData {
DbusTest *dt;
const char *username;
const char *password;
gboolean auth_succeeded;
gboolean auth_failed;
} SaslAuthData;
typedef struct SaslTestData {
DbusTest dt;
SaslAuthData sdata;
char *sasl_dir;
char *db_path;
} SaslTestData;
G_GNUC_BEGIN_IGNORE_DEPRECATIONS
static void
on_sasl_vnc_auth_credential(VncConnection *conn, GValueArray *creds,
SaslAuthData *data)
{
for (guint i = 0; i < creds->n_values; i++) {
int type = g_value_get_enum(g_value_array_get_nth(creds, i));
switch (type) {
case VNC_CONNECTION_CREDENTIAL_USERNAME:
vnc_connection_set_credential(conn, type, data->username);
break;
case VNC_CONNECTION_CREDENTIAL_PASSWORD:
vnc_connection_set_credential(conn, type, data->password);
break;
}
}
}
G_GNUC_END_IGNORE_DEPRECATIONS
static void
on_sasl_vnc_initialized(VncConnection *conn, SaslAuthData *data)
{
data->auth_succeeded = TRUE;
g_main_loop_quit(data->dt->loop);
}
static void
on_sasl_vnc_auth_failure(VncConnection *conn, const char *msg,
SaslAuthData *data)
{
data->auth_failed = TRUE;
g_main_loop_quit(data->dt->loop);
}
static void
on_sasl_vnc_error(VncConnection *conn, const char *msg,
SaslAuthData *data)
{
data->auth_failed = TRUE;
g_main_loop_quit(data->dt->loop);
}
/*
* Create a SASL configuration directory with a qemu.conf and a
* sasldb2 user database. Returns the path to the sasldb file,
* or NULL if saslpasswd2 is not available.
*/
static char *
create_sasl_config(const char *dir, const char *username,
const char *password)
{
g_autofree char *conf_path = g_strdup_printf("%s/qemu.conf", dir);
g_autofree char *db_path = g_strdup_printf("%s/sasldb2", dir);
g_autoptr(GError) err = NULL;
g_autoptr(GSubprocess) proc = NULL;
g_autofree char *conf = NULL;
GOutputStream *stdin_stream;
gboolean ok;
/* use PLAIN, and local auxprop sasldb plugin */
conf = g_strdup_printf(
"mech_list: plain\n"
"pwcheck_method: auxprop\n"
"auxprop_plugin: sasldb\n"
"sasldb_path: %s\n", db_path);
g_assert_true(g_file_set_contents(conf_path, conf, -1, NULL));
proc = g_subprocess_new(
G_SUBPROCESS_FLAGS_STDIN_PIPE |
G_SUBPROCESS_FLAGS_STDOUT_SILENCE |
G_SUBPROCESS_FLAGS_STDERR_SILENCE,
&err,
"saslpasswd2", "-f", db_path, "-a", "qemu", "-p", "-c",
username, NULL);
if (!proc) {
return NULL;
}
stdin_stream = g_subprocess_get_stdin_pipe(proc);
g_output_stream_write_all(stdin_stream, password,
strlen(password), NULL, NULL, NULL);
g_output_stream_close(stdin_stream, NULL, NULL);
ok = g_subprocess_wait_check(proc, NULL, &err);
if (!ok) {
return NULL;
}
return g_strdup(db_path);
}
static void
cleanup_sasl_config(const char *dir, const char *db_path)
{
g_autofree char *conf = g_strdup_printf("%s/qemu.conf", dir);
unlink(conf);
if (db_path) {
unlink(db_path);
}
rmdir(dir);
}
/*
* Set up SASL environment: create temp config dir, sasldb, and
* start qemu-vnc with the given extra_args. Returns FALSE if the
* test should be skipped.
*/
static gboolean
setup_sasl_test(SaslTestData *st, const char **extra_args)
{
if (!g_getenv("QTEST_QEMU_VNC_BINARY")) {
g_test_skip("QTEST_QEMU_VNC_BINARY not set");
return FALSE;
}
st->sasl_dir = g_dir_make_tmp("dbus-vnc-sasl-XXXXXX", NULL);
g_assert_nonnull(st->sasl_dir);
st->db_path = create_sasl_config(st->sasl_dir, SASL_TEST_USER,
SASL_TEST_PASS);
if (!st->db_path) {
g_test_skip("saslpasswd2 not available or failed");
cleanup_sasl_config(st->sasl_dir, NULL);
return FALSE;
}
g_setenv("SASL_CONF_PATH", st->sasl_dir, TRUE);
if (!setup_dbus_test_full(&st->dt, extra_args)) {
return FALSE;
}
return TRUE;
}
/*
* Connect to the VNC server using SASL and run the main loop
* until authentication completes or times out.
*/
static void
run_sasl_auth(SaslTestData *st, const char *username,
const char *password)
{
VncConnection *conn;
guint timeout_id;
int vnc_fd;
st->sdata.dt = &st->dt;
st->sdata.username = username;
st->sdata.password = password;
vnc_fd = wait_for_vnc_socket(st->dt.vnc_sock_path, VNC_TEST_TIMEOUT_MS);
g_assert(vnc_fd >= 0);
conn = vnc_connection_new();
g_signal_connect(conn, "vnc-error",
G_CALLBACK(on_sasl_vnc_error), &st->sdata);
g_signal_connect(conn, "vnc-auth-failure",
G_CALLBACK(on_sasl_vnc_auth_failure), &st->sdata);
g_signal_connect(conn, "vnc-auth-credential",
G_CALLBACK(on_sasl_vnc_auth_credential),
&st->sdata);
g_signal_connect(conn, "vnc-initialized",
G_CALLBACK(on_sasl_vnc_initialized), &st->sdata);
vnc_connection_set_auth_type(conn, VNC_CONNECTION_AUTH_SASL);
vnc_connection_open_fd(conn, vnc_fd);
st->dt.loop = g_main_loop_new(NULL, FALSE);
timeout_id = g_timeout_add_seconds(10, timeout_cb, NULL);
g_main_loop_run(st->dt.loop);
g_source_remove(timeout_id);
g_signal_handlers_disconnect_by_data(conn, &st->sdata);
vnc_connection_shutdown(conn);
g_object_unref(conn);
g_main_loop_unref(st->dt.loop);
st->dt.loop = NULL;
}
static void
cleanup_sasl_test(SaslTestData *st)
{
cleanup_dbus_test(&st->dt);
g_unsetenv("SASL_CONF_PATH");
cleanup_sasl_config(st->sasl_dir, st->db_path);
g_free(st->sasl_dir);
g_free(st->db_path);
}
static void
test_dbus_vnc_sasl_auth(void)
{
SaslTestData st = { 0 };
const char *extra_args[] = { "--sasl", NULL };
if (!setup_sasl_test(&st, extra_args)) {
return;
}
run_sasl_auth(&st, SASL_TEST_USER, SASL_TEST_PASS);
g_assert_true(st.sdata.auth_succeeded);
g_assert_false(st.sdata.auth_failed);
cleanup_sasl_test(&st);
}
static void
test_dbus_vnc_sasl_auth_bad_password(void)
{
SaslTestData st = { 0 };
const char *extra_args[] = { "--sasl", NULL };
if (!setup_sasl_test(&st, extra_args)) {
return;
}
run_sasl_auth(&st, SASL_TEST_USER, "wrongpassword");
g_assert_false(st.sdata.auth_succeeded);
g_assert_true(st.sdata.auth_failed);
cleanup_sasl_test(&st);
}
static void
test_dbus_vnc_sasl_authz_denied(void)
{
SaslTestData st = { 0 };
const char *extra_args[] = {
"--sasl",
"--object",
"authz-simple,id=authz0,identity=otheruser",
"--sasl-authz", "authz0",
NULL
};
if (!setup_sasl_test(&st, extra_args)) {
return;
}
run_sasl_auth(&st, SASL_TEST_USER, SASL_TEST_PASS);
g_assert_false(st.sdata.auth_succeeded);
g_assert_true(st.sdata.auth_failed);
cleanup_sasl_test(&st);
}
#endif /* CONFIG_VNC_SASL */
static void
test_dbus_vnc_tls_authz_no_creds(void)
{
const char *binary;
g_autoptr(GError) err = NULL;
g_autoptr(GSubprocess) proc = NULL;
gboolean ok;
binary = g_getenv("QTEST_QEMU_VNC_BINARY");
if (!binary) {
g_test_skip("QTEST_QEMU_VNC_BINARY not set");
return;
}
proc = g_subprocess_new(G_SUBPROCESS_FLAGS_STDERR_SILENCE,
&err,
binary,
"--tls-authz", "authz0",
NULL);
g_assert_no_error(err);
g_assert_nonnull(proc);
ok = g_subprocess_wait(proc, NULL, &err);
g_assert_no_error(err);
g_assert_true(ok);
g_assert_false(g_subprocess_get_successful(proc));
}
#ifdef CONFIG_TASN1
#define CLIENT_CERT_CN "qemu-vnc-test"
static char *
create_tls_certs(const char *dir)
{
char *keyfile = g_strdup_printf("%s/key.pem", dir);
char *cacert = g_strdup_printf("%s/ca-cert.pem", dir);
char *servercert = g_strdup_printf("%s/server-cert.pem", dir);
char *serverkey = g_strdup_printf("%s/server-key.pem", dir);
char *clientcert = g_strdup_printf("%s/client-cert.pem", dir);
test_tls_init(keyfile);
g_assert(link(keyfile, serverkey) == 0);
TLS_ROOT_REQ_SIMPLE(cacertreq, cacert);
TLS_CERT_REQ_SIMPLE_SERVER(servercertreq, cacertreq,
servercert, "localhost", NULL);
TLS_CERT_REQ_SIMPLE_CLIENT(clientcertreq, cacertreq,
CLIENT_CERT_CN, clientcert);
test_tls_deinit_cert(&clientcertreq);
test_tls_deinit_cert(&servercertreq);
test_tls_deinit_cert(&cacertreq);
g_free(cacert);
g_free(servercert);
g_free(serverkey);
g_free(clientcert);
return keyfile;
}
static void
cleanup_tls_certs(const char *dir, const char *keyfile)
{
g_autofree char *cacert = g_strdup_printf("%s/ca-cert.pem", dir);
g_autofree char *servercert = g_strdup_printf("%s/server-cert.pem", dir);
g_autofree char *serverkey = g_strdup_printf("%s/server-key.pem", dir);
g_autofree char *clientcert = g_strdup_printf("%s/client-cert.pem", dir);
unlink(cacert);
unlink(servercert);
unlink(serverkey);
unlink(clientcert);
unlink(keyfile);
test_tls_cleanup(keyfile);
rmdir(dir);
}
/*
* Do a minimal VNC/VeNCrypt negotiation on @fd up to the point where
* the TLS handshake should begin, then perform a GnuTLS handshake
* using the given credentials.
*/
static bool
try_raw_tls_connect(int fd, gnutls_certificate_credentials_t cred)
{
char buf[13];
uint8_t num_types, type;
uint8_t vencrypt_ver[2], ack;
uint8_t num_sub;
uint32_t subtype;
gnutls_session_t session;
int ret;
bool success;
/* RFB version exchange */
g_assert_cmpint(read(fd, buf, 12), ==, 12);
g_assert_cmpint(write(fd, "RFB 003.008\n", 12), ==, 12);
/* Select VeNCrypt (type 19) from the auth list */
g_assert_cmpint(read(fd, &num_types, 1), ==, 1);
for (int i = 0; i < num_types; i++) {
g_assert_cmpint(read(fd, &type, 1), ==, 1);
}
type = 19;
g_assert_cmpint(write(fd, &type, 1), ==, 1);
/* VeNCrypt version exchange */
g_assert_cmpint(read(fd, vencrypt_ver, 2), ==, 2);
g_assert_cmpint(write(fd, vencrypt_ver, 2), ==, 2);
g_assert_cmpint(read(fd, &ack, 1), ==, 1);
g_assert_cmpint(ack, ==, 0);
/* Select x509-none (260) sub-auth */
g_assert_cmpint(read(fd, &num_sub, 1), ==, 1);
for (int i = 0; i < num_sub; i++) {
g_assert_cmpint(read(fd, &subtype, 4), ==, 4);
}
subtype = htonl(260);
g_assert_cmpint(write(fd, &subtype, 4), ==, 4);
/* Server sends 1-byte ack (1 = accepted) before TLS starts */
g_assert_cmpint(read(fd, &ack, 1), ==, 1);
g_assert_cmpint(ack, ==, 1);
/* TLS handshake */
g_assert_cmpint(gnutls_init(&session, GNUTLS_CLIENT), >=, 0);
g_assert_cmpint(
gnutls_set_default_priority(session), >=, 0);
g_assert_cmpint(
gnutls_credentials_set(session, GNUTLS_CRD_CERTIFICATE, cred),
>=, 0);
gnutls_transport_set_int(session, fd);
do {
ret = gnutls_handshake(session);
} while (ret == GNUTLS_E_AGAIN || ret == GNUTLS_E_INTERRUPTED);
if (ret < 0) {
success = false;
} else {
/*
* Try reading the VNC security-result (4 bytes) — if the
* server rejected us it will have closed the connection.
*/
char tmp[4];
do {
ret = gnutls_record_recv(session, tmp, sizeof(tmp));
} while (ret == GNUTLS_E_AGAIN || ret == GNUTLS_E_INTERRUPTED);
success = (ret > 0);
}
gnutls_deinit(session);
return success;
}
static void
test_dbus_vnc_tls_server_props(void)
{
DbusTest dt = { 0 };
QemuVnc1Server *proxy = NULL;
g_autoptr(GError) err = NULL;
g_autofree char *tls_dir = NULL;
g_autofree char *keyfile = NULL;
if (!g_getenv("QTEST_QEMU_VNC_BINARY")) {
g_test_skip("QTEST_QEMU_VNC_BINARY not set");
return;
}
tls_dir = g_dir_make_tmp("dbus-vnc-tls-XXXXXX", NULL);
g_assert_nonnull(tls_dir);
keyfile = create_tls_certs(tls_dir);
{
const char *extra_args[] = {
"--tls-creds", tls_dir, NULL
};
if (!setup_dbus_test_full(&dt, extra_args)) {
goto cleanup;
}
}
proxy = create_server_proxy(dt.bus_conn, &err);
g_assert_no_error(err);
g_assert_nonnull(proxy);
g_assert_cmpstr(qemu_vnc1_server_get_auth(proxy), ==, "vencrypt");
g_assert_cmpstr(qemu_vnc1_server_get_vencrypt_sub_auth(proxy), ==,
"x509-none");
/*
* With verify-peer=no, a client without a certificate should
* be able to connect successfully through TLS.
*/
{
g_autofree char *ca_path =
g_strdup_printf("%s/ca-cert.pem", tls_dir);
gnutls_certificate_credentials_t cred;
int fd;
g_assert_cmpint(
gnutls_certificate_allocate_credentials(&cred), >=, 0);
g_assert_cmpint(
gnutls_certificate_set_x509_trust_file(
cred, ca_path, GNUTLS_X509_FMT_PEM), >=, 0);
fd = wait_for_vnc_socket(dt.vnc_sock_path, VNC_TEST_TIMEOUT_MS);
g_assert(fd >= 0);
g_assert_true(try_raw_tls_connect(fd, cred));
close(fd);
gnutls_certificate_free_credentials(cred);
}
cleanup:
g_clear_object(&proxy);
cleanup_dbus_test(&dt);
cleanup_tls_certs(tls_dir, keyfile);
}
static void
test_dbus_vnc_tls_authz(void)
{
DbusTest dt = { 0 };
g_autofree char *tls_dir = NULL;
g_autofree char *keyfile = NULL;
g_autofree char *ca_path = NULL;
if (!g_getenv("QTEST_QEMU_VNC_BINARY")) {
g_test_skip("QTEST_QEMU_VNC_BINARY not set");
return;
}
tls_dir = g_dir_make_tmp("dbus-vnc-tls-XXXXXX", NULL);
g_assert_nonnull(tls_dir);
keyfile = create_tls_certs(tls_dir);
/*
* The client cert has CN=qemu-vnc-test, so the DN string
* reported by GnuTLS is "CN=qemu-vnc-test". Configure
* authz-simple to accept exactly that identity.
*/
{
g_autofree char *identity =
g_strdup_printf("CN=%s", CLIENT_CERT_CN);
const char *extra_args[] = {
"--tls-creds", tls_dir,
"--object",
NULL, /* filled below */
"--tls-authz", "authz0",
NULL
};
g_autofree char *object_arg =
g_strdup_printf("authz-simple,id=authz0,identity=%s", identity);
extra_args[3] = object_arg;
if (!setup_dbus_test_full(&dt, extra_args)) {
goto cleanup;
}
}
ca_path = g_strdup_printf("%s/ca-cert.pem", tls_dir);
/*
* Connect without a client certificate.
* With verify-peer=yes the TLS handshake must fail.
*/
{
gnutls_certificate_credentials_t cred;
int fd;
g_assert_cmpint(
gnutls_certificate_allocate_credentials(&cred), >=, 0);
g_assert_cmpint(
gnutls_certificate_set_x509_trust_file(
cred, ca_path, GNUTLS_X509_FMT_PEM), >=, 0);
fd = wait_for_vnc_socket(dt.vnc_sock_path, VNC_TEST_TIMEOUT_MS);
g_assert(fd >= 0);
g_assert_false(try_raw_tls_connect(fd, cred));
close(fd);
gnutls_certificate_free_credentials(cred);
}
/*
* Connect with a valid client certificate whose DN
* matches the authz-simple identity. This must succeed.
*/
{
g_autofree char *cert_path =
g_strdup_printf("%s/client-cert.pem", tls_dir);
g_autofree char *key_path =
g_strdup_printf("%s/key.pem", tls_dir);
gnutls_certificate_credentials_t cred;
int fd;
g_assert_cmpint(
gnutls_certificate_allocate_credentials(&cred), >=, 0);
g_assert_cmpint(
gnutls_certificate_set_x509_trust_file(
cred, ca_path, GNUTLS_X509_FMT_PEM), >=, 0);
g_assert_cmpint(
gnutls_certificate_set_x509_key_file(
cred, cert_path, key_path, GNUTLS_X509_FMT_PEM), >=, 0);
fd = wait_for_vnc_socket(dt.vnc_sock_path, VNC_TEST_TIMEOUT_MS);
g_assert(fd >= 0);
g_assert_true(try_raw_tls_connect(fd, cred));
close(fd);
gnutls_certificate_free_credentials(cred);
}
cleanup:
cleanup_dbus_test(&dt);
cleanup_tls_certs(tls_dir, keyfile);
}
#endif /* CONFIG_TASN1 */
int
main(int argc, char **argv)
{
g_log_set_always_fatal(G_LOG_LEVEL_WARNING | G_LOG_LEVEL_CRITICAL);
/*
* Flaky test:
* ERROR:../tests/qtest/dbus-vnc-test.c:621:test_dbus_vnc_password_auth:
* assertion failed (qemu_vnc1_server_get_auth(proxy) == "vnc"): ("none" ==
* "vnc")
*/
if (!getenv("QEMU_TEST_FLAKY_TESTS")) {
g_test_skip("QEMU_TEST_FLAKY_TESTS not set");
return 0;
}
if (getenv("GTK_VNC_DEBUG")) {
vnc_util_set_debug(true);
}
g_test_init(&argc, &argv, NULL);
qtest_add_func("/dbus-vnc/basic", test_dbus_vnc_basic);
qtest_add_func("/dbus-vnc/server-props", test_dbus_vnc_server_props);
qtest_add_func("/dbus-vnc/client-lifecycle",
test_dbus_vnc_client_lifecycle);
qtest_add_func("/dbus-vnc/no-password", test_dbus_vnc_no_password);
qtest_add_func("/dbus-vnc/password-auth", test_dbus_vnc_password_auth);
qtest_add_func("/dbus-vnc/sasl-authz-no-sasl",
test_dbus_vnc_sasl_authz_no_sasl);
#ifdef CONFIG_VNC_SASL
qtest_add_func("/dbus-vnc/sasl-server-props",
test_dbus_vnc_sasl_server_props);
qtest_add_func("/dbus-vnc/sasl-auth",
test_dbus_vnc_sasl_auth);
qtest_add_func("/dbus-vnc/sasl-auth-bad-password",
test_dbus_vnc_sasl_auth_bad_password);
qtest_add_func("/dbus-vnc/sasl-authz-denied",
test_dbus_vnc_sasl_authz_denied);
#endif
qtest_add_func("/dbus-vnc/tls-authz-no-creds",
test_dbus_vnc_tls_authz_no_creds);
#ifdef CONFIG_TASN1
qtest_add_func("/dbus-vnc/tls-server-props",
test_dbus_vnc_tls_server_props);
qtest_add_func("/dbus-vnc/tls-authz",
test_dbus_vnc_tls_authz);
#endif
return g_test_run();
}