Import QEMU upstream snapshot d2e570c

Upstream: https://gitlab.com/qemu-project/qemu.git

Upstream-Commit: d2e570cc0f97b936902a5b1b86b73c0f5998b475
This commit is contained in:
2026-08-31 02:15:30 +02:00
commit cf256aa081
11315 changed files with 3598369 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
SKELETONS = rss.bpf.skeleton.h
LLVM_STRIP ?= llvm-strip
CLANG ?= clang
INC_FLAGS = `$(CLANG) -print-file-name=include`
EXTRA_CFLAGS ?= -O2 -g -target bpf
all: $(SKELETONS)
.PHONY: clean
clean:
rm -f $(SKELETONS) $(SKELETONS:%.skeleton.h=%.o)
%.o: %.c
$(CLANG) $(INC_FLAGS) \
-D__KERNEL__ -D__ASM_SYSREG_H \
-I../include $(LINUXINCLUDE) \
$(EXTRA_CFLAGS) -c $< -o $@
$(LLVM_STRIP) -g $@
%.skeleton.h: %.o
bpftool gen skeleton $< > $@
cp $@ ../../ebpf/
+569
View File
@@ -0,0 +1,569 @@
/*
* eBPF RSS program
*
* Developed by Daynix Computing LTD (http://www.daynix.com)
*
* Authors:
* Andrew Melnychenko <[email protected]>
* Yuri Benditovich <[email protected]>
*
* This work is licensed under the terms of the GNU GPL, version 2. See
* the COPYING file in the top-level directory.
*
* Prepare:
* Requires llvm, clang, bpftool, linux kernel tree
*
* Build rss.bpf.skeleton.h:
* make -f Makefile.ebpf clean all
*/
#include <stddef.h>
#include <stdbool.h>
#include <linux/bpf.h>
#include <linux/in.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/ipv6.h>
#include <linux/udp.h>
#include <linux/tcp.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#include <linux/virtio_net.h>
#define INDIRECTION_TABLE_SIZE 128
#define HASH_CALCULATION_BUFFER_SIZE 36
struct rss_config_t {
__u8 redirect;
__u8 populate_hash;
__u32 hash_types;
__u16 indirections_len;
__u16 default_queue;
} __attribute__((packed));
struct toeplitz_key_data_t {
__u32 leftmost_32_bits;
__u8 next_byte[HASH_CALCULATION_BUFFER_SIZE];
};
struct packet_hash_info_t {
__u8 is_ipv4;
__u8 is_ipv6;
__u8 is_udp;
__u8 is_tcp;
__u8 is_ipv6_ext_src;
__u8 is_ipv6_ext_dst;
__u8 is_fragmented;
__u16 src_port;
__u16 dst_port;
union {
struct {
__be32 in_src;
__be32 in_dst;
};
struct {
struct in6_addr in6_src;
struct in6_addr in6_dst;
struct in6_addr in6_ext_src;
struct in6_addr in6_ext_dst;
};
};
};
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(key_size, sizeof(__u32));
__uint(value_size, sizeof(struct rss_config_t));
__uint(max_entries, 1);
__uint(map_flags, BPF_F_MMAPABLE);
} tap_rss_map_configurations SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(key_size, sizeof(__u32));
__uint(value_size, sizeof(struct toeplitz_key_data_t));
__uint(max_entries, 1);
__uint(map_flags, BPF_F_MMAPABLE);
} tap_rss_map_toeplitz_key SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(key_size, sizeof(__u32));
__uint(value_size, sizeof(__u16));
__uint(max_entries, INDIRECTION_TABLE_SIZE);
__uint(map_flags, BPF_F_MMAPABLE);
} tap_rss_map_indirection_table SEC(".maps");
static inline void net_rx_rss_add_chunk(__u8 *rss_input, size_t *bytes_written,
const void *ptr, size_t size) {
__builtin_memcpy(&rss_input[*bytes_written], ptr, size);
*bytes_written += size;
}
static inline
void net_toeplitz_add(__u32 *result,
__u8 *input,
__u32 len
, struct toeplitz_key_data_t *key) {
__u32 accumulator = *result;
__u32 leftmost_32_bits = key->leftmost_32_bits;
__u32 byte;
for (byte = 0; byte < HASH_CALCULATION_BUFFER_SIZE; byte++) {
__u8 input_byte = input[byte];
__u8 key_byte = key->next_byte[byte];
__u8 bit;
for (bit = 0; bit < 8; bit++) {
if (input_byte & (1 << 7)) {
accumulator ^= leftmost_32_bits;
}
leftmost_32_bits =
(leftmost_32_bits << 1) | ((key_byte & (1 << 7)) >> 7);
input_byte <<= 1;
key_byte <<= 1;
}
}
*result = accumulator;
}
static inline int ip6_extension_header_type(__u8 hdr_type)
{
switch (hdr_type) {
case IPPROTO_HOPOPTS:
case IPPROTO_ROUTING:
case IPPROTO_FRAGMENT:
case IPPROTO_ICMPV6:
case IPPROTO_NONE:
case IPPROTO_DSTOPTS:
case IPPROTO_MH:
return 1;
default:
return 0;
}
}
/*
* According to
* https://www.iana.org/assignments/ipv6-parameters/ipv6-parameters.xhtml
* we expect that there are would be no more than 11 extensions in IPv6 header,
* also there is 27 TLV options for Destination and Hop-by-hop extensions.
* Need to choose reasonable amount of maximum extensions/options we may
* check to find ext src/dst.
*/
#define IP6_EXTENSIONS_COUNT 11
#define IP6_OPTIONS_COUNT 30
static inline int parse_ipv6_ext(struct __sk_buff *skb,
struct packet_hash_info_t *info,
__u8 *l4_protocol, size_t *l4_offset)
{
int err = 0;
if (!ip6_extension_header_type(*l4_protocol)) {
return 0;
}
struct ipv6_opt_hdr ext_hdr = {};
for (unsigned int i = 0; i < IP6_EXTENSIONS_COUNT; ++i) {
err = bpf_skb_load_bytes_relative(skb, *l4_offset, &ext_hdr,
sizeof(ext_hdr), BPF_HDR_START_NET);
if (err) {
goto error;
}
if (*l4_protocol == IPPROTO_ROUTING) {
struct ipv6_rt_hdr ext_rt = {};
err = bpf_skb_load_bytes_relative(skb, *l4_offset, &ext_rt,
sizeof(ext_rt), BPF_HDR_START_NET);
if (err) {
goto error;
}
if ((ext_rt.type == IPV6_SRCRT_TYPE_2) &&
(ext_rt.hdrlen == sizeof(struct in6_addr) / 8) &&
(ext_rt.segments_left == 1)) {
err = bpf_skb_load_bytes_relative(skb,
*l4_offset + offsetof(struct rt2_hdr, addr),
&info->in6_ext_dst, sizeof(info->in6_ext_dst),
BPF_HDR_START_NET);
if (err) {
goto error;
}
info->is_ipv6_ext_dst = 1;
}
} else if (*l4_protocol == IPPROTO_DSTOPTS) {
struct ipv6_opt_t {
__u8 type;
__u8 length;
} __attribute__((packed)) opt = {};
size_t opt_offset = sizeof(ext_hdr);
for (unsigned int j = 0; j < IP6_OPTIONS_COUNT; ++j) {
err = bpf_skb_load_bytes_relative(skb, *l4_offset + opt_offset,
&opt, sizeof(opt), BPF_HDR_START_NET);
if (err) {
goto error;
}
if (opt.type == IPV6_TLV_HAO) {
err = bpf_skb_load_bytes_relative(skb,
*l4_offset + opt_offset
+ offsetof(struct ipv6_destopt_hao, addr),
&info->in6_ext_src, sizeof(info->in6_ext_src),
BPF_HDR_START_NET);
if (err) {
goto error;
}
info->is_ipv6_ext_src = 1;
break;
}
opt_offset += (opt.type == IPV6_TLV_PAD1) ?
1 : opt.length + sizeof(opt);
if (opt_offset + 1 >= ext_hdr.hdrlen * 8) {
break;
}
}
} else if (*l4_protocol == IPPROTO_FRAGMENT) {
info->is_fragmented = true;
}
*l4_protocol = ext_hdr.nexthdr;
*l4_offset += (ext_hdr.hdrlen + 1) * 8;
if (!ip6_extension_header_type(ext_hdr.nexthdr)) {
return 0;
}
}
return 0;
error:
return err;
}
static __be16 parse_eth_type(struct __sk_buff *skb)
{
unsigned int offset = 12;
__be16 ret = 0;
int err = 0;
err = bpf_skb_load_bytes_relative(skb, offset, &ret, sizeof(ret),
BPF_HDR_START_MAC);
if (err) {
return 0;
}
switch (bpf_ntohs(ret)) {
case ETH_P_8021AD:
offset += 4;
case ETH_P_8021Q:
offset += 4;
err = bpf_skb_load_bytes_relative(skb, offset, &ret, sizeof(ret),
BPF_HDR_START_MAC);
default:
break;
}
if (err) {
return 0;
}
return ret;
}
static inline int parse_packet(struct __sk_buff *skb,
struct packet_hash_info_t *info)
{
int err = 0;
if (!info || !skb) {
return -1;
}
size_t l4_offset = 0;
__u8 l4_protocol = 0;
__u16 l3_protocol = bpf_ntohs(parse_eth_type(skb));
if (l3_protocol == 0) {
err = -1;
goto error;
}
if (l3_protocol == ETH_P_IP) {
info->is_ipv4 = 1;
struct iphdr ip = {};
err = bpf_skb_load_bytes_relative(skb, 0, &ip, sizeof(ip),
BPF_HDR_START_NET);
if (err) {
goto error;
}
info->in_src = ip.saddr;
info->in_dst = ip.daddr;
info->is_fragmented = !!(bpf_ntohs(ip.frag_off) & (0x2000 | 0x1fff));
l4_protocol = ip.protocol;
l4_offset = ip.ihl * 4;
} else if (l3_protocol == ETH_P_IPV6) {
info->is_ipv6 = 1;
struct ipv6hdr ip6 = {};
err = bpf_skb_load_bytes_relative(skb, 0, &ip6, sizeof(ip6),
BPF_HDR_START_NET);
if (err) {
goto error;
}
info->in6_src = ip6.saddr;
info->in6_dst = ip6.daddr;
l4_protocol = ip6.nexthdr;
l4_offset = sizeof(ip6);
err = parse_ipv6_ext(skb, info, &l4_protocol, &l4_offset);
if (err) {
goto error;
}
}
if (l4_protocol != 0 && !info->is_fragmented) {
if (l4_protocol == IPPROTO_TCP) {
info->is_tcp = 1;
struct tcphdr tcp = {};
err = bpf_skb_load_bytes_relative(skb, l4_offset, &tcp, sizeof(tcp),
BPF_HDR_START_NET);
if (err) {
goto error;
}
info->src_port = tcp.source;
info->dst_port = tcp.dest;
} else if (l4_protocol == IPPROTO_UDP) { /* TODO: add udplite? */
info->is_udp = 1;
struct udphdr udp = {};
err = bpf_skb_load_bytes_relative(skb, l4_offset, &udp, sizeof(udp),
BPF_HDR_START_NET);
if (err) {
goto error;
}
info->src_port = udp.source;
info->dst_port = udp.dest;
}
}
return 0;
error:
return err;
}
static inline bool calculate_rss_hash(struct __sk_buff *skb,
struct rss_config_t *config,
struct toeplitz_key_data_t *toe,
__u32 *result)
{
__u8 rss_input[HASH_CALCULATION_BUFFER_SIZE] = {};
size_t bytes_written = 0;
int err = 0;
struct packet_hash_info_t packet_info = {};
err = parse_packet(skb, &packet_info);
if (err) {
return false;
}
if (packet_info.is_ipv4) {
if (packet_info.is_tcp &&
config->hash_types & VIRTIO_NET_RSS_HASH_TYPE_TCPv4) {
net_rx_rss_add_chunk(rss_input, &bytes_written,
&packet_info.in_src,
sizeof(packet_info.in_src));
net_rx_rss_add_chunk(rss_input, &bytes_written,
&packet_info.in_dst,
sizeof(packet_info.in_dst));
net_rx_rss_add_chunk(rss_input, &bytes_written,
&packet_info.src_port,
sizeof(packet_info.src_port));
net_rx_rss_add_chunk(rss_input, &bytes_written,
&packet_info.dst_port,
sizeof(packet_info.dst_port));
} else if (packet_info.is_udp &&
config->hash_types & VIRTIO_NET_RSS_HASH_TYPE_UDPv4) {
net_rx_rss_add_chunk(rss_input, &bytes_written,
&packet_info.in_src,
sizeof(packet_info.in_src));
net_rx_rss_add_chunk(rss_input, &bytes_written,
&packet_info.in_dst,
sizeof(packet_info.in_dst));
net_rx_rss_add_chunk(rss_input, &bytes_written,
&packet_info.src_port,
sizeof(packet_info.src_port));
net_rx_rss_add_chunk(rss_input, &bytes_written,
&packet_info.dst_port,
sizeof(packet_info.dst_port));
} else if (config->hash_types & VIRTIO_NET_RSS_HASH_TYPE_IPv4) {
net_rx_rss_add_chunk(rss_input, &bytes_written,
&packet_info.in_src,
sizeof(packet_info.in_src));
net_rx_rss_add_chunk(rss_input, &bytes_written,
&packet_info.in_dst,
sizeof(packet_info.in_dst));
}
} else if (packet_info.is_ipv6) {
if (packet_info.is_tcp &&
config->hash_types & VIRTIO_NET_RSS_HASH_TYPE_TCPv6) {
if (packet_info.is_ipv6_ext_src &&
config->hash_types & VIRTIO_NET_RSS_HASH_TYPE_TCP_EX) {
net_rx_rss_add_chunk(rss_input, &bytes_written,
&packet_info.in6_ext_src,
sizeof(packet_info.in6_ext_src));
} else {
net_rx_rss_add_chunk(rss_input, &bytes_written,
&packet_info.in6_src,
sizeof(packet_info.in6_src));
}
if (packet_info.is_ipv6_ext_dst &&
config->hash_types & VIRTIO_NET_RSS_HASH_TYPE_TCP_EX) {
net_rx_rss_add_chunk(rss_input, &bytes_written,
&packet_info.in6_ext_dst,
sizeof(packet_info.in6_ext_dst));
} else {
net_rx_rss_add_chunk(rss_input, &bytes_written,
&packet_info.in6_dst,
sizeof(packet_info.in6_dst));
}
net_rx_rss_add_chunk(rss_input, &bytes_written,
&packet_info.src_port,
sizeof(packet_info.src_port));
net_rx_rss_add_chunk(rss_input, &bytes_written,
&packet_info.dst_port,
sizeof(packet_info.dst_port));
} else if (packet_info.is_udp &&
config->hash_types & VIRTIO_NET_RSS_HASH_TYPE_UDPv6) {
if (packet_info.is_ipv6_ext_src &&
config->hash_types & VIRTIO_NET_RSS_HASH_TYPE_UDP_EX) {
net_rx_rss_add_chunk(rss_input, &bytes_written,
&packet_info.in6_ext_src,
sizeof(packet_info.in6_ext_src));
} else {
net_rx_rss_add_chunk(rss_input, &bytes_written,
&packet_info.in6_src,
sizeof(packet_info.in6_src));
}
if (packet_info.is_ipv6_ext_dst &&
config->hash_types & VIRTIO_NET_RSS_HASH_TYPE_UDP_EX) {
net_rx_rss_add_chunk(rss_input, &bytes_written,
&packet_info.in6_ext_dst,
sizeof(packet_info.in6_ext_dst));
} else {
net_rx_rss_add_chunk(rss_input, &bytes_written,
&packet_info.in6_dst,
sizeof(packet_info.in6_dst));
}
net_rx_rss_add_chunk(rss_input, &bytes_written,
&packet_info.src_port,
sizeof(packet_info.src_port));
net_rx_rss_add_chunk(rss_input, &bytes_written,
&packet_info.dst_port,
sizeof(packet_info.dst_port));
} else if (config->hash_types & VIRTIO_NET_RSS_HASH_TYPE_IPv6) {
if (packet_info.is_ipv6_ext_src &&
config->hash_types & VIRTIO_NET_RSS_HASH_TYPE_IP_EX) {
net_rx_rss_add_chunk(rss_input, &bytes_written,
&packet_info.in6_ext_src,
sizeof(packet_info.in6_ext_src));
} else {
net_rx_rss_add_chunk(rss_input, &bytes_written,
&packet_info.in6_src,
sizeof(packet_info.in6_src));
}
if (packet_info.is_ipv6_ext_dst &&
config->hash_types & VIRTIO_NET_RSS_HASH_TYPE_IP_EX) {
net_rx_rss_add_chunk(rss_input, &bytes_written,
&packet_info.in6_ext_dst,
sizeof(packet_info.in6_ext_dst));
} else {
net_rx_rss_add_chunk(rss_input, &bytes_written,
&packet_info.in6_dst,
sizeof(packet_info.in6_dst));
}
}
}
if (!bytes_written) {
return false;
}
net_toeplitz_add(result, rss_input, bytes_written, toe);
return true;
}
SEC("socket")
int tun_rss_steering_prog(struct __sk_buff *skb)
{
struct rss_config_t *config;
struct toeplitz_key_data_t *toe;
__u32 key = 0;
__u32 hash = 0;
config = bpf_map_lookup_elem(&tap_rss_map_configurations, &key);
toe = bpf_map_lookup_elem(&tap_rss_map_toeplitz_key, &key);
if (!config || !toe) {
return 0;
}
if (config->redirect && calculate_rss_hash(skb, config, toe, &hash)) {
__u32 table_idx = hash % config->indirections_len;
__u16 *queue = 0;
queue = bpf_map_lookup_elem(&tap_rss_map_indirection_table,
&table_idx);
if (queue) {
return *queue;
}
}
return config->default_queue;
}
char _license[] SEC("license") = "GPL v2";
+538
View File
@@ -0,0 +1,538 @@
/*
* Privileged RAPL MSR helper commands for QEMU
*
* Copyright (C) 2024 Red Hat, Inc. <[email protected]>
*
* Author: Anthony Harivel <[email protected]>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; under version 2 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
#include "qemu/osdep.h"
#include <getopt.h>
#include <sys/ioctl.h>
#ifdef CONFIG_LIBCAP_NG
#include <cap-ng.h>
#endif
#include <pwd.h>
#include <grp.h>
#include "qemu/help-texts.h"
#include "qapi/error.h"
#include "qemu/cutils.h"
#include "qemu/main-loop.h"
#include "qemu/module.h"
#include "qemu/error-report.h"
#include "qemu/config-file.h"
#include "qemu-version.h"
#include "qapi/error.h"
#include "qemu/error-report.h"
#include "qemu/log.h"
#include "qemu/systemd.h"
#include "io/channel.h"
#include "io/channel-socket.h"
#include "trace/control.h"
#include "qemu-version.h"
#include "rapl-msr-index.h"
#define MSR_PATH_TEMPLATE "/dev/cpu/%u/msr"
static char *socket_path;
static char *pidfile;
static enum { RUNNING, TERMINATE, TERMINATING } state;
static QIOChannelSocket *server_ioc;
static int server_watch;
static int num_active_sockets = 1;
static bool verbose;
#ifdef CONFIG_LIBCAP_NG
static int uid = -1;
static int gid = -1;
#endif
static void compute_default_paths(void)
{
g_autofree char *state = qemu_get_local_state_dir();
socket_path = g_build_filename(state, "run", "qemu-vmsr-helper.sock", NULL);
pidfile = g_build_filename(state, "run", "qemu-vmsr-helper.pid", NULL);
}
static int is_intel_processor(void)
{
int ebx, ecx, edx;
/* Execute CPUID instruction with eax=0 (basic identification) */
asm volatile (
"cpuid"
: "=b" (ebx), "=c" (ecx), "=d" (edx)
: "a" (0)
);
/*
* Check if processor is "GenuineIntel"
* 0x756e6547 = "Genu"
* 0x49656e69 = "ineI"
* 0x6c65746e = "ntel"
*/
return (ebx == 0x756e6547) && (edx == 0x49656e69) && (ecx == 0x6c65746e);
}
static int is_rapl_enabled(void)
{
const char *path = "/sys/class/powercap/intel-rapl/enabled";
FILE *file = fopen(path, "r");
int value = 0;
if (file != NULL) {
if (fscanf(file, "%d", &value) != 1) {
error_report("INTEL RAPL not enabled");
}
fclose(file);
} else {
error_report("Error opening %s", path);
}
return value;
}
/*
* Check if the TID that request the MSR read
* belongs to the peer. It be should a TID of a vCPU.
*/
static bool is_tid_present(pid_t pid, pid_t tid)
{
g_autofree char *tidPath = g_strdup_printf("/proc/%d/task/%d", pid, tid);
/* Check if the TID directory exists within the PID directory */
if (access(tidPath, F_OK) == 0) {
return true;
}
error_report("Failed to open /proc at %s", tidPath);
return false;
}
/*
* Only the RAPL MSR in target/i386/cpu.h are allowed
*/
static bool is_msr_allowed(uint32_t reg)
{
switch (reg) {
case MSR_RAPL_POWER_UNIT:
case MSR_PKG_POWER_LIMIT:
case MSR_PKG_ENERGY_STATUS:
case MSR_PKG_POWER_INFO:
return true;
default:
return false;
}
}
static uint64_t vmsr_read_msr(uint32_t msr_register, unsigned int cpu_id)
{
int fd;
uint64_t result = 0;
g_autofree char *path = g_strdup_printf(MSR_PATH_TEMPLATE, cpu_id);
fd = open(path, O_RDONLY);
if (fd < 0) {
error_report("Failed to open MSR file at %s", path);
return result;
}
if (pread(fd, &result, sizeof(result), msr_register) != sizeof(result)) {
error_report("Failed to read MSR");
result = 0;
}
close(fd);
return result;
}
static void usage(const char *name)
{
(printf) (
"Usage: %s [OPTIONS] FILE\n"
"Virtual RAPL MSR helper program for QEMU\n"
"\n"
" -h, --help display this help and exit\n"
" -V, --version output version information and exit\n"
"\n"
" -d, --daemon run in the background\n"
" -f, --pidfile=PATH PID file when running as a daemon\n"
" (default '%s')\n"
" -k, --socket=PATH path to the unix socket\n"
" (default '%s')\n"
" -T, --trace [[enable=]<pattern>][,events=<file>][,file=<file>]\n"
" specify tracing options\n"
#ifdef CONFIG_LIBCAP_NG
" -u, --user=USER user to drop privileges to\n"
" -g, --group=GROUP group to drop privileges to\n"
#endif
"\n"
QEMU_HELP_BOTTOM "\n"
, name, pidfile, socket_path);
}
static void version(const char *name)
{
printf(
"%s " QEMU_FULL_VERSION "\n"
"Written by Anthony Harivel.\n"
"\n"
QEMU_COPYRIGHT "\n"
"This is free software; see the source for copying conditions. There is NO\n"
"warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n"
, name);
}
typedef struct VMSRHelperClient {
QIOChannelSocket *ioc;
Coroutine *co;
} VMSRHelperClient;
static void coroutine_fn vh_co_entry(void *opaque)
{
VMSRHelperClient *client = opaque;
Error *local_err = NULL;
unsigned int peer_pid;
uint32_t request[3];
uint64_t vmsr;
int r;
if (!qio_channel_set_blocking(QIO_CHANNEL(client->ioc),
false, &local_err)) {
goto out;
}
qio_channel_set_follow_coroutine_ctx(QIO_CHANNEL(client->ioc), true);
/*
* Check peer credentials
*/
r = qio_channel_get_peerpid(QIO_CHANNEL(client->ioc),
&peer_pid,
&local_err);
if (r < 0) {
goto out;
}
for (;;) {
/*
* Read the requested MSR
* Only RAPL MSR in rapl-msr-index.h is allowed
*/
r = qio_channel_read_all_eof(QIO_CHANNEL(client->ioc),
(char *) &request, sizeof(request), &local_err);
if (r <= 0) {
break;
}
if (!is_msr_allowed(request[0])) {
error_report("Requested unallowed msr: %d", request[0]);
break;
}
vmsr = vmsr_read_msr(request[0], request[1]);
if (!is_tid_present(peer_pid, request[2])) {
error_report("Requested TID not in peer PID: %d %d",
peer_pid, request[2]);
vmsr = 0;
}
r = qio_channel_write_all(QIO_CHANNEL(client->ioc),
(char *) &vmsr,
sizeof(vmsr),
&local_err);
if (r < 0) {
break;
}
}
out:
if (local_err) {
if (!verbose) {
error_free(local_err);
} else {
error_report_err(local_err);
}
}
object_unref(OBJECT(client->ioc));
g_free(client);
}
static gboolean accept_client(QIOChannel *ioc,
GIOCondition cond,
gpointer opaque)
{
QIOChannelSocket *cioc;
VMSRHelperClient *vmsrh;
cioc = qio_channel_socket_accept(QIO_CHANNEL_SOCKET(ioc),
NULL);
if (!cioc) {
return TRUE;
}
vmsrh = g_new(VMSRHelperClient, 1);
vmsrh->ioc = cioc;
vmsrh->co = qemu_coroutine_create(vh_co_entry, vmsrh);
qemu_coroutine_enter(vmsrh->co);
return TRUE;
}
static void termsig_handler(int signum)
{
qatomic_cmpxchg(&state, RUNNING, TERMINATE);
qemu_notify_event();
}
static void close_server_socket(void)
{
assert(server_ioc);
g_source_remove(server_watch);
server_watch = -1;
object_unref(OBJECT(server_ioc));
num_active_sockets--;
}
#ifdef CONFIG_LIBCAP_NG
static int drop_privileges(void)
{
/* clear all capabilities */
capng_clear(CAPNG_SELECT_BOTH);
if (capng_update(CAPNG_ADD, CAPNG_EFFECTIVE | CAPNG_PERMITTED,
CAP_SYS_RAWIO) < 0) {
return -1;
}
return 0;
}
#endif
int main(int argc, char **argv)
{
const char *sopt = "hVk:f:dT:u:g:vq";
struct option lopt[] = {
{ "help", no_argument, NULL, 'h' },
{ "version", no_argument, NULL, 'V' },
{ "socket", required_argument, NULL, 'k' },
{ "pidfile", required_argument, NULL, 'f' },
{ "daemon", no_argument, NULL, 'd' },
{ "trace", required_argument, NULL, 'T' },
{ "verbose", no_argument, NULL, 'v' },
{ NULL, 0, NULL, 0 }
};
int opt_ind = 0;
int ch;
Error *local_err = NULL;
bool daemonize = false;
bool pidfile_specified = false;
bool socket_path_specified = false;
unsigned socket_activation;
struct sigaction sa_sigterm;
memset(&sa_sigterm, 0, sizeof(sa_sigterm));
sa_sigterm.sa_handler = termsig_handler;
sigaction(SIGTERM, &sa_sigterm, NULL);
sigaction(SIGINT, &sa_sigterm, NULL);
sigaction(SIGHUP, &sa_sigterm, NULL);
signal(SIGPIPE, SIG_IGN);
error_init(argv[0]);
module_call_init(MODULE_INIT_TRACE);
module_call_init(MODULE_INIT_QOM);
qemu_add_opts(&qemu_trace_opts);
qemu_init_exec_dir(argv[0]);
compute_default_paths();
/*
* Sanity check
* 1. cpu must be Intel cpu
* 2. RAPL must be enabled
*/
if (!is_intel_processor()) {
error_report("error: CPU is not INTEL cpu");
exit(EXIT_FAILURE);
}
if (!is_rapl_enabled()) {
error_report("error: RAPL driver not enable");
exit(EXIT_FAILURE);
}
while ((ch = getopt_long(argc, argv, sopt, lopt, &opt_ind)) != -1) {
switch (ch) {
case 'k':
g_free(socket_path);
socket_path = g_strdup(optarg);
socket_path_specified = true;
if (socket_path[0] != '/') {
error_report("socket path must be absolute");
exit(EXIT_FAILURE);
}
break;
case 'f':
g_free(pidfile);
pidfile = g_strdup(optarg);
pidfile_specified = true;
break;
#ifdef CONFIG_LIBCAP_NG
case 'u': {
unsigned long res;
struct passwd *userinfo = getpwnam(optarg);
if (userinfo) {
uid = userinfo->pw_uid;
} else if (qemu_strtoul(optarg, NULL, 10, &res) == 0 &&
(uid_t)res == res) {
uid = res;
} else {
error_report("invalid user '%s'", optarg);
exit(EXIT_FAILURE);
}
break;
}
case 'g': {
unsigned long res;
struct group *groupinfo = getgrnam(optarg);
if (groupinfo) {
gid = groupinfo->gr_gid;
} else if (qemu_strtoul(optarg, NULL, 10, &res) == 0 &&
(gid_t)res == res) {
gid = res;
} else {
error_report("invalid group '%s'", optarg);
exit(EXIT_FAILURE);
}
break;
}
#else
case 'u':
case 'g':
error_report("-%c not supported by this %s", ch, argv[0]);
exit(1);
#endif
case 'd':
daemonize = true;
break;
case 'v':
verbose = true;
break;
case 'T':
trace_opt_parse(optarg);
break;
case 'V':
version(argv[0]);
exit(EXIT_SUCCESS);
break;
case 'h':
usage(argv[0]);
exit(EXIT_SUCCESS);
break;
case '?':
error_report("Try `%s --help' for more information.", argv[0]);
exit(EXIT_FAILURE);
}
}
if (!trace_init_backends()) {
exit(EXIT_FAILURE);
}
trace_init_file();
qemu_set_log(LOG_TRACE, &error_fatal);
socket_activation = check_socket_activation();
if (socket_activation == 0) {
SocketAddress saddr;
saddr = (SocketAddress){
.type = SOCKET_ADDRESS_TYPE_UNIX,
.u.q_unix.path = socket_path,
};
server_ioc = qio_channel_socket_new();
if (qio_channel_socket_listen_sync(server_ioc, &saddr,
1, &local_err) < 0) {
object_unref(OBJECT(server_ioc));
error_report_err(local_err);
return 1;
}
} else {
/* Using socket activation - check user didn't use -p etc. */
if (socket_path_specified) {
error_report("Unix socket can't be set when"
"using socket activation");
exit(EXIT_FAILURE);
}
/* Can only listen on a single socket. */
if (socket_activation > 1) {
error_report("%s does not support socket activation"
"with LISTEN_FDS > 1",
argv[0]);
exit(EXIT_FAILURE);
}
server_ioc = qio_channel_socket_new_fd(FIRST_SOCKET_ACTIVATION_FD,
&local_err);
if (server_ioc == NULL) {
error_reportf_err(local_err,
"Failed to use socket activation: ");
exit(EXIT_FAILURE);
}
}
qemu_init_main_loop(&error_fatal);
server_watch = qio_channel_add_watch(QIO_CHANNEL(server_ioc),
G_IO_IN,
accept_client,
NULL, NULL);
if (daemonize) {
if (daemon(0, 0) < 0) {
error_report("Failed to daemonize: %s", strerror(errno));
exit(EXIT_FAILURE);
}
}
if (daemonize || pidfile_specified) {
qemu_write_pidfile(pidfile, &error_fatal);
}
#ifdef CONFIG_LIBCAP_NG
if (drop_privileges() < 0) {
error_report("Failed to drop privileges: %s", strerror(errno));
exit(EXIT_FAILURE);
}
#endif
info_report("Listening on %s", socket_path);
state = RUNNING;
do {
main_loop_wait(false);
if (state == TERMINATE) {
state = TERMINATING;
close_server_socket();
}
} while (num_active_sockets > 0);
exit(EXIT_SUCCESS);
}
+28
View File
@@ -0,0 +1,28 @@
/*
* Allowed list of MSR for Privileged RAPL MSR helper commands for QEMU
*
* Copyright (C) 2023 Red Hat, Inc. <[email protected]>
*
* Author: Anthony Harivel <[email protected]>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; under version 2 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
/*
* Should stay in sync with the RAPL MSR
* in target/i386/cpu.h
*/
#define MSR_RAPL_POWER_UNIT 0x00000606
#define MSR_PKG_POWER_LIMIT 0x00000610
#define MSR_PKG_ENERGY_STATUS 0x00000611
#define MSR_PKG_POWER_INFO 0x00000614
View File
+308
View File
@@ -0,0 +1,308 @@
/*
* Standalone VNC server connecting to QEMU via D-Bus display interface.
* Audio support. Only one audio stream is tracked.
* Mixing/resampling to be added, if needed.
*
* Copyright (C) 2026 Red Hat, Inc.
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "qemu/osdep.h"
#include "qemu/audio.h"
#include "qemu/audio-capture.h"
#include "qemu/sockets.h"
#include "qemu/error-report.h"
#include "ui/dbus-display1.h"
#include "trace.h"
#include "qemu-vnc.h"
struct CaptureVoiceOut {
struct audsettings as;
struct audio_capture_ops ops;
void *opaque;
QLIST_ENTRY(CaptureVoiceOut) entries;
};
typedef struct AudioOut {
guint64 id;
struct audsettings as;
} AudioOut;
static QLIST_HEAD(, CaptureVoiceOut) capture_list =
QLIST_HEAD_INITIALIZER(capture_list);
static GDBusConnection *audio_listener_conn;
static AudioOut audio_out;
static bool audsettings_eq(const struct audsettings *a,
const struct audsettings *b)
{
return a->freq == b->freq &&
a->nchannels == b->nchannels &&
a->fmt == b->fmt &&
a->big_endian == b->big_endian;
}
static gboolean
on_audio_out_init(QemuDBusDisplay1AudioOutListener *listener,
GDBusMethodInvocation *invocation,
guint64 id, guchar bits, gboolean is_signed,
gboolean is_float, guint freq, guchar nchannels,
guint bytes_per_frame, guint bytes_per_second,
gboolean be, gpointer user_data)
{
AudioFormat fmt;
switch (bits) {
case 8:
fmt = is_signed ? AUDIO_FORMAT_S8 : AUDIO_FORMAT_U8;
break;
case 16:
fmt = is_signed ? AUDIO_FORMAT_S16 : AUDIO_FORMAT_U16;
break;
case 32:
fmt = is_float ? AUDIO_FORMAT_F32 :
is_signed ? AUDIO_FORMAT_S32 : AUDIO_FORMAT_U32;
break;
default:
g_return_val_if_reached(DBUS_METHOD_INVOCATION_HANDLED);
}
struct audsettings as = {
.freq = freq,
.nchannels = nchannels,
.fmt = fmt,
.big_endian = be,
};
audio_out = (AudioOut) {
.id = id,
.as = as,
};
trace_qemu_vnc_audio_out_init(id, freq, nchannels, bits);
qemu_dbus_display1_audio_out_listener_complete_init(
listener, invocation);
return DBUS_METHOD_INVOCATION_HANDLED;
}
static gboolean
on_audio_out_fini(QemuDBusDisplay1AudioOutListener *listener,
GDBusMethodInvocation *invocation,
guint64 id, gpointer user_data)
{
trace_qemu_vnc_audio_out_fini(id);
qemu_dbus_display1_audio_out_listener_complete_fini(
listener, invocation);
return DBUS_METHOD_INVOCATION_HANDLED;
}
static gboolean
on_audio_out_set_enabled(QemuDBusDisplay1AudioOutListener *listener,
GDBusMethodInvocation *invocation,
guint64 id, gboolean enabled,
gpointer user_data)
{
CaptureVoiceOut *cap;
trace_qemu_vnc_audio_out_set_enabled(id, enabled);
if (id == audio_out.id) {
QLIST_FOREACH(cap, &capture_list, entries) {
cap->ops.notify(cap->opaque,
enabled ? AUD_CNOTIFY_ENABLE
: AUD_CNOTIFY_DISABLE);
}
}
qemu_dbus_display1_audio_out_listener_complete_set_enabled(
listener, invocation);
return DBUS_METHOD_INVOCATION_HANDLED;
}
static gboolean
on_audio_out_set_volume(QemuDBusDisplay1AudioOutListener *listener,
GDBusMethodInvocation *invocation,
guint64 id, gboolean mute,
GVariant *volume, gpointer user_data)
{
qemu_dbus_display1_audio_out_listener_complete_set_volume(
listener, invocation);
return DBUS_METHOD_INVOCATION_HANDLED;
}
static gboolean
on_audio_out_write(QemuDBusDisplay1AudioOutListener *listener,
GDBusMethodInvocation *invocation,
guint64 id, GVariant *data,
gpointer user_data)
{
CaptureVoiceOut *cap;
gsize size;
const void *buf;
if (id == audio_out.id) {
buf = g_variant_get_fixed_array(data, &size, 1);
trace_qemu_vnc_audio_out_write(id, size);
QLIST_FOREACH(cap, &capture_list, entries) {
/* we don't handle audio resampling/format conversion */
if (audsettings_eq(&cap->as, &audio_out.as)) {
cap->ops.capture(cap->opaque, buf, size);
}
}
}
qemu_dbus_display1_audio_out_listener_complete_write(
listener, invocation);
return DBUS_METHOD_INVOCATION_HANDLED;
}
CaptureVoiceOut *audio_be_add_capture(
AudioBackend *be,
const struct audsettings *as,
const struct audio_capture_ops *ops,
void *opaque)
{
CaptureVoiceOut *cap;
if (!audio_listener_conn) {
return NULL;
}
cap = g_new0(CaptureVoiceOut, 1);
cap->ops = *ops;
cap->opaque = opaque;
cap->as = *as;
QLIST_INSERT_HEAD(&capture_list, cap, entries);
return cap;
}
void audio_be_del_capture(
AudioBackend *be,
CaptureVoiceOut *cap,
void *cb_opaque)
{
if (!cap) {
return;
}
cap->ops.destroy(cap->opaque);
QLIST_REMOVE(cap, entries);
g_free(cap);
}
/*
* Dummy audio backend — the VNC server only needs a non-NULL pointer
* so that audio capture registration doesn't bail out. The pointer
* is never dereferenced by our code (audio_be_add_capture ignores it).
*/
static AudioBackend dummy_audio_be;
AudioBackend *audio_get_default_audio_be(Error **errp)
{
return &dummy_audio_be;
}
AudioBackend *audio_be_by_name(const char *name, Error **errp)
{
return NULL;
}
static void
on_register_audio_listener_finished(GObject *source_object,
GAsyncResult *res,
gpointer user_data)
{
GThread *thread = user_data;
g_autoptr(GError) err = NULL;
g_autoptr(GDBusObjectSkeleton) obj = NULL;
GDBusObjectManagerServer *server;
QemuDBusDisplay1AudioOutListener *audio_skel;
qemu_dbus_display1_audio_call_register_out_listener_finish(
QEMU_DBUS_DISPLAY1_AUDIO(source_object),
NULL, res, &err);
if (err) {
error_report("RegisterOutListener failed: %s", err->message);
g_thread_join(thread);
return;
}
audio_listener_conn = g_thread_join(thread);
if (!audio_listener_conn) {
return;
}
server = g_dbus_object_manager_server_new(DBUS_DISPLAY1_ROOT);
obj = g_dbus_object_skeleton_new(
DBUS_DISPLAY1_ROOT "/AudioOutListener");
audio_skel = qemu_dbus_display1_audio_out_listener_skeleton_new();
g_object_connect(audio_skel,
"signal::handle-init",
on_audio_out_init, NULL,
"signal::handle-fini",
on_audio_out_fini, NULL,
"signal::handle-set-enabled",
on_audio_out_set_enabled, NULL,
"signal::handle-set-volume",
on_audio_out_set_volume, NULL,
"signal::handle-write",
on_audio_out_write, NULL,
NULL);
g_dbus_object_skeleton_add_interface(
obj, G_DBUS_INTERFACE_SKELETON(audio_skel));
g_dbus_object_manager_server_export(server, obj);
g_dbus_object_manager_server_set_connection(
server, audio_listener_conn);
g_dbus_connection_start_message_processing(audio_listener_conn);
}
void audio_setup(GDBusObjectManager *manager)
{
g_autoptr(GError) err = NULL;
g_autoptr(GUnixFDList) fd_list = NULL;
g_autoptr(GDBusInterface) iface = NULL;
GThread *thread;
int pair[2];
int idx;
iface = g_dbus_object_manager_get_interface(
manager, DBUS_DISPLAY1_ROOT "/Audio",
"org.qemu.Display1.Audio");
if (!iface) {
return;
}
if (qemu_socketpair(AF_UNIX, SOCK_STREAM, 0, pair) < 0) {
error_report("audio socketpair failed: %s", strerror(errno));
return;
}
fd_list = g_unix_fd_list_new();
idx = g_unix_fd_list_append(fd_list, pair[1], &err);
close(pair[1]);
if (idx < 0) {
close(pair[0]);
error_report("Failed to append fd: %s", err->message);
return;
}
thread = p2p_dbus_thread_new(pair[0]);
qemu_dbus_display1_audio_call_register_out_listener(
QEMU_DBUS_DISPLAY1_AUDIO(iface),
g_variant_new_handle(idx),
G_DBUS_CALL_FLAGS_NONE, -1,
fd_list, NULL,
on_register_audio_listener_finished,
thread);
}
+148
View File
@@ -0,0 +1,148 @@
/*
* Standalone VNC server connecting to QEMU via D-Bus display interface.
*
* Copyright (C) 2026 Red Hat, Inc.
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "qemu/osdep.h"
#include "qemu/sockets.h"
#include "qemu/error-report.h"
#include "qapi/util.h"
#include "qapi-types-char.h"
#include "ui/dbus-display1.h"
#include "trace.h"
#include "qemu-vnc.h"
typedef struct ChardevRegisterData {
QemuDBusDisplay1Chardev *proxy;
int local_fd;
char *name;
bool echo;
ChardevVCEncoding encoding;
} ChardevRegisterData;
static void
on_chardev_register_finished(GObject *source_object,
GAsyncResult *res,
gpointer user_data)
{
ChardevRegisterData *data = user_data;
g_autoptr(GError) err = NULL;
QemuTextConsole *tc;
if (!qemu_dbus_display1_chardev_call_register_finish(
data->proxy, NULL, res, &err)) {
error_report("Chardev Register failed for %s: %s",
data->name, err->message);
close(data->local_fd);
goto out;
}
tc = qemu_vnc_text_console_new(data->name, data->local_fd, data->echo,
data->encoding);
if (!tc) {
close(data->local_fd);
goto out;
}
trace_qemu_vnc_chardev_connected(data->name);
out:
g_object_unref(data->proxy);
g_free(data->name);
g_free(data);
}
/* Default chardevs to expose as VNC text consoles */
static const char * const default_names[] = {
"org.qemu.console.serial.0",
"org.qemu.monitor.hmp.0",
NULL,
};
/* Active chardev names list (points to CLI args or default_names) */
static const char * const *names;
static void
chardev_register(QemuDBusDisplay1Chardev *proxy,
ChardevVCEncoding encoding)
{
g_autoptr(GUnixFDList) fd_list = NULL;
ChardevRegisterData *data;
const char *name;
int pair[2];
int idx;
name = qemu_dbus_display1_chardev_get_name(proxy);
if (!name || !g_strv_contains(names, name)) {
return;
}
if (qemu_socketpair(AF_UNIX, SOCK_STREAM, 0, pair) < 0) {
error_report("chardev socketpair failed: %s", strerror(errno));
return;
}
fd_list = g_unix_fd_list_new();
idx = g_unix_fd_list_append(fd_list, pair[1], NULL);
close(pair[1]);
data = g_new0(ChardevRegisterData, 1);
data->proxy = g_object_ref(proxy);
data->local_fd = pair[0];
data->name = g_strdup(name);
data->echo = qemu_dbus_display1_chardev_get_echo(proxy);
data->encoding = encoding;
qemu_dbus_display1_chardev_call_register(
proxy, g_variant_new_handle(idx),
G_DBUS_CALL_FLAGS_NONE, -1,
fd_list, NULL,
on_chardev_register_finished, data);
}
void chardev_setup(const char * const *chardev_names,
GDBusObjectManager *manager)
{
GList *objects, *l;
names = chardev_names ? chardev_names : default_names;
objects = g_dbus_object_manager_get_objects(manager);
for (l = objects; l; l = l->next) {
GDBusObject *obj = l->data;
const char *path = g_dbus_object_get_object_path(obj);
g_autoptr(GDBusInterface) iface = NULL;
g_autoptr(GDBusInterface) enc_iface = NULL;
ChardevVCEncoding encoding = CHARDEV_VC_ENCODING_UTF8;
if (!g_str_has_prefix(path, DBUS_DISPLAY1_ROOT "/Chardev_")) {
continue;
}
iface = g_dbus_object_get_interface(
obj, "org.qemu.Display1.Chardev");
if (!iface) {
continue;
}
enc_iface = g_dbus_object_get_interface(
obj, "org.qemu.Display1.Chardev.VCEncoding");
if (enc_iface) {
const char *enc_str =
qemu_dbus_display1_chardev_vcencoding_get_encoding(
QEMU_DBUS_DISPLAY1_CHARDEV_VCENCODING(enc_iface));
int enc = qapi_enum_parse(&ChardevVCEncoding_lookup,
enc_str, -1, NULL);
if (enc >= 0) {
encoding = enc;
}
}
chardev_register(QEMU_DBUS_DISPLAY1_CHARDEV(iface), encoding);
}
g_list_free_full(objects, g_object_unref);
}
+376
View File
@@ -0,0 +1,376 @@
/*
* Standalone VNC server connecting to QEMU via D-Bus display interface.
*
* Copyright (C) 2026 Red Hat, Inc.
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "qemu/osdep.h"
#include "qemu/error-report.h"
#include "ui/clipboard.h"
#include "ui/dbus-display1.h"
#include "trace.h"
#include "qemu-vnc.h"
#define MIME_TEXT_PLAIN_UTF8 "text/plain;charset=utf-8"
typedef struct {
GDBusMethodInvocation *invocation;
QemuClipboardType type;
guint timeout_id;
} VncDBusClipboardRequest;
static QemuDBusDisplay1Clipboard *clipboard_proxy;
static QemuDBusDisplay1Clipboard *clipboard_skel;
static QemuClipboardPeer clipboard_peer;
static uint32_t clipboard_serial;
static VncDBusClipboardRequest
clipboard_request[QEMU_CLIPBOARD_SELECTION__COUNT];
static void
vnc_dbus_clipboard_complete_request(
GDBusMethodInvocation *invocation,
QemuClipboardInfo *info,
QemuClipboardType type)
{
GVariant *v_data = g_variant_new_from_data(
G_VARIANT_TYPE("ay"),
info->types[type].data,
info->types[type].size,
TRUE,
(GDestroyNotify)qemu_clipboard_info_unref,
qemu_clipboard_info_ref(info));
qemu_dbus_display1_clipboard_complete_request(
clipboard_skel, invocation,
MIME_TEXT_PLAIN_UTF8, v_data);
}
static void
vnc_dbus_clipboard_request_cancelled(VncDBusClipboardRequest *req)
{
if (!req->invocation) {
return;
}
g_dbus_method_invocation_return_error(
req->invocation,
G_DBUS_ERROR,
G_DBUS_ERROR_FAILED,
"Cancelled clipboard request");
g_clear_object(&req->invocation);
g_clear_handle_id(&req->timeout_id, g_source_remove);;
}
static gboolean
vnc_dbus_clipboard_request_timeout(gpointer user_data)
{
vnc_dbus_clipboard_request_cancelled(user_data);
return G_SOURCE_REMOVE;
}
static void
vnc_dbus_clipboard_request(QemuClipboardInfo *info,
QemuClipboardType type)
{
g_autofree char *mime = NULL;
g_autoptr(GVariant) v_data = NULL;
g_autoptr(GError) err = NULL;
const char *data = NULL;
const char *mimes[] = { MIME_TEXT_PLAIN_UTF8, NULL };
size_t n;
if (type != QEMU_CLIPBOARD_TYPE_TEXT) {
return;
}
if (!clipboard_proxy) {
return;
}
if (!qemu_dbus_display1_clipboard_call_request_sync(
clipboard_proxy,
info->selection,
mimes,
G_DBUS_CALL_FLAGS_NONE, -1, &mime, &v_data, NULL, &err)) {
error_report("Failed to request clipboard: %s", err->message);
return;
}
if (!g_str_equal(mime, MIME_TEXT_PLAIN_UTF8)) {
error_report("Unsupported returned MIME: %s", mime);
return;
}
data = g_variant_get_fixed_array(v_data, &n, 1);
qemu_clipboard_set_data(&clipboard_peer, info, type,
n, data, true);
}
static void
vnc_dbus_clipboard_update_info(QemuClipboardInfo *info)
{
bool self_update = info->owner == &clipboard_peer;
const char *mime[QEMU_CLIPBOARD_TYPE__COUNT + 1] = { 0, };
VncDBusClipboardRequest *req;
int i = 0;
if (info->owner == NULL) {
if (clipboard_proxy) {
qemu_dbus_display1_clipboard_call_release(
clipboard_proxy,
info->selection,
G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
}
return;
}
if (self_update) {
return;
}
req = &clipboard_request[info->selection];
if (req->invocation && info->types[req->type].data) {
vnc_dbus_clipboard_complete_request(
req->invocation, info, req->type);
g_clear_object(&req->invocation);
g_clear_handle_id(&req->timeout_id, g_source_remove);;
return;
}
if (info->types[QEMU_CLIPBOARD_TYPE_TEXT].available) {
mime[i++] = MIME_TEXT_PLAIN_UTF8;
}
if (i > 0 && clipboard_proxy) {
uint32_t serial = info->has_serial ?
info->serial : ++clipboard_serial;
qemu_dbus_display1_clipboard_call_grab(
clipboard_proxy,
info->selection,
serial,
mime,
G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
}
}
static void
vnc_dbus_clipboard_notify(Notifier *notifier, void *data)
{
QemuClipboardNotify *notify = data;
switch (notify->type) {
case QEMU_CLIPBOARD_UPDATE_INFO:
vnc_dbus_clipboard_update_info(notify->info);
return;
case QEMU_CLIPBOARD_RESET_SERIAL:
if (clipboard_proxy) {
qemu_dbus_display1_clipboard_call_register(
clipboard_proxy,
G_DBUS_CALL_FLAGS_NONE,
-1, NULL, NULL, NULL);
}
return;
}
}
static gboolean
on_clipboard_register(QemuDBusDisplay1Clipboard *clipboard,
GDBusMethodInvocation *invocation,
gpointer user_data)
{
clipboard_serial = 0;
qemu_clipboard_reset_serial();
qemu_dbus_display1_clipboard_complete_register(
clipboard, invocation);
return DBUS_METHOD_INVOCATION_HANDLED;
}
static gboolean
on_clipboard_unregister(QemuDBusDisplay1Clipboard *clipboard,
GDBusMethodInvocation *invocation,
gpointer user_data)
{
int i;
for (i = 0; i < G_N_ELEMENTS(clipboard_request); ++i) {
vnc_dbus_clipboard_request_cancelled(&clipboard_request[i]);
}
qemu_dbus_display1_clipboard_complete_unregister(
clipboard, invocation);
return DBUS_METHOD_INVOCATION_HANDLED;
}
static gboolean
on_clipboard_grab(QemuDBusDisplay1Clipboard *clipboard,
GDBusMethodInvocation *invocation,
gint arg_selection,
guint arg_serial,
const gchar *const *arg_mimes,
gpointer user_data)
{
QemuClipboardSelection s = arg_selection;
g_autoptr(QemuClipboardInfo) info = NULL;
if (s >= QEMU_CLIPBOARD_SELECTION__COUNT) {
g_dbus_method_invocation_return_error(
invocation,
G_DBUS_ERROR,
G_DBUS_ERROR_FAILED,
"Invalid clipboard selection: %d", arg_selection);
return DBUS_METHOD_INVOCATION_HANDLED;
}
trace_qemu_vnc_clipboard_grab(arg_selection, arg_serial);
info = qemu_clipboard_info_new(&clipboard_peer, s);
if (g_strv_contains(arg_mimes, MIME_TEXT_PLAIN_UTF8)) {
info->types[QEMU_CLIPBOARD_TYPE_TEXT].available = true;
}
info->serial = arg_serial;
info->has_serial = true;
if (qemu_clipboard_check_serial(info, true)) {
qemu_clipboard_update(info);
}
qemu_dbus_display1_clipboard_complete_grab(
clipboard, invocation);
return DBUS_METHOD_INVOCATION_HANDLED;
}
static gboolean
on_clipboard_release(QemuDBusDisplay1Clipboard *clipboard,
GDBusMethodInvocation *invocation,
gint arg_selection,
gpointer user_data)
{
trace_qemu_vnc_clipboard_release(arg_selection);
qemu_clipboard_peer_release(&clipboard_peer, arg_selection);
qemu_dbus_display1_clipboard_complete_release(
clipboard, invocation);
return DBUS_METHOD_INVOCATION_HANDLED;
}
static gboolean
on_clipboard_request(QemuDBusDisplay1Clipboard *clipboard,
GDBusMethodInvocation *invocation,
gint arg_selection,
const gchar *const *arg_mimes,
gpointer user_data)
{
QemuClipboardSelection s = arg_selection;
QemuClipboardType type = QEMU_CLIPBOARD_TYPE_TEXT;
QemuClipboardInfo *info = NULL;
trace_qemu_vnc_clipboard_request(arg_selection);
if (s >= QEMU_CLIPBOARD_SELECTION__COUNT) {
g_dbus_method_invocation_return_error(
invocation,
G_DBUS_ERROR,
G_DBUS_ERROR_FAILED,
"Invalid clipboard selection: %d", arg_selection);
return DBUS_METHOD_INVOCATION_HANDLED;
}
if (clipboard_request[s].invocation) {
g_dbus_method_invocation_return_error(
invocation,
G_DBUS_ERROR,
G_DBUS_ERROR_FAILED,
"Pending request");
return DBUS_METHOD_INVOCATION_HANDLED;
}
info = qemu_clipboard_info(s);
if (!info || !info->owner || info->owner == &clipboard_peer) {
g_dbus_method_invocation_return_error(
invocation,
G_DBUS_ERROR,
G_DBUS_ERROR_FAILED,
"Empty clipboard");
return DBUS_METHOD_INVOCATION_HANDLED;
}
if (!g_strv_contains(arg_mimes, MIME_TEXT_PLAIN_UTF8) ||
!info->types[type].available) {
g_dbus_method_invocation_return_error(
invocation,
G_DBUS_ERROR,
G_DBUS_ERROR_FAILED,
"Unhandled MIME types requested");
return DBUS_METHOD_INVOCATION_HANDLED;
}
if (info->types[type].data) {
vnc_dbus_clipboard_complete_request(invocation, info, type);
} else {
qemu_clipboard_request(info, type);
clipboard_request[s].invocation = g_object_ref(invocation);
clipboard_request[s].type = type;
clipboard_request[s].timeout_id =
g_timeout_add_seconds(5,
vnc_dbus_clipboard_request_timeout,
&clipboard_request[s]);
}
return DBUS_METHOD_INVOCATION_HANDLED;
}
void clipboard_setup(GDBusObjectManager *manager, GDBusConnection *bus)
{
g_autoptr(GError) err = NULL;
g_autoptr(GDBusInterface) iface = NULL;
iface = g_dbus_object_manager_get_interface(
manager, DBUS_DISPLAY1_ROOT "/Clipboard",
"org.qemu.Display1.Clipboard");
if (!iface) {
return;
}
clipboard_proxy = g_object_ref(QEMU_DBUS_DISPLAY1_CLIPBOARD(iface));
clipboard_skel = qemu_dbus_display1_clipboard_skeleton_new();
g_object_connect(clipboard_skel,
"signal::handle-register",
on_clipboard_register, NULL,
"signal::handle-unregister",
on_clipboard_unregister, NULL,
"signal::handle-grab",
on_clipboard_grab, NULL,
"signal::handle-release",
on_clipboard_release, NULL,
"signal::handle-request",
on_clipboard_request, NULL,
NULL);
if (!g_dbus_interface_skeleton_export(
G_DBUS_INTERFACE_SKELETON(clipboard_skel),
bus,
DBUS_DISPLAY1_ROOT "/Clipboard",
&err)) {
error_report("Failed to export clipboard: %s", err->message);
g_clear_object(&clipboard_skel);
g_clear_object(&clipboard_proxy);
return;
}
clipboard_peer.name = "dbus";
clipboard_peer.notifier.notify = vnc_dbus_clipboard_notify;
clipboard_peer.request = vnc_dbus_clipboard_request;
qemu_clipboard_peer_register(&clipboard_peer);
qemu_dbus_display1_clipboard_call_register(
clipboard_proxy,
G_DBUS_CALL_FLAGS_NONE,
-1, NULL, NULL, NULL);
}
+170
View File
@@ -0,0 +1,170 @@
/*
* Minimal QemuConsole helpers for the standalone qemu-vnc binary.
*
* Copyright (C) 2026 Red Hat, Inc.
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "qemu/osdep.h"
#include "ui/console.h"
#include "ui/console-priv.h"
#include "ui/vt100.h"
#include "qapi-types-char.h"
#include "qemu-vnc.h"
#include "trace.h"
/*
* Our own QemuTextConsole definition — the one in console-vc.c uses
* a Chardev* backend which is not available in the standalone binary.
* Here we drive the VT100 emulator directly over a raw file descriptor.
*/
typedef struct QemuTextConsole {
QemuConsole parent;
QemuVT100 vt;
int chardev_fd;
guint io_watch_id;
char *name;
} QemuTextConsole;
typedef QemuConsoleClass QemuTextConsoleClass;
OBJECT_DEFINE_TYPE(QemuTextConsole, qemu_text_console,
QEMU_TEXT_CONSOLE, QEMU_CONSOLE)
static void qemu_text_console_class_init(ObjectClass *oc, const void *data)
{
}
static void text_console_invalidate(void *opaque)
{
QemuTextConsole *s = QEMU_TEXT_CONSOLE(opaque);
vt100_set_image(&s->vt, QEMU_CONSOLE(s)->surface->image);
vt100_refresh(&s->vt);
}
static const GraphicHwOps text_console_ops = {
.invalidate = text_console_invalidate,
};
static void qemu_text_console_init(Object *obj)
{
QemuTextConsole *c = QEMU_TEXT_CONSOLE(obj);
QEMU_CONSOLE(c)->hw_ops = &text_console_ops;
QEMU_CONSOLE(c)->hw = c;
}
static void qemu_text_console_finalize(Object *obj)
{
QemuTextConsole *tc = QEMU_TEXT_CONSOLE(obj);
vt100_fini(&tc->vt);
if (tc->io_watch_id) {
g_source_remove(tc->io_watch_id);
}
if (tc->chardev_fd >= 0) {
close(tc->chardev_fd);
}
g_free(tc->name);
}
static void text_console_out_flush(QemuVT100 *vt)
{
QemuTextConsole *tc = container_of(vt, QemuTextConsole, vt);
const uint8_t *data;
uint32_t len;
while (!fifo8_is_empty(&vt->out_fifo)) {
ssize_t ret;
data = fifo8_pop_bufptr(&vt->out_fifo,
fifo8_num_used(&vt->out_fifo), &len);
ret = write(tc->chardev_fd, data, len);
if (ret < 0) {
trace_qemu_vnc_console_io_error(tc->name);
break;
}
}
}
static void text_console_image_update(QemuVT100 *vt, int x, int y, int w, int h)
{
QemuTextConsole *tc = container_of(vt, QemuTextConsole, vt);
QemuConsole *con = QEMU_CONSOLE(tc);
qemu_console_update(con, x, y, w, h);
}
static gboolean text_console_io_cb(GIOChannel *source,
GIOCondition cond, gpointer data)
{
QemuTextConsole *tc = data;
uint8_t buf[4096];
ssize_t n;
if (cond & (G_IO_HUP | G_IO_ERR)) {
tc->io_watch_id = 0;
return G_SOURCE_REMOVE;
}
n = read(tc->chardev_fd, buf, sizeof(buf));
if (n <= 0) {
trace_qemu_vnc_console_io_error(tc->name);
tc->io_watch_id = 0;
return G_SOURCE_REMOVE;
}
vt100_input(&tc->vt, buf, n);
return G_SOURCE_CONTINUE;
}
QemuTextConsole *qemu_vnc_text_console_new(const char *name,
int fd, bool echo,
ChardevVCEncoding encoding)
{
int w = TEXT_COLS * TEXT_FONT_WIDTH;
int h = TEXT_ROWS * TEXT_FONT_HEIGHT;
QemuTextConsole *tc;
QemuConsole *con;
pixman_image_t *image;
GIOChannel *chan;
tc = QEMU_TEXT_CONSOLE(object_new(TYPE_QEMU_TEXT_CONSOLE));
con = QEMU_CONSOLE(tc);
tc->name = g_strdup(name);
tc->chardev_fd = fd;
image = pixman_image_create_bits(PIXMAN_x8r8g8b8, w, h, NULL, 0);
con->surface = qemu_create_displaysurface_pixman(image);
con->scanout.kind = SCANOUT_SURFACE;
qemu_pixman_image_unref(image);
vt100_init(&tc->vt, con->surface->image, encoding,
text_console_image_update, text_console_out_flush);
tc->vt.echo = echo;
vt100_refresh(&tc->vt);
chan = g_io_channel_unix_new(fd);
g_io_channel_set_encoding(chan, NULL, NULL);
tc->io_watch_id = g_io_add_watch(chan,
G_IO_IN | G_IO_HUP | G_IO_ERR,
text_console_io_cb, tc);
g_io_channel_unref(chan);
return tc;
}
void qemu_text_console_handle_keysym(QemuTextConsole *s, int keysym)
{
vt100_keysym(&s->vt, keysym);
}
void qemu_text_console_update_size(QemuTextConsole *c)
{
qemu_console_text_resize(QEMU_CONSOLE(c), c->vt.width, c->vt.height);
}
+474
View File
@@ -0,0 +1,474 @@
/*
* D-Bus interface for qemu-vnc standalone VNC server.
*
* Copyright (C) 2026 Red Hat, Inc.
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "qemu/osdep.h"
#include "qemu/cutils.h"
#include "qapi-types-trace.h"
#include "system/system.h"
#include "qapi/qapi-types-ui.h"
#include "qapi/qapi-commands-ui.h"
#include "qemu-vnc.h"
#include "qemu-vnc1.h"
#include "qapi/qapi-emit-events.h"
#include "qobject/qdict.h"
#include "ui/vnc.h"
#include "trace.h"
typedef struct VncDbusClient {
QemuVnc1ClientSkeleton *skeleton;
char *path;
char *host;
char *service;
unsigned int id;
QTAILQ_ENTRY(VncDbusClient) next;
} VncDbusClient;
static QemuVnc1ServerSkeleton *server_skeleton;
static GDBusObjectManagerServer *obj_manager;
static unsigned int next_client_id;
static QTAILQ_HEAD(, VncDbusClient)
dbus_clients = QTAILQ_HEAD_INITIALIZER(dbus_clients);
static VncDbusClient *vnc_dbus_find_client(const char *host,
const char *service)
{
VncDbusClient *c;
QTAILQ_FOREACH(c, &dbus_clients, next) {
if (g_str_equal(c->host, host) &&
g_str_equal(c->service, service)) {
return c;
}
}
return NULL;
}
static void vnc_dbus_update_clients_property(void)
{
VncDbusClient *c;
GPtrArray *paths;
const char **strv;
paths = g_ptr_array_new();
QTAILQ_FOREACH(c, &dbus_clients, next) {
g_ptr_array_add(paths, c->path);
}
g_ptr_array_add(paths, NULL);
strv = (const char **)paths->pdata;
qemu_vnc1_server_set_clients(QEMU_VNC1_SERVER(server_skeleton), strv);
g_ptr_array_free(paths, TRUE);
}
void vnc_dbus_client_connected(const char *host, const char *service,
const char *family, bool websocket)
{
VncDbusClient *c;
g_autoptr(GDBusObjectSkeleton) obj = NULL;
if (!server_skeleton) {
return;
}
c = g_new0(VncDbusClient, 1);
c->id = next_client_id++;
c->host = g_strdup(host);
c->service = g_strdup(service);
c->path = g_strdup_printf("/org/qemu/Vnc1/Client_%u", c->id);
c->skeleton = QEMU_VNC1_CLIENT_SKELETON(qemu_vnc1_client_skeleton_new());
qemu_vnc1_client_set_host(QEMU_VNC1_CLIENT(c->skeleton), host);
qemu_vnc1_client_set_service(QEMU_VNC1_CLIENT(c->skeleton), service);
qemu_vnc1_client_set_family(QEMU_VNC1_CLIENT(c->skeleton), family);
qemu_vnc1_client_set_web_socket(QEMU_VNC1_CLIENT(c->skeleton), websocket);
qemu_vnc1_client_set_x509_dname(QEMU_VNC1_CLIENT(c->skeleton), "");
qemu_vnc1_client_set_sasl_username(QEMU_VNC1_CLIENT(c->skeleton), "");
obj = g_dbus_object_skeleton_new(c->path);
g_dbus_object_skeleton_add_interface(
obj, G_DBUS_INTERFACE_SKELETON(c->skeleton));
g_dbus_object_manager_server_export(obj_manager, obj);
QTAILQ_INSERT_TAIL(&dbus_clients, c, next);
vnc_dbus_update_clients_property();
qemu_vnc1_server_emit_client_connected(
QEMU_VNC1_SERVER(server_skeleton), c->path);
}
void vnc_dbus_client_initialized(const char *host, const char *service,
const char *x509_dname,
const char *sasl_username)
{
VncDbusClient *c;
if (!server_skeleton) {
return;
}
c = vnc_dbus_find_client(host, service);
if (!c) {
trace_qemu_vnc_client_not_found(host, service);
return;
}
if (x509_dname) {
qemu_vnc1_client_set_x509_dname(
QEMU_VNC1_CLIENT(c->skeleton), x509_dname);
}
if (sasl_username) {
qemu_vnc1_client_set_sasl_username(
QEMU_VNC1_CLIENT(c->skeleton), sasl_username);
}
qemu_vnc1_server_emit_client_initialized(
QEMU_VNC1_SERVER(server_skeleton), c->path);
}
void vnc_dbus_client_disconnected(const char *host, const char *service)
{
VncDbusClient *c;
if (!server_skeleton) {
return;
}
c = vnc_dbus_find_client(host, service);
if (!c) {
trace_qemu_vnc_client_not_found(host, service);
return;
}
qemu_vnc1_server_emit_client_disconnected(
QEMU_VNC1_SERVER(server_skeleton), c->path);
g_dbus_object_manager_server_unexport(obj_manager, c->path);
QTAILQ_REMOVE(&dbus_clients, c, next);
vnc_dbus_update_clients_property();
g_object_unref(c->skeleton);
g_free(c->path);
g_free(c->host);
g_free(c->service);
g_free(c);
}
static gboolean
on_set_password(QemuVnc1Server *iface,
GDBusMethodInvocation *invocation,
const gchar *password,
gpointer user_data)
{
Error *err = NULL;
if (vnc_display_password("default", password, &err) < 0) {
g_dbus_method_invocation_return_error(
invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
"%s", error_get_pretty(err));
error_free(err);
return TRUE;
}
qemu_vnc1_server_complete_set_password(iface, invocation);
return TRUE;
}
static gboolean
on_expire_password(QemuVnc1Server *iface,
GDBusMethodInvocation *invocation,
const gchar *time_str,
gpointer user_data)
{
time_t when;
if (g_str_equal(time_str, "now")) {
when = 0;
} else if (g_str_equal(time_str, "never")) {
when = TIME_MAX;
} else if (time_str[0] == '+') {
int seconds;
if (qemu_strtoi(time_str + 1, NULL, 10, &seconds) < 0) {
g_dbus_method_invocation_return_error(
invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
"Invalid time format: %s", time_str);
return TRUE;
}
when = time(NULL) + seconds;
} else {
int64_t epoch;
if (qemu_strtoi64(time_str, NULL, 10, &epoch) < 0) {
g_dbus_method_invocation_return_error(
invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,
"Invalid time format: %s", time_str);
return TRUE;
}
when = epoch;
}
if (vnc_display_pw_expire("default", when) < 0) {
g_dbus_method_invocation_return_error(
invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
"Failed to set password expiry");
return TRUE;
}
qemu_vnc1_server_complete_expire_password(iface, invocation);
return TRUE;
}
static gboolean
on_reload_certificates(QemuVnc1Server *iface,
GDBusMethodInvocation *invocation,
gpointer user_data)
{
Error *err = NULL;
if (!vnc_display_reload_certs("default", &err)) {
g_dbus_method_invocation_return_error(
invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
"%s", error_get_pretty(err));
error_free(err);
return TRUE;
}
qemu_vnc1_server_complete_reload_certificates(iface, invocation);
return TRUE;
}
static gboolean
on_add_client(QemuVnc1Server *iface,
GDBusMethodInvocation *invocation,
GUnixFDList *fd_list,
GVariant *arg_socket,
gboolean skipauth,
gpointer user_data)
{
gint32 handle = g_variant_get_handle(arg_socket);
g_autoptr(GError) err = NULL;
int fd;
fd = g_unix_fd_list_get(fd_list, handle, &err);
if (fd < 0) {
g_dbus_method_invocation_return_error(
invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
"Failed to get fd: %s", err->message);
return TRUE;
}
vnc_display_add_client("default", fd, skipauth);
qemu_vnc1_server_complete_add_client(iface, invocation, NULL);
return TRUE;
}
static void vnc_dbus_add_listeners(VncInfo2 *info)
{
GVariantBuilder builder;
VncServerInfo2List *entry;
g_variant_builder_init(&builder, G_VARIANT_TYPE("aa{sv}"));
for (entry = info->server; entry; entry = entry->next) {
VncServerInfo2 *s = entry->value;
const char *vencrypt_str = "";
if (s->has_vencrypt) {
vencrypt_str = VncVencryptSubAuth_str(s->vencrypt);
}
g_variant_builder_open(&builder, G_VARIANT_TYPE("a{sv}"));
g_variant_builder_add(&builder, "{sv}", "Host",
g_variant_new_string(s->host));
g_variant_builder_add(&builder, "{sv}", "Service",
g_variant_new_string(s->service));
g_variant_builder_add(&builder, "{sv}", "Family",
g_variant_new_string(
NetworkAddressFamily_str(s->family)));
g_variant_builder_add(&builder, "{sv}", "WebSocket",
g_variant_new_boolean(s->websocket));
g_variant_builder_add(&builder, "{sv}", "Auth",
g_variant_new_string(
VncPrimaryAuth_str(s->auth)));
g_variant_builder_add(&builder, "{sv}", "VencryptSubAuth",
g_variant_new_string(vencrypt_str));
g_variant_builder_close(&builder);
}
qemu_vnc1_server_set_listeners(
QEMU_VNC1_SERVER(server_skeleton),
g_variant_builder_end(&builder));
}
void vnc_dbus_setup(GDBusConnection *bus)
{
g_autoptr(GDBusObjectSkeleton) server_obj = NULL;
g_autoptr(VncInfo2List) info_list = NULL;
Error *err = NULL;
const char *auth_str = "none";
const char *vencrypt_str = "";
obj_manager = g_dbus_object_manager_server_new("/org/qemu/Vnc1");
server_skeleton = QEMU_VNC1_SERVER_SKELETON(
qemu_vnc1_server_skeleton_new());
qemu_vnc1_server_set_name(QEMU_VNC1_SERVER(server_skeleton),
qemu_name ? qemu_name : "");
qemu_vnc1_server_set_clients(QEMU_VNC1_SERVER(server_skeleton), NULL);
/* Query auth info from the VNC display */
info_list = qmp_query_vnc_servers(&err);
if (info_list) {
VncInfo2 *info = info_list->value;
auth_str = VncPrimaryAuth_str(info->auth);
if (info->has_vencrypt) {
vencrypt_str = VncVencryptSubAuth_str(info->vencrypt);
}
vnc_dbus_add_listeners(info);
}
qemu_vnc1_server_set_auth(QEMU_VNC1_SERVER(server_skeleton), auth_str);
qemu_vnc1_server_set_vencrypt_sub_auth(
QEMU_VNC1_SERVER(server_skeleton), vencrypt_str);
g_signal_connect(server_skeleton, "handle-set-password",
G_CALLBACK(on_set_password), NULL);
g_signal_connect(server_skeleton, "handle-expire-password",
G_CALLBACK(on_expire_password), NULL);
g_signal_connect(server_skeleton, "handle-reload-certificates",
G_CALLBACK(on_reload_certificates), NULL);
g_signal_connect(server_skeleton, "handle-add-client",
G_CALLBACK(on_add_client), NULL);
server_obj = g_dbus_object_skeleton_new("/org/qemu/Vnc1/Server");
g_dbus_object_skeleton_add_interface(
server_obj, G_DBUS_INTERFACE_SKELETON(server_skeleton));
g_dbus_object_manager_server_export(obj_manager, server_obj);
g_dbus_object_manager_server_set_connection(obj_manager, bus);
if (g_dbus_connection_get_flags(bus)
& G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION) {
g_bus_own_name_on_connection(
bus, "org.qemu.vnc",
G_BUS_NAME_OWNER_FLAGS_NONE,
NULL, NULL, NULL, NULL);
}
}
void vnc_action_shutdown(VncState *vs)
{
VncDbusClient *c;
c = vnc_dbus_find_client(vs->info->host, vs->info->service);
if (!c) {
trace_qemu_vnc_client_not_found(vs->info->host, vs->info->service);
return;
}
qemu_vnc1_client_emit_shutdown_request(QEMU_VNC1_CLIENT(c->skeleton));
}
void vnc_action_reset(VncState *vs)
{
VncDbusClient *c;
c = vnc_dbus_find_client(vs->info->host, vs->info->service);
if (!c) {
trace_qemu_vnc_client_not_found(vs->info->host, vs->info->service);
return;
}
qemu_vnc1_client_emit_reset_request(QEMU_VNC1_CLIENT(c->skeleton));
}
/*
* Override the stub qapi_event_emit() to capture VNC events
* and forward them to the D-Bus interface.
*/
void qapi_event_emit(QAPIEvent event, QDict *qdict)
{
QDict *data, *client;
const char *host, *service, *family;
bool websocket;
if (event != QAPI_EVENT_VNC_CONNECTED &&
event != QAPI_EVENT_VNC_INITIALIZED &&
event != QAPI_EVENT_VNC_DISCONNECTED) {
return;
}
data = qdict_get_qdict(qdict, "data");
if (!data) {
return;
}
client = qdict_get_qdict(data, "client");
if (!client) {
return;
}
host = qdict_get_str(client, "host");
service = qdict_get_str(client, "service");
family = qdict_get_str(client, "family");
websocket = qdict_get_bool(client, "websocket");
switch (event) {
case QAPI_EVENT_VNC_CONNECTED:
vnc_dbus_client_connected(host, service, family, websocket);
break;
case QAPI_EVENT_VNC_INITIALIZED: {
const char *x509_dname = NULL;
const char *sasl_username = NULL;
if (qdict_haskey(client, "x509_dname")) {
x509_dname = qdict_get_str(client, "x509_dname");
}
if (qdict_haskey(client, "sasl_username")) {
sasl_username = qdict_get_str(client, "sasl_username");
}
vnc_dbus_client_initialized(host, service,
x509_dname, sasl_username);
break;
}
case QAPI_EVENT_VNC_DISCONNECTED:
vnc_dbus_client_disconnected(host, service);
break;
default:
break;
}
}
void vnc_dbus_emit_leaving(const char *reason)
{
if (!server_skeleton) {
return;
}
qemu_vnc1_server_emit_leaving(QEMU_VNC1_SERVER(server_skeleton), reason);
}
void vnc_dbus_cleanup(void)
{
VncDbusClient *c, *next;
QTAILQ_FOREACH_SAFE(c, &dbus_clients, next, next) {
g_dbus_object_manager_server_unexport(obj_manager, c->path);
QTAILQ_REMOVE(&dbus_clients, c, next);
g_object_unref(c->skeleton);
g_free(c->path);
g_free(c->host);
g_free(c->service);
g_free(c);
}
g_clear_object(&server_skeleton);
g_clear_object(&obj_manager);
}
+456
View File
@@ -0,0 +1,456 @@
/*
* D-Bus display listener — scanout, update and cursor handling.
*
* Copyright (C) 2026 Red Hat, Inc.
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "qemu/osdep.h"
#include "qemu/sockets.h"
#include "qemu/error-report.h"
#include "ui/console-priv.h"
#include "ui/dbus-display1.h"
#include "ui/surface.h"
#include "trace.h"
#include "qemu-vnc.h"
typedef struct ConsoleData {
QemuDBusDisplay1Console *console_proxy;
QemuDBusDisplay1Keyboard *keyboard_proxy;
QemuDBusDisplay1Mouse *mouse_proxy;
QemuGraphicConsole *gfx_con;
GDBusConnection *listener_conn;
/*
* When true the surface is backed by a read-only mmap (ScanoutMap path)
* and Update messages must be rejected because compositing into the
* surface is not possible. The plain Scanout path provides a writable
* copy and clears this flag.
*/
bool read_only;
} ConsoleData;
static void display_ui_info(void *opaque, uint32_t head, QemuUIInfo *info)
{
ConsoleData *cd = opaque;
g_autoptr(GError) err = NULL;
if (!cd || !cd->console_proxy) {
return;
}
qemu_dbus_display1_console_call_set_uiinfo_sync(
cd->console_proxy,
info->width_mm, info->height_mm,
info->xoff, info->yoff,
info->width, info->height,
G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err);
if (err) {
error_report("SetUIInfo failed: %s", err->message);
}
}
static void
scanout_image_destroy(pixman_image_t *image, void *data)
{
g_variant_unref(data);
}
typedef struct {
void *addr;
size_t len;
} ScanoutMapData;
static void
scanout_map_destroy(pixman_image_t *image, void *data)
{
ScanoutMapData *map = data;
munmap(map->addr, map->len);
g_free(map);
}
static gboolean
on_scanout(QemuDBusDisplay1Listener *listener,
GDBusMethodInvocation *invocation,
guint width, guint height, guint stride,
guint pixman_format, GVariant *data,
gpointer user_data)
{
ConsoleData *cd = user_data;
QemuConsole *con = QEMU_CONSOLE(cd->gfx_con);
gsize size;
const uint8_t *pixels;
pixman_image_t *image;
DisplaySurface *surface;
trace_qemu_vnc_scanout(width, height, stride, pixman_format);
pixels = g_variant_get_fixed_array(data, &size, 1);
image = pixman_image_create_bits((pixman_format_code_t)pixman_format,
width, height, (uint32_t *)pixels, stride);
assert(image);
g_variant_ref(data);
pixman_image_set_destroy_function(image, scanout_image_destroy, data);
cd->read_only = false;
surface = qemu_create_displaysurface_pixman(image);
qemu_console_set_surface(con, surface);
qemu_dbus_display1_listener_complete_scanout(listener, invocation);
return DBUS_METHOD_INVOCATION_HANDLED;
}
static gboolean
on_update(QemuDBusDisplay1Listener *listener,
GDBusMethodInvocation *invocation,
gint x, gint y, gint w, gint h,
guint stride, guint pixman_format, GVariant *data,
gpointer user_data)
{
ConsoleData *cd = user_data;
QemuConsole *con = QEMU_CONSOLE(cd->gfx_con);
DisplaySurface *surface = qemu_console_surface(con);
gsize size;
const uint8_t *pixels;
pixman_image_t *src;
trace_qemu_vnc_update(x, y, w, h, stride, pixman_format);
if (!surface || cd->read_only) {
g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
G_DBUS_ERROR_FAILED, "No active or writable console");
return DBUS_METHOD_INVOCATION_HANDLED;
}
pixels = g_variant_get_fixed_array(data, &size, 1);
src = pixman_image_create_bits((pixman_format_code_t)pixman_format,
w, h, (uint32_t *)pixels, stride);
assert(src);
pixman_image_composite(PIXMAN_OP_SRC, src, NULL,
surface->image,
0, 0, 0, 0, x, y, w, h);
pixman_image_unref(src);
qemu_console_update(con, x, y, w, h);
qemu_dbus_display1_listener_complete_update(listener, invocation);
return DBUS_METHOD_INVOCATION_HANDLED;
}
static gboolean
on_scanout_map(QemuDBusDisplay1ListenerUnixMap *listener,
GDBusMethodInvocation *invocation,
GUnixFDList *fd_list,
GVariant *arg_handle,
guint offset, guint width, guint height,
guint stride, guint pixman_format,
gpointer user_data)
{
ConsoleData *cd = user_data;
gint32 handle = g_variant_get_handle(arg_handle);
g_autoptr(GError) err = NULL;
DisplaySurface *surface;
int fd;
void *addr;
size_t len = (size_t)height * stride;
pixman_image_t *image;
trace_qemu_vnc_scanout_map(width, height, stride, pixman_format, offset);
fd = g_unix_fd_list_get(fd_list, handle, &err);
if (fd < 0) {
g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
G_DBUS_ERROR_FAILED, "Failed to get fd: %s", err->message);
return DBUS_METHOD_INVOCATION_HANDLED;
}
/* MAP_PRIVATE: we only read; avoid propagating writes back to QEMU */
addr = mmap(NULL, len, PROT_READ, MAP_PRIVATE, fd, offset);
close(fd);
if (addr == MAP_FAILED) {
g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,
G_DBUS_ERROR_FAILED, "mmap failed: %s", g_strerror(errno));
return DBUS_METHOD_INVOCATION_HANDLED;
}
image = pixman_image_create_bits((pixman_format_code_t)pixman_format,
width, height, addr, stride);
assert(image);
{
ScanoutMapData *map = g_new0(ScanoutMapData, 1);
map->addr = addr;
map->len = len;
pixman_image_set_destroy_function(image, scanout_map_destroy, map);
}
cd->read_only = true;
surface = qemu_create_displaysurface_pixman(image);
qemu_console_set_surface(QEMU_CONSOLE(cd->gfx_con), surface);
qemu_dbus_display1_listener_unix_map_complete_scanout_map(
listener, invocation, NULL);
return DBUS_METHOD_INVOCATION_HANDLED;
}
static gboolean
on_update_map(QemuDBusDisplay1ListenerUnixMap *listener,
GDBusMethodInvocation *invocation,
guint x, guint y, guint w, guint h,
gpointer user_data)
{
ConsoleData *cd = user_data;
trace_qemu_vnc_update_map(x, y, w, h);
qemu_console_update(QEMU_CONSOLE(cd->gfx_con), x, y, w, h);
qemu_dbus_display1_listener_unix_map_complete_update_map(
listener, invocation);
return DBUS_METHOD_INVOCATION_HANDLED;
}
static gboolean
on_cursor_define(QemuDBusDisplay1Listener *listener,
GDBusMethodInvocation *invocation,
gint width, gint height,
gint hot_x, gint hot_y,
GVariant *data,
gpointer user_data)
{
ConsoleData *cd = user_data;
gsize size;
const uint8_t *pixels;
QEMUCursor *c;
trace_qemu_vnc_cursor_define(width, height, hot_x, hot_y);
c = cursor_alloc(width, height);
if (!c) {
qemu_dbus_display1_listener_complete_cursor_define(
listener, invocation);
return DBUS_METHOD_INVOCATION_HANDLED;
}
c->hot_x = hot_x;
c->hot_y = hot_y;
pixels = g_variant_get_fixed_array(data, &size, 1);
memcpy(c->data, pixels, MIN(size, (gsize)width * height * 4));
qemu_console_set_cursor(QEMU_CONSOLE(cd->gfx_con), c);
cursor_unref(c);
qemu_dbus_display1_listener_complete_cursor_define(
listener, invocation);
return DBUS_METHOD_INVOCATION_HANDLED;
}
typedef struct {
GMainLoop *loop;
GThread *thread;
GDBusConnection *listener_conn;
} ListenerSetupData;
static void
on_register_listener_finished(GObject *source_object,
GAsyncResult *res,
gpointer user_data)
{
ListenerSetupData *data = user_data;
g_autoptr(GError) err = NULL;
qemu_dbus_display1_console_call_register_listener_finish(
QEMU_DBUS_DISPLAY1_CONSOLE(source_object),
NULL,
res, &err);
if (err) {
error_report("RegisterListener failed: %s", err->message);
g_main_loop_quit(data->loop);
return;
}
data->listener_conn = g_thread_join(data->thread);
g_main_loop_quit(data->loop);
}
static GDBusConnection *
console_register_display_listener(QemuDBusDisplay1Console *console)
{
g_autoptr(GError) err = NULL;
g_autoptr(GMainLoop) loop = NULL;
g_autoptr(GUnixFDList) fd_list = NULL;
ListenerSetupData data = { 0 };
int pair[2];
int idx;
if (qemu_socketpair(AF_UNIX, SOCK_STREAM, 0, pair) < 0) {
error_report("socketpair failed: %s", strerror(errno));
return NULL;
}
fd_list = g_unix_fd_list_new();
idx = g_unix_fd_list_append(fd_list, pair[1], &err);
close(pair[1]);
if (idx < 0) {
close(pair[0]);
error_report("Failed to append fd: %s", err->message);
return NULL;
}
loop = g_main_loop_new(NULL, FALSE);
data.loop = loop;
data.thread = p2p_dbus_thread_new(pair[0]);
qemu_dbus_display1_console_call_register_listener(
console,
g_variant_new_handle(idx),
G_DBUS_CALL_FLAGS_NONE,
-1,
fd_list,
NULL,
on_register_listener_finished,
&data);
g_main_loop_run(loop);
return data.listener_conn;
}
static void
setup_display_listener(ConsoleData *cd)
{
g_autoptr(GDBusObjectSkeleton) obj = NULL;
GDBusObjectManagerServer *server;
QemuDBusDisplay1Listener *iface;
QemuDBusDisplay1ListenerUnixMap *iface_map;
server = g_dbus_object_manager_server_new(DBUS_DISPLAY1_ROOT);
obj = g_dbus_object_skeleton_new(DBUS_DISPLAY1_ROOT "/Listener");
/* Main listener interface */
iface = qemu_dbus_display1_listener_skeleton_new();
g_object_connect(iface,
"signal::handle-scanout", on_scanout, cd,
"signal::handle-update", on_update, cd,
"signal::handle-cursor-define", on_cursor_define, cd,
NULL);
g_dbus_object_skeleton_add_interface(obj,
G_DBUS_INTERFACE_SKELETON(iface));
/* Unix shared memory map interface */
iface_map = qemu_dbus_display1_listener_unix_map_skeleton_new();
g_object_connect(iface_map,
"signal::handle-scanout-map", on_scanout_map, cd,
"signal::handle-update-map", on_update_map, cd,
NULL);
g_dbus_object_skeleton_add_interface(obj,
G_DBUS_INTERFACE_SKELETON(iface_map));
{
const gchar *ifaces[] = {
"org.qemu.Display1.Listener.Unix.Map", NULL
};
g_object_set(iface, "interfaces", ifaces, NULL);
}
g_dbus_object_manager_server_export(server, obj);
g_dbus_object_manager_server_set_connection(server,
cd->listener_conn);
g_dbus_connection_start_message_processing(cd->listener_conn);
}
static const GraphicHwOps vnc_hw_ops = {
.ui_info = display_ui_info,
};
bool console_setup(GDBusConnection *bus, const char *bus_name,
const char *console_path)
{
g_autoptr(GError) err = NULL;
ConsoleData *cd;
QemuConsole *con;
cd = g_new0(ConsoleData, 1);
cd->console_proxy = qemu_dbus_display1_console_proxy_new_sync(
bus, G_DBUS_PROXY_FLAGS_NONE, bus_name,
console_path, NULL, &err);
if (!cd->console_proxy) {
error_report("Failed to create console proxy for %s: %s",
console_path, err->message);
g_free(cd);
return false;
}
cd->keyboard_proxy = QEMU_DBUS_DISPLAY1_KEYBOARD(
qemu_dbus_display1_keyboard_proxy_new_sync(
bus, G_DBUS_PROXY_FLAGS_NONE, bus_name,
console_path, NULL, &err));
if (!cd->keyboard_proxy) {
error_report("Failed to create keyboard proxy for %s: %s",
console_path, err->message);
g_object_unref(cd->console_proxy);
g_free(cd);
return false;
}
g_clear_error(&err);
cd->mouse_proxy = QEMU_DBUS_DISPLAY1_MOUSE(
qemu_dbus_display1_mouse_proxy_new_sync(
bus, G_DBUS_PROXY_FLAGS_NONE, bus_name,
console_path, NULL, &err));
if (!cd->mouse_proxy) {
error_report("Failed to create mouse proxy for %s: %s",
console_path, err->message);
g_object_unref(cd->keyboard_proxy);
g_object_unref(cd->console_proxy);
g_free(cd);
return false;
}
con = qemu_graphic_console_create(NULL, 0, &vnc_hw_ops, cd);
cd->gfx_con = QEMU_GRAPHIC_CONSOLE(con);
cd->listener_conn = console_register_display_listener(
cd->console_proxy);
if (!cd->listener_conn) {
error_report("Failed to setup D-Bus listener for %s",
console_path);
g_object_unref(cd->mouse_proxy);
g_object_unref(cd->keyboard_proxy);
g_object_unref(cd->console_proxy);
g_free(cd);
return false;
}
setup_display_listener(cd);
input_setup(cd->keyboard_proxy, cd->mouse_proxy);
return true;
}
QemuDBusDisplay1Keyboard *console_get_keyboard(const QemuConsole *con)
{
ConsoleData *cd;
if (!QEMU_IS_GRAPHIC_CONSOLE(con)) {
return NULL;
}
cd = con->hw;
return cd ? cd->keyboard_proxy : NULL;
}
QemuDBusDisplay1Mouse *console_get_mouse(const QemuConsole *con)
{
ConsoleData *cd;
if (!QEMU_IS_GRAPHIC_CONSOLE(con)) {
return NULL;
}
cd = con->hw;
return cd ? cd->mouse_proxy : NULL;
}
+225
View File
@@ -0,0 +1,225 @@
/*
* Keyboard and mouse input dispatch via D-Bus.
*
* Copyright (C) 2026 Red Hat, Inc.
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "qemu/osdep.h"
#include "ui/dbus-display1.h"
#include "ui/input.h"
#include "trace.h"
#include "qemu-vnc.h"
static NotifierList mouse_mode_notifiers =
NOTIFIER_LIST_INITIALIZER(mouse_mode_notifiers);
static NotifierList led_notifiers =
NOTIFIER_LIST_INITIALIZER(led_notifiers);
/* Track the target console for pending mouse events (used by sync) */
static QemuConsole *mouse_target;
/*
* The D-Bus Keyboard.Modifiers property uses the same
* bit layout as QEMU's LED constants.
*/
static guint modifiers;
void qemu_input_led_notifier_add(Notifier *n)
{
notifier_list_add(&led_notifiers, n);
}
void qemu_input_led_notifier_remove(Notifier *n)
{
notifier_remove(n);
}
uint32_t qemu_input_get_leds_mask(const QemuConsole *con)
{
return modifiers;
}
static void
on_keyboard_modifiers_changed(GObject *gobject, GParamSpec *pspec,
gpointer user_data)
{
modifiers = qemu_dbus_display1_keyboard_get_modifiers(
QEMU_DBUS_DISPLAY1_KEYBOARD(gobject));
notifier_list_notify(&led_notifiers, NULL);
}
void qemu_add_mouse_mode_change_notifier(Notifier *notify)
{
notifier_list_add(&mouse_mode_notifiers, notify);
}
void qemu_remove_mouse_mode_change_notifier(Notifier *notify)
{
notifier_remove(notify);
}
void qemu_input_event_send_key_delay(uint32_t delay_ms)
{
}
void qemu_input_event_send_key_linux(QemuConsole *src, unsigned int lnx,
bool down)
{
QemuDBusDisplay1Keyboard *kbd;
guint qnum;
trace_qemu_vnc_key_event(lnx, down);
if (!src) {
return;
}
kbd = console_get_keyboard(src);
if (!kbd) {
return;
}
if (lnx >= qemu_input_map_linux_to_qnum_len) {
return;
}
qnum = qemu_input_map_linux_to_qnum[lnx];
if (down) {
qemu_dbus_display1_keyboard_call_press(
kbd, qnum,
G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
} else {
qemu_dbus_display1_keyboard_call_release(
kbd, qnum,
G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
}
}
static guint abs_x, abs_y;
static bool abs_pending;
static gint rel_dx, rel_dy;
static bool rel_pending;
void qemu_input_queue_abs(QemuConsole *src, InputAxis axis,
int value, int min_in, int max_in)
{
if (axis == INPUT_AXIS_X) {
abs_x = value;
} else if (axis == INPUT_AXIS_Y) {
abs_y = value;
}
abs_pending = true;
mouse_target = src;
}
void qemu_input_queue_rel(QemuConsole *src, InputAxis axis, int value)
{
if (axis == INPUT_AXIS_X) {
rel_dx += value;
} else if (axis == INPUT_AXIS_Y) {
rel_dy += value;
}
rel_pending = true;
mouse_target = src;
}
void qemu_input_event_sync(void)
{
QemuDBusDisplay1Mouse *mouse;
if (!mouse_target) {
return;
}
mouse = console_get_mouse(mouse_target);
if (!mouse) {
abs_pending = false;
rel_pending = false;
return;
}
if (abs_pending) {
trace_qemu_vnc_input_abs(abs_x, abs_y);
abs_pending = false;
qemu_dbus_display1_mouse_call_set_abs_position(
mouse, abs_x, abs_y,
G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
}
if (rel_pending) {
trace_qemu_vnc_input_rel(rel_dx, rel_dy);
rel_pending = false;
qemu_dbus_display1_mouse_call_rel_motion(
mouse, rel_dx, rel_dy,
G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
rel_dx = 0;
rel_dy = 0;
}
}
bool qemu_input_is_absolute(const QemuConsole *con)
{
QemuDBusDisplay1Mouse *mouse;
if (!con) {
return false;
}
mouse = console_get_mouse(con);
if (!mouse) {
return false;
}
return qemu_dbus_display1_mouse_get_is_absolute(mouse);
}
static void
on_mouse_is_absolute_changed(GObject *gobject, GParamSpec *pspec,
gpointer user_data)
{
notifier_list_notify(&mouse_mode_notifiers, NULL);
}
void qemu_input_update_buttons(QemuConsole *src, uint32_t *button_map,
uint32_t button_old, uint32_t button_new)
{
QemuDBusDisplay1Mouse *mouse;
uint32_t changed;
int i;
if (!src) {
return;
}
mouse = console_get_mouse(src);
if (!mouse) {
return;
}
changed = button_old ^ button_new;
for (i = 0; i < 32; i++) {
if (!(changed & (1u << i))) {
continue;
}
trace_qemu_vnc_input_btn(i, !!(button_new & (1u << i)));
if (button_new & (1u << i)) {
qemu_dbus_display1_mouse_call_press(
mouse, i,
G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
} else {
qemu_dbus_display1_mouse_call_release(
mouse, i,
G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);
}
}
}
void input_setup(QemuDBusDisplay1Keyboard *kbd,
QemuDBusDisplay1Mouse *mouse)
{
g_signal_connect(kbd, "notify::modifiers",
G_CALLBACK(on_keyboard_modifiers_changed), NULL);
g_signal_connect(mouse, "notify::is-absolute",
G_CALLBACK(on_mouse_is_absolute_changed), NULL);
}
+26
View File
@@ -0,0 +1,26 @@
vnca = vnc_ss.apply({}, strict: false)
qemu_vnc1 = custom_target('qemu-vnc1 gdbus-codegen',
output: ['qemu-vnc1.h', 'qemu-vnc1.c'],
input: files('qemu-vnc1.xml'),
command: [gdbus_codegen, '@INPUT@',
'--glib-min-required', '2.64',
'--output-directory', meson.current_build_dir(),
'--interface-prefix', 'org.qemu.',
'--c-namespace', 'Qemu',
'--generate-c-code', '@BASENAME@'])
qemu_vnc = executable('qemu-vnc',
sources: ['qemu-vnc.c', 'display.c', 'input.c',
'audio.c', 'chardev.c', 'clipboard.c', 'console.c',
'dbus.c', 'stubs.c', 'utils.c',
vnca.sources(), dbus_display1, qemu_vnc1],
dependencies: [vnca.dependencies(), io, crypto, qemuutil, gio, ui])
# The executable lives in a subdirectory of the build tree, but
# get_relocated_path() looks for qemu-bundle relative to the binary.
# Create a symlink so that firmware/keymap lookup works during development.
run_command('ln', '-sfn',
'../../qemu-bundle',
meson.current_build_dir() / 'qemu-bundle',
check: false)
+581
View File
@@ -0,0 +1,581 @@
/*
* Standalone VNC server connecting to QEMU via D-Bus display interface.
*
* Copyright (C) 2026 Red Hat, Inc.
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "qemu/osdep.h"
#include "qemu/cutils.h"
#include "qemu/datadir.h"
#include "qemu/error-report.h"
#include "qemu/config-file.h"
#include "qemu/option.h"
#include "qemu/log.h"
#include "qemu/main-loop.h"
#include "qemu-version.h"
#include "ui/vnc.h"
#include "crypto/secret.h"
#include "crypto/tlscredsx509.h"
#include "qom/object_interfaces.h"
#include "trace.h"
#include "qemu-vnc.h"
const char *qemu_name;
const char *keyboard_layout;
typedef struct {
GDBusConnection *bus;
const char *bus_name;
const char * const *chardev_names;
char *terminate_reason;
bool no_vt;
bool terminate;
bool owner_seen;
bool wait_for_owner;
} QemuVncState;
static GType
dbus_display_get_proxy_type(GDBusObjectManagerClient *manager,
const gchar *object_path,
const gchar *interface_name,
gpointer user_data)
{
static const struct {
const char *iface;
GType (*get_type)(void);
} types[] = {
{ "org.qemu.Display1.Clipboard",
qemu_dbus_display1_clipboard_proxy_get_type },
{ "org.qemu.Display1.Audio",
qemu_dbus_display1_audio_proxy_get_type },
{ "org.qemu.Display1.Chardev",
qemu_dbus_display1_chardev_proxy_get_type },
{ "org.qemu.Display1.Chardev.VCEncoding",
qemu_dbus_display1_chardev_vcencoding_proxy_get_type },
};
if (!interface_name) {
return G_TYPE_DBUS_OBJECT_PROXY;
}
for (int i = 0; i < G_N_ELEMENTS(types); i++) {
if (g_str_equal(interface_name, types[i].iface)) {
return types[i].get_type();
}
}
return G_TYPE_DBUS_PROXY;
}
static void
on_bus_closed(GDBusConnection *connection,
gboolean remote_peer_vanished,
GError *error,
gpointer user_data)
{
QemuVncState *state = user_data;
state->terminate_reason = g_strdup("D-Bus connection closed");
state->terminate = true;
qemu_notify_event();
}
static void
on_manager_ready(GObject *source_object,
GAsyncResult *res,
gpointer user_data)
{
QemuVncState *state = user_data;
g_autoptr(GError) err = NULL;
g_autoptr(GDBusObjectManager) manager = NULL;
GList *objects, *l;
g_autoptr(GPtrArray) console_paths = NULL;
bool found = false;
Error *local_err = NULL;
manager = G_DBUS_OBJECT_MANAGER(
g_dbus_object_manager_client_new_finish(res, &err));
if (!manager) {
error_report("Failed to create object manager: %s",
err->message);
g_assert_not_reached();
return;
}
/*
* Discover all Console objects and sort them so that console
* indices are assigned in a predictable order matching QEMU's.
*/
console_paths = g_ptr_array_new_with_free_func(g_free);
objects = g_dbus_object_manager_get_objects(manager);
for (l = objects; l; l = l->next) {
GDBusObject *obj = l->data;
const char *path = g_dbus_object_get_object_path(obj);
if (g_str_has_prefix(path, DBUS_DISPLAY1_ROOT "/Console_")) {
g_ptr_array_add(console_paths, g_strdup(path));
}
}
g_list_free_full(objects, g_object_unref);
g_ptr_array_sort(console_paths, (GCompareFunc)qemu_pstrcmp0);
for (guint i = 0; i < console_paths->len; i++) {
const char *path = g_ptr_array_index(console_paths, i);
if (!console_setup(state->bus, state->bus_name, path)) {
error_report("Failed to setup console %s", path);
continue;
}
found = true;
}
if (!found) {
error_report("No consoles found");
state->terminate_reason = g_strdup("No consoles found");
state->terminate = true;
qemu_notify_event();
return;
}
/*
* Create the VNC display now that consoles exist, so that the
* display change listener is registered against a valid console.
*/
if (!vnc_display_new("default", &local_err)) {
error_report("Failed to create VNC display: %s",
error_get_pretty(local_err));
g_assert_not_reached();
return;
}
vnc_dbus_setup(state->bus);
clipboard_setup(manager, state->bus);
audio_setup(manager);
if (!state->no_vt) {
chardev_setup(state->chardev_names, manager);
}
}
static void
start_display_setup(QemuVncState *state)
{
g_autoptr(QemuDBusDisplay1VMProxy) vm_proxy =
QEMU_DBUS_DISPLAY1_VM_PROXY(
qemu_dbus_display1_vm_proxy_new_sync(
state->bus, G_DBUS_PROXY_FLAGS_NONE,
state->bus_name,
DBUS_DISPLAY1_ROOT "/VM", NULL, NULL));
if (vm_proxy) {
qemu_name = g_strdup(qemu_dbus_display1_vm_get_name(
QEMU_DBUS_DISPLAY1_VM(vm_proxy)));
}
g_dbus_object_manager_client_new(
state->bus,
G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_NONE,
state->bus_name, DBUS_DISPLAY1_ROOT,
dbus_display_get_proxy_type,
NULL, NULL, NULL,
on_manager_ready, state);
}
static void
on_owner_appeared(GDBusConnection *connection,
const gchar *name,
const gchar *name_owner,
gpointer user_data)
{
QemuVncState *state = user_data;
if (state->owner_seen) {
return;
}
info_report("D-Bus name %s appeared.", name);
state->owner_seen = true;
trace_qemu_vnc_owner_appeared(name);
start_display_setup(state);
}
static void
on_owner_vanished(GDBusConnection *connection,
const gchar *name,
gpointer user_data)
{
QemuVncState *state = user_data;
trace_qemu_vnc_owner_vanished(name);
if (!state->owner_seen) {
if (state->wait_for_owner) {
return;
}
error_report("D-Bus name %s not found. "
"Is QEMU running? "
"Use --wait to wait for it to appear.", name);
state->terminate_reason =
g_strdup_printf("D-Bus name %s not found", name);
} else {
error_report("D-Bus peer %s vanished, terminating", name);
state->terminate_reason =
g_strdup_printf("D-Bus peer %s vanished", name);
}
state->terminate = true;
qemu_notify_event();
}
static GDBusConnection *
setup_dbus_connection(int dbus_p2p_fd, const char *dbus_address,
char **bus_name)
{
g_autoptr(GError) err = NULL;
GDBusConnection *bus;
if (dbus_p2p_fd >= 0) {
g_autoptr(GSocket) socket = NULL;
g_autoptr(GSocketConnection) socketc = NULL;
if (*bus_name) {
error_report("--bus-name is not supported with --dbus-p2p-fd");
return NULL;
}
socket = g_socket_new_from_fd(dbus_p2p_fd, &err);
if (!socket) {
error_report("Failed to create socket from fd %d: %s",
dbus_p2p_fd, err->message);
return NULL;
}
socketc = g_socket_connection_factory_create_connection(socket);
if (!socketc) {
error_report("Failed to create socket connection");
return NULL;
}
bus = g_dbus_connection_new_sync(
G_IO_STREAM(socketc), NULL,
G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT,
NULL, NULL, &err);
} else if (dbus_address) {
GDBusConnectionFlags flags =
G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT;
if (*bus_name) {
flags |= G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION;
}
bus = g_dbus_connection_new_for_address_sync(
dbus_address, flags, NULL, NULL, &err);
} else {
bus = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &err);
if (!*bus_name) {
*bus_name = g_strdup("org.qemu");
}
}
if (!bus) {
error_report("Failed to connect to D-Bus: %s", err->message);
}
return bus;
}
static bool
setup_credentials(const char *tls_creds_dir, const char *tls_authz,
bool *has_vnc_password)
{
Error *local_err = NULL;
const char *creds_dir;
/*
* Set up TLS credentials if requested. The object must exist
* before vnc_display_open() which looks it up by ID.
*/
if (tls_creds_dir) {
if (!object_new_with_props(TYPE_QCRYPTO_TLS_CREDS_X509,
object_get_objects_root(),
"tlscreds0",
&local_err,
"endpoint", "server",
"dir", tls_creds_dir,
"verify-peer", tls_authz ? "yes" : "no",
NULL)) {
error_report_err(local_err);
return false;
}
}
/*
* Check for systemd credentials: if a vnc-password credential
* file exists, create a QCryptoSecret and enable VNC password auth.
*/
creds_dir = g_getenv("CREDENTIALS_DIRECTORY");
if (creds_dir) {
g_autofree char *password_path =
g_build_filename(creds_dir, "vnc-password", NULL);
if (g_file_test(password_path, G_FILE_TEST_EXISTS)) {
if (!object_new_with_props(TYPE_QCRYPTO_SECRET,
object_get_objects_root(),
"vncsecret0",
&local_err,
"file", password_path,
NULL)) {
error_report_err(local_err);
return false;
}
*has_vnc_password = true;
}
}
return true;
}
static bool
setup_vnc_opts(const char *vnc_addr, const char *tls_creds_dir,
const char *tls_authz, bool sasl, const char *sasl_authz,
bool has_vnc_password, const char *ws_addr,
const char *share, bool password, bool lossy,
bool non_adaptive)
{
g_autoptr(GString) opts_str = g_string_new(vnc_addr);
QemuOptsList *olist = qemu_find_opts("vnc");
QemuOpts *opts;
if (tls_creds_dir) {
g_string_append(opts_str, ",tls-creds=tlscreds0");
}
if (tls_authz) {
g_string_append_printf(opts_str, ",tls-authz=%s", tls_authz);
}
if (sasl) {
g_string_append(opts_str, ",sasl=on");
}
if (sasl_authz) {
g_string_append_printf(opts_str, ",sasl-authz=%s", sasl_authz);
}
if (has_vnc_password) {
g_string_append(opts_str, ",password-secret=vncsecret0");
}
if (ws_addr) {
g_string_append_printf(opts_str, ",websocket=%s", ws_addr);
}
if (share) {
g_string_append_printf(opts_str, ",share=%s", share);
}
if (password && !has_vnc_password) {
g_string_append(opts_str, ",password=on");
}
if (lossy) {
g_string_append(opts_str, ",lossy=on");
}
if (non_adaptive) {
g_string_append(opts_str, ",non-adaptive=on");
}
opts = qemu_opts_parse_noisily(olist, opts_str->str, true);
if (!opts) {
return false;
}
qemu_opts_set_id(opts, g_strdup("default"));
return true;
}
int
main(int argc, char *argv[])
{
g_autoptr(GError) err = NULL;
g_autoptr(GDBusConnection) bus = NULL;
g_autofree char *dbus_address = NULL;
g_autofree char *bus_name = NULL;
int dbus_p2p_fd = -1;
g_autofree char *vnc_addr = NULL;
g_autofree char *ws_addr = NULL;
g_autofree char *share = NULL;
g_autofree char *tls_creds_dir = NULL;
g_autofree char *tls_authz = NULL;
g_autofree char *sasl_authz = NULL;
g_autofree char *trace_opt = NULL;
g_auto(GStrv) chardev_names = NULL;
g_auto(GStrv) object_strs = NULL;
QemuVncState state = { 0 };
bool has_vnc_password = false;
bool show_version = false;
bool no_vt = false;
bool wait_for_owner = false;
bool password = false;
bool sasl = false;
bool lossy = false;
bool non_adaptive = false;
g_autoptr(GOptionContext) context = NULL;
GOptionEntry entries[] = {
{ "dbus-address", 'a', 0, G_OPTION_ARG_STRING, &dbus_address,
"D-Bus address to connect to (default: session bus)", "ADDRESS" },
{ "dbus-p2p-fd", 'p', 0, G_OPTION_ARG_INT, &dbus_p2p_fd,
"D-Bus peer-to-peer socket file descriptor", "FD" },
{ "bus-name", 'n', 0, G_OPTION_ARG_STRING, &bus_name,
"D-Bus bus name (default: org.qemu)", "NAME" },
{ "wait", 'W', 0, G_OPTION_ARG_NONE, &wait_for_owner,
"Wait for the D-Bus name to appear", NULL },
{ "vnc-addr", 'l', 0, G_OPTION_ARG_STRING, &vnc_addr,
"VNC display address (default localhost:0, \"none\" to disable)",
"ADDR" },
{ "websocket", 'w', 0, G_OPTION_ARG_STRING, &ws_addr,
"WebSocket address (e.g. port number or addr:port)", "ADDR" },
{ "share", 's', 0, G_OPTION_ARG_STRING, &share,
"Display sharing policy "
"(allow-exclusive|force-shared|ignore)", "POLICY" },
{ "tls-creds", 't', 0, G_OPTION_ARG_STRING, &tls_creds_dir,
"TLS x509 credentials directory", "DIR" },
{ "tls-authz", 0, 0, G_OPTION_ARG_STRING, &tls_authz,
"ID of a QAuthZ object for TLS client certificate "
"authorization", "ID" },
{ "object", 'O', 0, G_OPTION_ARG_STRING_ARRAY, &object_strs,
"QEMU user-creatable object "
"(e.g. authz-list-file,id=auth0,filename=acl.json)", "OBJDEF" },
{ "vt-chardev", 'C', 0, G_OPTION_ARG_STRING_ARRAY, &chardev_names,
"Chardev type names to expose as text console (repeatable, "
"default: serial & hmp)", "NAME" },
{ "no-vt", 'N', 0, G_OPTION_ARG_NONE, &no_vt,
"Do not expose any chardevs as text consoles", NULL },
{ "keyboard-layout", 'k', 0, G_OPTION_ARG_STRING, &keyboard_layout,
"Keyboard layout", "LAYOUT" },
{ "trace", 'T', 0, G_OPTION_ARG_STRING, &trace_opt,
"Trace options (same as QEMU -trace)", "PATTERN" },
{ "version", 'V', 0, G_OPTION_ARG_NONE, &show_version,
"Print version information and exit", NULL },
{ "password", 0, 0, G_OPTION_ARG_NONE, &password,
"Require password authentication (use D-Bus SetPassword to set)",
NULL },
{ "lossy", 0, 0, G_OPTION_ARG_NONE, &lossy,
"Enable lossy compression", NULL },
{ "non-adaptive", 0, 0, G_OPTION_ARG_NONE, &non_adaptive,
"Disable adaptive encodings", NULL },
{ "sasl", 0, 0, G_OPTION_ARG_NONE, &sasl,
"Enable SASL authentication", NULL },
{ "sasl-authz", 0, 0, G_OPTION_ARG_STRING, &sasl_authz,
"ID of a QAuthZ object for SASL username "
"authorization", "ID" },
{ NULL }
};
qemu_init_exec_dir(argv[0]);
qemu_add_data_dir(g_strdup(CONFIG_QEMU_DATADIR));
qemu_add_data_dir(get_relocated_path(CONFIG_QEMU_DATADIR));
module_call_init(MODULE_INIT_TRACE);
module_call_init(MODULE_INIT_QOM);
module_call_init(MODULE_INIT_OPTS);
qemu_add_opts(&qemu_trace_opts);
context = g_option_context_new(NULL);
g_option_context_set_summary(context,
"Standalone VNC server connecting to a QEMU instance via the\n"
"D-Bus display interface (org.qemu.Display1).");
g_option_context_add_main_entries(context, entries, NULL);
if (!g_option_context_parse(context, &argc, &argv, &err)) {
error_report("Option parsing failed: %s", err->message);
return 1;
}
if (show_version) {
printf("qemu-vnc " QEMU_FULL_VERSION "\n");
return 0;
}
if (trace_opt) {
trace_opt_parse(trace_opt);
qemu_set_log(LOG_TRACE, &error_fatal);
}
trace_init_file();
qemu_init_main_loop(&error_fatal);
if (!vnc_addr) {
vnc_addr = g_strdup("localhost:0");
}
if (object_strs) {
for (int i = 0; object_strs[i]; i++) {
user_creatable_process_cmdline(object_strs[i]);
}
}
if (tls_authz && !tls_creds_dir) {
error_report("--tls-authz requires --tls-creds");
return 1;
}
if (sasl_authz && !sasl) {
error_report("--sasl-authz requires --sasl");
return 1;
}
if (dbus_p2p_fd >= 0 && dbus_address) {
error_report("--dbus-p2p-fd and --dbus-address are"
" mutually exclusive");
return 1;
}
if (wait_for_owner && dbus_p2p_fd >= 0) {
error_report("--wait is not supported with --dbus-p2p-fd");
return 1;
}
bus = setup_dbus_connection(dbus_p2p_fd, dbus_address, &bus_name);
if (!bus) {
return 1;
}
if (wait_for_owner && !bus_name) {
error_report("--wait requires a D-Bus bus name (--bus-name)");
return 1;
}
if (!setup_credentials(tls_creds_dir, tls_authz, &has_vnc_password)) {
return 1;
}
if (!setup_vnc_opts(vnc_addr, tls_creds_dir, tls_authz, sasl, sasl_authz,
has_vnc_password, ws_addr, share, password, lossy,
non_adaptive)) {
return 1;
}
state.bus = bus;
state.bus_name = bus_name;
state.chardev_names = (const char * const *)chardev_names;
state.no_vt = no_vt;
state.wait_for_owner = wait_for_owner;
g_signal_connect(bus, "closed", G_CALLBACK(on_bus_closed), &state);
if (bus_name) {
if (wait_for_owner) {
info_report("Waiting for D-Bus name %s to appear...", bus_name);
}
g_bus_watch_name_on_connection(bus, bus_name,
G_BUS_NAME_WATCHER_FLAGS_NONE,
on_owner_appeared,
on_owner_vanished,
&state, NULL);
} else {
state.owner_seen = true;
start_display_setup(&state);
}
while (!state.terminate) {
main_loop_wait(false);
}
vnc_dbus_emit_leaving(state.terminate_reason ?: "Shutting down");
vnc_dbus_cleanup();
vnc_cleanup();
g_free(state.terminate_reason);
return 0;
}
+49
View File
@@ -0,0 +1,49 @@
/*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#ifndef TOOLS_QEMU_VNC_H
#define TOOLS_QEMU_VNC_H
#include "qemu/osdep.h"
#include <gio/gunixfdlist.h>
#include "qemu/dbus.h"
#include "qapi-types-char.h"
#include "ui/console.h"
#include "ui/dbus-display1.h"
#define TEXT_COLS 80
#define TEXT_ROWS 24
#define TEXT_FONT_WIDTH 8
#define TEXT_FONT_HEIGHT 16
QemuTextConsole *qemu_vnc_text_console_new(const char *name,
int fd, bool echo,
ChardevVCEncoding encoding);
void input_setup(QemuDBusDisplay1Keyboard *kbd,
QemuDBusDisplay1Mouse *mouse);
bool console_setup(GDBusConnection *bus, const char *bus_name,
const char *console_path);
QemuDBusDisplay1Keyboard *console_get_keyboard(const QemuConsole *con);
QemuDBusDisplay1Mouse *console_get_mouse(const QemuConsole *con);
void audio_setup(GDBusObjectManager *manager);
void clipboard_setup(GDBusObjectManager *manager, GDBusConnection *bus);
void chardev_setup(const char * const *chardev_names,
GDBusObjectManager *manager);
GThread *p2p_dbus_thread_new(int fd);
void vnc_dbus_setup(GDBusConnection *bus);
void vnc_dbus_emit_leaving(const char *reason);
void vnc_dbus_cleanup(void);
void vnc_dbus_client_connected(const char *host, const char *service,
const char *family, bool websocket);
void vnc_dbus_client_initialized(const char *host, const char *service,
const char *x509_dname,
const char *sasl_username);
void vnc_dbus_client_disconnected(const char *host, const char *service);
#endif /* TOOLS_QEMU_VNC_H */
+201
View File
@@ -0,0 +1,201 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
SPDX-License-Identifier: GPL-2.0-or-later
-->
<node>
<!--
org.qemu.Vnc1.Server:
This interface is implemented on ``/org/qemu/Vnc1/Server``.
It provides management and monitoring of the VNC server.
-->
<interface name="org.qemu.Vnc1.Server">
<!--
Name:
The VM name.
-->
<property name="Name" type="s" access="read"/>
<!--
Auth:
Primary authentication method (none, vnc, vencrypt, sasl, etc.).
-->
<property name="Auth" type="s" access="read"/>
<!--
VencryptSubAuth:
VEncrypt sub-authentication method, if applicable.
Empty string otherwise.
-->
<property name="VencryptSubAuth" type="s" access="read"/>
<!--
Clients:
Object paths of connected VNC clients.
-->
<property name="Clients" type="ao" access="read"/>
<!--
Listeners:
List of listening sockets. Each entry is a dictionary with keys:
``Host`` (s), ``Service`` (s), ``Family`` (s),
``WebSocket`` (b), ``Auth`` (s), ``VencryptSubAuth`` (s).
-->
<property name="Listeners" type="aa{sv}" access="read"/>
<!--
SetPassword:
@password: The new VNC password.
Change the VNC password. Existing clients are unaffected.
-->
<method name="SetPassword">
<arg type="s" name="password" direction="in"/>
</method>
<!--
ExpirePassword:
@time: Expiry specification.
Set password expiry. Values: ``"now"``, ``"never"``,
``"+N"`` (seconds from now), ``"N"`` (absolute epoch seconds).
-->
<method name="ExpirePassword">
<arg type="s" name="time" direction="in"/>
</method>
<!--
ReloadCertificates:
Reload TLS certificates from disk.
-->
<method name="ReloadCertificates"/>
<!--
AddClient:
@socket: file descriptor of a connected socket.
@skipauth: whether to skip VNC authentication.
Add a VNC client from an already-connected socket.
-->
<method name="AddClient">
<arg type="h" name="socket" direction="in"/>
<arg type="b" name="skipauth" direction="in"/>
</method>
<!--
ClientConnected:
@client: Object path of the new client.
Emitted when a VNC client TCP connection is established
(before authentication).
-->
<signal name="ClientConnected">
<arg type="o" name="client"/>
</signal>
<!--
ClientInitialized:
@client: Object path of the client.
Emitted when a VNC client has completed authentication
and is active.
-->
<signal name="ClientInitialized">
<arg type="o" name="client"/>
</signal>
<!--
ClientDisconnected:
@client: Object path of the client.
Emitted when a VNC client disconnects.
-->
<signal name="ClientDisconnected">
<arg type="o" name="client"/>
</signal>
<!--
Leaving:
@reason: A human-readable reason for shutting down (e.g.
"D-Bus peer org.qemu vanished").
Emitted when the VNC server is shutting down cleanly.
Clients should expect the connection to close shortly after.
-->
<signal name="Leaving">
<arg type="s" name="reason"/>
</signal>
</interface>
<!--
org.qemu.Vnc1.Client:
This interface is implemented on ``/org/qemu/Vnc1/Client_$id``.
It exposes information about a connected VNC client.
-->
<interface name="org.qemu.Vnc1.Client">
<!--
Host:
Client IP address.
-->
<property name="Host" type="s" access="read"/>
<!--
Service:
Client port or service name. This may depend on the host systems
service database so symbolic names should not be relied on.
-->
<property name="Service" type="s" access="read"/>
<!--
Family:
Address family (ipv4, ipv6, unix).
-->
<property name="Family" type="s" access="read"/>
<!--
WebSocket:
Whether this is a WebSocket connection.
-->
<property name="WebSocket" type="b" access="read"/>
<!--
X509Dname:
X.509 distinguished name (empty if not applicable).
-->
<property name="X509Dname" type="s" access="read"/>
<!--
SaslUsername:
SASL username (empty if not applicable).
-->
<property name="SaslUsername" type="s" access="read"/>
<!--
ShutdownRequest:
Emitted when the VNC client requests a guest shutdown.
-->
<signal name="ShutdownRequest"/>
<!--
ResetRequest:
Emitted when the VNC client requests a guest reset.
-->
<signal name="ResetRequest"/>
</interface>
</node>
+57
View File
@@ -0,0 +1,57 @@
/*
* Stubs for qemu-vnc standalone binary.
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "qemu/osdep.h"
#include "system/runstate.h"
#include "hw/core/qdev.h"
#include "monitor/monitor.h"
#include "migration/vmstate.h"
bool runstate_is_running(void)
{
return true;
}
bool phase_check(MachineInitPhase phase)
{
return true;
}
DeviceState *qdev_find_recursive(BusState *bus, const char *id)
{
return NULL;
}
/*
* Provide the monitor stubs locally so that the linker does not
* pull stubs/monitor-core.c.o from libqemuutil.a (which would
* bring a conflicting qapi_event_emit definition).
*/
Monitor *monitor_cur(void)
{
return NULL;
}
Monitor *monitor_set_cur(Coroutine *co, Monitor *mon)
{
return NULL;
}
int monitor_vprintf(Monitor *mon, const char *fmt, va_list ap)
{
return -1;
}
/*
* Link-time stubs for VMState symbols referenced by VNC code.
* The standalone binary never performs migration, so these are
* never actually used at runtime.
*/
const VMStateInfo vmstate_info_bool = {};
const VMStateInfo vmstate_info_int32 = {};
const VMStateInfo vmstate_info_uint32 = {};
const VMStateInfo vmstate_info_buffer = {};
+21
View File
@@ -0,0 +1,21 @@
qemu_vnc_audio_out_fini(uint64_t id) "id=%" PRIu64
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"
qemu_vnc_audio_out_set_enabled(uint64_t id, bool enabled) "id=%" PRIu64 " enabled=%d"
qemu_vnc_audio_out_write(uint64_t id, size_t size) "id=%" PRIu64 " size=%zu"
qemu_vnc_chardev_connected(const char *name) "name=%s"
qemu_vnc_clipboard_grab(int selection, uint32_t serial) "selection=%d serial=%u"
qemu_vnc_clipboard_release(int selection) "selection=%d"
qemu_vnc_clipboard_request(int selection) "selection=%d"
qemu_vnc_client_not_found(const char *host, const char *service) "host=%s service=%s"
qemu_vnc_console_io_error(const char *name) "name=%s"
qemu_vnc_cursor_define(int width, int height, int hot_x, int hot_y) "w=%d h=%d hot=%d,%d"
qemu_vnc_input_abs(uint32_t x, uint32_t y) "x=%u y=%u"
qemu_vnc_input_btn(int button, bool press) "button=%d press=%d"
qemu_vnc_input_rel(int dx, int dy) "dx=%d dy=%d"
qemu_vnc_key_event(unsigned int lnx, bool down) "lnx=%u down=%d"
qemu_vnc_owner_appeared(const char *name) "peer=%s"
qemu_vnc_owner_vanished(const char *name) "peer=%s"
qemu_vnc_scanout(uint32_t width, uint32_t height, uint32_t stride, uint32_t format) "w=%u h=%u stride=%u fmt=0x%x"
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"
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"
qemu_vnc_update_map(uint32_t x, uint32_t y, uint32_t w, uint32_t h) "x=%u y=%u w=%u h=%u"
+4
View File
@@ -0,0 +1,4 @@
/*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "trace/trace-tools_qemu_vnc.h"
+59
View File
@@ -0,0 +1,59 @@
/*
* Standalone VNC server connecting to QEMU via D-Bus display interface.
*
* Copyright (C) 2026 Red Hat, Inc.
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "qemu/osdep.h"
#include "qemu/error-report.h"
#include "qemu-vnc.h"
static GDBusConnection *
dbus_p2p_from_fd(int fd)
{
g_autoptr(GError) err = NULL;
g_autoptr(GSocket) socket = NULL;
g_autoptr(GSocketConnection) socketc = NULL;
GDBusConnection *conn;
socket = g_socket_new_from_fd(fd, &err);
if (!socket) {
error_report("Failed to create socket: %s", err->message);
return NULL;
}
socketc = g_socket_connection_factory_create_connection(socket);
if (!socketc) {
error_report("Failed to create socket connection");
return NULL;
}
conn = g_dbus_connection_new_sync(
G_IO_STREAM(socketc), NULL,
G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT |
G_DBUS_CONNECTION_FLAGS_DELAY_MESSAGE_PROCESSING,
NULL, NULL, &err);
if (!conn) {
error_report("Failed to create D-Bus connection: %s", err->message);
return NULL;
}
return conn;
}
static gpointer
p2p_server_setup_thread(gpointer data)
{
return dbus_p2p_from_fd(GPOINTER_TO_INT(data));
}
GThread *
p2p_dbus_thread_new(int fd)
{
return g_thread_new("p2p-server-setup",
p2p_server_setup_thread,
GINT_TO_POINTER(fd));
}