diff mbox series

[v2,2/7] ui/vdagent: core infrastructure

Message ID 20210318091647.3233178-3-kraxel@redhat.com
State New
Headers show
Series ui: add vdagent implementation and clipboard support. | expand

Commit Message

Gerd Hoffmann March 18, 2021, 9:16 a.m. UTC
The vdagent protocol allows the guest agent (spice-vdagent) and the
spice client exchange messages to implement features which require
guest cooperation, for example clipboard support.

This is a qemu implementation of the spice client side.  This allows
the spice guest agent talk to qemu directly when not using the spice
protocol.

usage: qemu \
  -chardev vdagent,id=vdagent \
  -device virtserialport,chardev=vdagent,name=com.redhat.spice.0

This patch adds just the protocol basics: initial handshake and
capability negotiation.

Signed-off-by: Gerd Hoffmann <kraxel@redhat.com>
---
 ui/vdagent.c    | 279 ++++++++++++++++++++++++++++++++++++++++++++++++
 qapi/char.json  |  13 +++
 ui/meson.build  |   1 +
 ui/trace-events |   8 ++
 4 files changed, 301 insertions(+)
 create mode 100644 ui/vdagent.c

Comments

Marc-André Lureau March 18, 2021, 10:32 a.m. UTC | #1
On Thu, Mar 18, 2021 at 1:17 PM Gerd Hoffmann <kraxel@redhat.com> wrote:

> The vdagent protocol allows the guest agent (spice-vdagent) and the
> spice client exchange messages to implement features which require
> guest cooperation, for example clipboard support.
>
> This is a qemu implementation of the spice client side.  This allows
> the spice guest agent talk to qemu directly when not using the spice
> protocol.
>
> usage: qemu \
>   -chardev vdagent,id=vdagent \
>   -device virtserialport,chardev=vdagent,name=com.redhat.spice.0
>
> This patch adds just the protocol basics: initial handshake and
> capability negotiation.
>
> Signed-off-by: Gerd Hoffmann <kraxel@redhat.com>
> ---
>  ui/vdagent.c    | 279 ++++++++++++++++++++++++++++++++++++++++++++++++
>  qapi/char.json  |  13 +++
>  ui/meson.build  |   1 +
>  ui/trace-events |   8 ++
>  4 files changed, 301 insertions(+)
>  create mode 100644 ui/vdagent.c
>
> diff --git a/ui/vdagent.c b/ui/vdagent.c
> new file mode 100644
> index 000000000000..146ddb8e31b4
> --- /dev/null
> +++ b/ui/vdagent.c
> @@ -0,0 +1,279 @@
> +#include "qemu/osdep.h"
> +#include "include/qemu-common.h"
> +#include "chardev/char.h"
> +#include "trace.h"
> +
> +#include "qapi/qapi-types-char.h"
> +
> +#include "spice/vd_agent.h"
> +
> +struct VDAgentChardev {
> +    Chardev parent;
> +
> +    /* guest vdagent */
> +    uint32_t caps;
> +    VDIChunkHeader chunk;
> +    uint32_t chunksize;
> +    uint8_t *msgbuf;
> +    uint32_t msgsize;
> +};
> +typedef struct VDAgentChardev VDAgentChardev;
> +
> +#define TYPE_CHARDEV_VDAGENT "chardev-vdagent"
> +
> +DECLARE_INSTANCE_CHECKER(VDAgentChardev, VDAGENT_CHARDEV,
> +                         TYPE_CHARDEV_VDAGENT);
> +
> +/* ------------------------------------------------------------------ */
> +/* names, for debug logging                                           */
> +
> +static const char *cap_name[] = {
> +    [VD_AGENT_CAP_MOUSE_STATE]                    = "mouse-state",
> +    [VD_AGENT_CAP_MONITORS_CONFIG]                = "monitors-config",
> +    [VD_AGENT_CAP_REPLY]                          = "reply",
> +    [VD_AGENT_CAP_CLIPBOARD]                      = "clipboard",
> +    [VD_AGENT_CAP_DISPLAY_CONFIG]                 = "display-config",
> +    [VD_AGENT_CAP_CLIPBOARD_BY_DEMAND]            = "clipboard-by-demand",
> +    [VD_AGENT_CAP_CLIPBOARD_SELECTION]            = "clipboard-selection",
> +    [VD_AGENT_CAP_SPARSE_MONITORS_CONFIG]         =
> "sparse-monitors-config",
> +    [VD_AGENT_CAP_GUEST_LINEEND_LF]               = "guest-lineend-lf",
> +    [VD_AGENT_CAP_GUEST_LINEEND_CRLF]             = "guest-lineend-crlf",
> +    [VD_AGENT_CAP_MAX_CLIPBOARD]                  = "max-clipboard",
> +    [VD_AGENT_CAP_AUDIO_VOLUME_SYNC]              = "audio-volume-sync",
> +    [VD_AGENT_CAP_MONITORS_CONFIG_POSITION]       =
> "monitors-config-position",
> +    [VD_AGENT_CAP_FILE_XFER_DISABLED]             = "file-xfer-disabled",
> +    [VD_AGENT_CAP_FILE_XFER_DETAILED_ERRORS]      =
> "file-xfer-detailed-errors",
> +#if 0
> +    [VD_AGENT_CAP_GRAPHICS_DEVICE_INFO]           =
> "graphics-device-info",
> +    [VD_AGENT_CAP_CLIPBOARD_NO_RELEASE_ON_REGRAB] =
> "clipboard-no-release-on-regrab",
> +    [VD_AGENT_CAP_CLIPBOARD_GRAB_SERIAL]          =
> "clipboard-grab-serial",
> +#endif
> +};
> +
> +static const char *msg_name[] = {
> +    [VD_AGENT_MOUSE_STATE]           = "mouse-state",
> +    [VD_AGENT_MONITORS_CONFIG]       = "monitors-config",
> +    [VD_AGENT_REPLY]                 = "reply",
> +    [VD_AGENT_CLIPBOARD]             = "clipboard",
> +    [VD_AGENT_DISPLAY_CONFIG]        = "display-config",
> +    [VD_AGENT_ANNOUNCE_CAPABILITIES] = "announce-capabilities",
> +    [VD_AGENT_CLIPBOARD_GRAB]        = "clipboard-grab",
> +    [VD_AGENT_CLIPBOARD_REQUEST]     = "clipboard-request",
> +    [VD_AGENT_CLIPBOARD_RELEASE]     = "clipboard-release",
> +    [VD_AGENT_FILE_XFER_START]       = "file-xfer-start",
> +    [VD_AGENT_FILE_XFER_STATUS]      = "file-xfer-status",
> +    [VD_AGENT_FILE_XFER_DATA]        = "file-xfer-data",
> +    [VD_AGENT_CLIENT_DISCONNECTED]   = "client-disconnected",
> +    [VD_AGENT_MAX_CLIPBOARD]         = "max-clipboard",
> +    [VD_AGENT_AUDIO_VOLUME_SYNC]     = "audio-volume-sync",
> +#if 0
> +    [VD_AGENT_GRAPHICS_DEVICE_INFO]  = "graphics-device-info",
> +#endif
> +};
> +
> +#define GET_NAME(_m, _v) \
> +    (((_v) < ARRAY_SIZE(_m) && (_m[_v])) ? (_m[_v]) : "???")
> +
> +/* ------------------------------------------------------------------ */
> +/* send messages                                                      */
> +
> +static void vdagent_send_buf(VDAgentChardev *vd, void *ptr, uint32_t
> msgsize)
> +{
> +    uint8_t *msgbuf = ptr;
> +    uint32_t len, pos = 0;
> +
> +    while (pos < msgsize) {
> +        len = qemu_chr_be_can_write(CHARDEV(vd));
> +        if (len > msgsize - pos) {
> +            len = msgsize - pos;
> +        }
> +        qemu_chr_be_write(CHARDEV(vd), msgbuf + pos, len);
> +        pos += len;
> +    }
> +}
> +
> +static void vdagent_send_msg(VDAgentChardev *vd, VDAgentMessage *msg)
> +{
> +    uint8_t *msgbuf = (void *)msg;
> +    uint32_t msgsize = sizeof(VDAgentMessage) + msg->size;
> +    uint32_t msgoff = 0;
> +    VDIChunkHeader chunk;
> +
> +    trace_vdagent_send(GET_NAME(msg_name, msg->type));
> +
> +    msg->protocol = VD_AGENT_PROTOCOL;
> +
> +    while (msgoff < msgsize) {
> +        chunk.port = VDP_CLIENT_PORT;
> +        chunk.size = msgsize - msgoff;
> +        if (chunk.size > 1024) {
> +            chunk.size = 1024;
> +        }
> +        vdagent_send_buf(vd, &chunk, sizeof(chunk));
> +        vdagent_send_buf(vd, msgbuf + msgoff, chunk.size);
> +        msgoff += chunk.size;
> +    }
> +}
> +
> +static void vdagent_send_caps(VDAgentChardev *vd)
> +{
> +    g_autofree VDAgentMessage *msg = g_malloc0(sizeof(VDAgentMessage) +
> +
>  sizeof(VDAgentAnnounceCapabilities) +
> +                                               sizeof(uint32_t));
> +
> +    msg->type = VD_AGENT_ANNOUNCE_CAPABILITIES;
> +    msg->size = sizeof(VDAgentAnnounceCapabilities) + sizeof(uint32_t);
> +
> +    vdagent_send_msg(vd, msg);
> +}
> +
> +/* ------------------------------------------------------------------ */
> +/* chardev backend                                                    */
> +
> +static void vdagent_chr_open(Chardev *chr,
> +                             ChardevBackend *backend,
> +                             bool *be_opened,
> +                             Error **errp)
> +{
> +    *be_opened = true;
> +}
> +
> +static void vdagent_chr_recv_caps(VDAgentChardev *vd, VDAgentMessage *msg)
> +{
> +    VDAgentAnnounceCapabilities *caps = (void *)msg->data;
> +    int i;
> +
> +    if (msg->size < (sizeof(VDAgentAnnounceCapabilities) +
> +                     sizeof(uint32_t))) {
> +        return;
> +    }
> +
> +    for (i = 0; i < ARRAY_SIZE(cap_name); i++) {
> +        if (caps->caps[0] & (1 << i)) {
> +            trace_vdagent_peer_cap(GET_NAME(cap_name, i));
> +        }
> +    }
> +
> +    vd->caps = caps->caps[0];
> +    if (caps->request) {
> +        vdagent_send_caps(vd);
> +    }
> +}
> +
> +static void vdagent_chr_recv(VDAgentChardev *vd)
> +{
> +    VDAgentMessage *msg = (void *)vd->msgbuf;
> +
> +    if (vd->msgsize < sizeof(*msg)) {
> +        fprintf(stderr, "%s: message too small: %d < %zd\n", __func__,
> +                vd->msgsize, sizeof(*msg));
> +        return;
> +    }
> +    if (vd->msgsize != msg->size + sizeof(*msg)) {
> +        /* FIXME: handle parse messages splitted into multiple chunks */
> +        fprintf(stderr, "%s: size mismatch: chunk %d, msg %d (+%zd)\n",
> +                __func__, vd->msgsize, msg->size, sizeof(*msg));
>

Not fixing? You handle chunking on sending but not on receiving?

+        return;
> +    }
> +
> +    trace_vdagent_recv_msg(GET_NAME(msg_name, msg->type));
> +
> +    switch (msg->type) {
> +    case VD_AGENT_ANNOUNCE_CAPABILITIES:
> +        vdagent_chr_recv_caps(vd, msg);
> +        break;
> +    default:
> +        break;
> +    }
> +}
> +
> +static void vdagent_reset_bufs(VDAgentChardev *vd)
> +{
> +    memset(&vd->chunk, 0, sizeof(vd->chunk));
> +    vd->chunksize = 0;
> +    g_free(vd->msgbuf);
> +    vd->msgbuf = NULL;
> +    vd->msgsize = 0;
> +}
> +
> +static int vdagent_chr_write(Chardev *chr, const uint8_t *buf, int len)
> +{
> +    VDAgentChardev *vd = VDAGENT_CHARDEV(chr);
> +    uint32_t copy, ret = len;
> +
> +    while (len) {
> +        if (vd->chunksize < sizeof(vd->chunk)) {
> +            copy = sizeof(vd->chunk) - vd->chunksize;
> +            if (copy > len) {
> +                copy = len;
> +            }
> +            memcpy((void *)(&vd->chunk) + vd->chunksize, buf, copy);
> +            vd->chunksize += copy;
> +            buf += copy;
> +            len -= copy;
> +            if (vd->chunksize < sizeof(vd->chunk)) {
> +                break;
> +            }
> +
> +            assert(vd->msgbuf == NULL);
> +            vd->msgbuf = g_malloc0(vd->chunk.size);
> +        }
> +
> +        copy = vd->chunk.size - vd->msgsize;
> +        if (copy > len) {
> +            copy = len;
> +        }
> +        memcpy(vd->msgbuf + vd->msgsize, buf, copy);
> +        vd->msgsize += copy;
> +        buf += copy;
> +        len -= copy;
> +
> +        if (vd->msgsize == vd->chunk.size) {
> +            trace_vdagent_recv_chunk(vd->chunk.size);
> +            vdagent_chr_recv(vd);
> +            vdagent_reset_bufs(vd);
> +        }
> +    }
> +
> +    return ret;
> +}
> +
> +static void vdagent_chr_set_fe_open(struct Chardev *chr, int fe_open)
> +{
> +    VDAgentChardev *vd = VDAGENT_CHARDEV(chr);
> +
> +    if (!fe_open) {
> +        trace_vdagent_close();
> +        /* reset state */
> +        vdagent_reset_bufs(vd);
> +        vd->caps = 0;
> +        return;
> +    }
> +
> +    trace_vdagent_open();
> +}
> +
> +/* ------------------------------------------------------------------ */
> +
> +static void vdagent_chr_class_init(ObjectClass *oc, void *data)
> +{
> +    ChardevClass *cc = CHARDEV_CLASS(oc);
> +
> +    cc->open             = vdagent_chr_open;
> +    cc->chr_write        = vdagent_chr_write;
> +    cc->chr_set_fe_open  = vdagent_chr_set_fe_open;
> +}
> +
> +static const TypeInfo vdagent_chr_type_info = {
> +    .name = TYPE_CHARDEV_VDAGENT,
> +    .parent = TYPE_CHARDEV,
> +    .instance_size = sizeof(VDAgentChardev),
> +    .class_init = vdagent_chr_class_init,
> +};
> +
> +static void register_types(void)
> +{
> +    type_register_static(&vdagent_chr_type_info);
> +}
> +
> +type_init(register_types);
> diff --git a/qapi/char.json b/qapi/char.json
> index 6413970fa73b..6e565ce42753 100644
> --- a/qapi/char.json
> +++ b/qapi/char.json
> @@ -390,6 +390,17 @@
>    'data': { '*size': 'int' },
>    'base': 'ChardevCommon' }
>
> +##
> +# @ChardevVDAgent:
> +#
> +# Configuration info for vdagent.
> +#
> +# Since: 6.0
> +##
> +{ 'struct': 'ChardevVDAgent',
> +  'data': { },
> +  'base': 'ChardevCommon' }
> +
>  ##
>  # @ChardevBackend:
>  #
> @@ -417,6 +428,8 @@
>                            'if': 'defined(CONFIG_SPICE)' },
>              'spiceport': { 'type': 'ChardevSpicePort',
>                             'if': 'defined(CONFIG_SPICE)' },
> +            'vdagent': { 'type': 'ChardevVDAgent',
> +                         'if': 'defined(CONFIG_SPICE)' },
>              'vc': 'ChardevVC',
>              'ringbuf': 'ChardevRingbuf',
>              # next one is just for compatibility
> diff --git a/ui/meson.build b/ui/meson.build
> index fc4fb75c2869..3d382b3b9019 100644
> --- a/ui/meson.build
> +++ b/ui/meson.build
> @@ -14,6 +14,7 @@ softmmu_ss.add(files(
>    'qemu-pixman.c',
>  ))
>  softmmu_ss.add([spice_headers, files('spice-module.c')])
> +softmmu_ss.add(when: spice_headers, if_true: files('vdagent.c'))
>
>  softmmu_ss.add(when: 'CONFIG_LINUX', if_true: files('input-linux.c'))
>  softmmu_ss.add(when: cocoa, if_true: files('cocoa.m'))
> diff --git a/ui/trace-events b/ui/trace-events
> index 5d1da6f23668..c286065f1a94 100644
> --- a/ui/trace-events
> +++ b/ui/trace-events
> @@ -124,3 +124,11 @@ xkeymap_extension(const char *name) "extension '%s'"
>  xkeymap_vendor(const char *name) "vendor '%s'"
>  xkeymap_keycodes(const char *name) "keycodes '%s'"
>  xkeymap_keymap(const char *name) "keymap '%s'"
> +
> +# vdagent.c
> +vdagent_open(void) ""
> +vdagent_close(void) ""
> +vdagent_send(const char *name) "msg %s"
> +vdagent_recv_chunk(uint32_t size) "size %d"
> +vdagent_recv_msg(const char *name) "msg %s"
> +vdagent_peer_cap(const char *name) "cap %s"
> --
> 2.30.2
>
>
>
Gerd Hoffmann March 22, 2021, 10:27 a.m. UTC | #2
Hi,

> > +    if (vd->msgsize != msg->size + sizeof(*msg)) {
> > +        /* FIXME: handle parse messages splitted into multiple chunks */
> > +        fprintf(stderr, "%s: size mismatch: chunk %d, msg %d (+%zd)\n",
> > +                __func__, vd->msgsize, msg->size, sizeof(*msg));
> >
> 
> Not fixing? You handle chunking on sending but not on receiving?

linux vdagent doesn't do chunking on send, so no need (and also no
testcase) so far.

Didn't try windows guests (yet), but that is next on my clipboard
todo list.

take care,
  Gerd
Gerd Hoffmann March 24, 2021, 9:46 a.m. UTC | #3
On Mon, Mar 22, 2021 at 11:27:17AM +0100, Gerd Hoffmann wrote:
>   Hi,
> 
> > > +    if (vd->msgsize != msg->size + sizeof(*msg)) {
> > > +        /* FIXME: handle parse messages splitted into multiple chunks */
> > > +        fprintf(stderr, "%s: size mismatch: chunk %d, msg %d (+%zd)\n",
> > > +                __func__, vd->msgsize, msg->size, sizeof(*msg));
> > >
> > 
> > Not fixing? You handle chunking on sending but not on receiving?
> 
> linux vdagent doesn't do chunking on send, so no need (and also no
> testcase) so far.
> 
> Didn't try windows guests (yet), but that is next on my clipboard
> todo list.

Hmm, windows guest agent doesn't has VD_AGENT_CAP_CLIPBOARD_SELECTION,
so I have to deal with that.

Windows guests do actually send large messages in chunks, so I have
something to test with, good.

What are VD_AGENT_CAP_GUEST_LINEEND_LF + VD_AGENT_CAP_GUEST_LINEEND_CRLF
are good for btw?  Are linefeeds converted automatically between dos and
unix conventions?  If so, who is supposed to handle that?

take care,
  Gerd
Marc-André Lureau March 24, 2021, 10:31 a.m. UTC | #4
On Wed, Mar 24, 2021 at 1:47 PM Gerd Hoffmann <kraxel@redhat.com> wrote:

> On Mon, Mar 22, 2021 at 11:27:17AM +0100, Gerd Hoffmann wrote:
> >   Hi,
> >
> > > > +    if (vd->msgsize != msg->size + sizeof(*msg)) {
> > > > +        /* FIXME: handle parse messages splitted into multiple
> chunks */
> > > > +        fprintf(stderr, "%s: size mismatch: chunk %d, msg %d
> (+%zd)\n",
> > > > +                __func__, vd->msgsize, msg->size, sizeof(*msg));
> > > >
> > >
> > > Not fixing? You handle chunking on sending but not on receiving?
> >
> > linux vdagent doesn't do chunking on send, so no need (and also no
> > testcase) so far.
> >
> > Didn't try windows guests (yet), but that is next on my clipboard
> > todo list.
>
> Hmm, windows guest agent doesn't has VD_AGENT_CAP_CLIPBOARD_SELECTION,
> so I have to deal with that.
>

ok


> Windows guests do actually send large messages in chunks, so I have
> something to test with, good.
>
> What are VD_AGENT_CAP_GUEST_LINEEND_LF + VD_AGENT_CAP_GUEST_LINEEND_CRLF
> are good for btw?  Are linefeeds converted automatically between dos and
> unix conventions?  If so, who is supposed to handle that?
>
>
I think that means the guest agent uses a text clipboard with CRLF ending,
both ways. spice-gtk has dos2unix & unix2dos helpers to deal with that.
diff mbox series

Patch

diff --git a/ui/vdagent.c b/ui/vdagent.c
new file mode 100644
index 000000000000..146ddb8e31b4
--- /dev/null
+++ b/ui/vdagent.c
@@ -0,0 +1,279 @@ 
+#include "qemu/osdep.h"
+#include "include/qemu-common.h"
+#include "chardev/char.h"
+#include "trace.h"
+
+#include "qapi/qapi-types-char.h"
+
+#include "spice/vd_agent.h"
+
+struct VDAgentChardev {
+    Chardev parent;
+
+    /* guest vdagent */
+    uint32_t caps;
+    VDIChunkHeader chunk;
+    uint32_t chunksize;
+    uint8_t *msgbuf;
+    uint32_t msgsize;
+};
+typedef struct VDAgentChardev VDAgentChardev;
+
+#define TYPE_CHARDEV_VDAGENT "chardev-vdagent"
+
+DECLARE_INSTANCE_CHECKER(VDAgentChardev, VDAGENT_CHARDEV,
+                         TYPE_CHARDEV_VDAGENT);
+
+/* ------------------------------------------------------------------ */
+/* names, for debug logging                                           */
+
+static const char *cap_name[] = {
+    [VD_AGENT_CAP_MOUSE_STATE]                    = "mouse-state",
+    [VD_AGENT_CAP_MONITORS_CONFIG]                = "monitors-config",
+    [VD_AGENT_CAP_REPLY]                          = "reply",
+    [VD_AGENT_CAP_CLIPBOARD]                      = "clipboard",
+    [VD_AGENT_CAP_DISPLAY_CONFIG]                 = "display-config",
+    [VD_AGENT_CAP_CLIPBOARD_BY_DEMAND]            = "clipboard-by-demand",
+    [VD_AGENT_CAP_CLIPBOARD_SELECTION]            = "clipboard-selection",
+    [VD_AGENT_CAP_SPARSE_MONITORS_CONFIG]         = "sparse-monitors-config",
+    [VD_AGENT_CAP_GUEST_LINEEND_LF]               = "guest-lineend-lf",
+    [VD_AGENT_CAP_GUEST_LINEEND_CRLF]             = "guest-lineend-crlf",
+    [VD_AGENT_CAP_MAX_CLIPBOARD]                  = "max-clipboard",
+    [VD_AGENT_CAP_AUDIO_VOLUME_SYNC]              = "audio-volume-sync",
+    [VD_AGENT_CAP_MONITORS_CONFIG_POSITION]       = "monitors-config-position",
+    [VD_AGENT_CAP_FILE_XFER_DISABLED]             = "file-xfer-disabled",
+    [VD_AGENT_CAP_FILE_XFER_DETAILED_ERRORS]      = "file-xfer-detailed-errors",
+#if 0
+    [VD_AGENT_CAP_GRAPHICS_DEVICE_INFO]           = "graphics-device-info",
+    [VD_AGENT_CAP_CLIPBOARD_NO_RELEASE_ON_REGRAB] = "clipboard-no-release-on-regrab",
+    [VD_AGENT_CAP_CLIPBOARD_GRAB_SERIAL]          = "clipboard-grab-serial",
+#endif
+};
+
+static const char *msg_name[] = {
+    [VD_AGENT_MOUSE_STATE]           = "mouse-state",
+    [VD_AGENT_MONITORS_CONFIG]       = "monitors-config",
+    [VD_AGENT_REPLY]                 = "reply",
+    [VD_AGENT_CLIPBOARD]             = "clipboard",
+    [VD_AGENT_DISPLAY_CONFIG]        = "display-config",
+    [VD_AGENT_ANNOUNCE_CAPABILITIES] = "announce-capabilities",
+    [VD_AGENT_CLIPBOARD_GRAB]        = "clipboard-grab",
+    [VD_AGENT_CLIPBOARD_REQUEST]     = "clipboard-request",
+    [VD_AGENT_CLIPBOARD_RELEASE]     = "clipboard-release",
+    [VD_AGENT_FILE_XFER_START]       = "file-xfer-start",
+    [VD_AGENT_FILE_XFER_STATUS]      = "file-xfer-status",
+    [VD_AGENT_FILE_XFER_DATA]        = "file-xfer-data",
+    [VD_AGENT_CLIENT_DISCONNECTED]   = "client-disconnected",
+    [VD_AGENT_MAX_CLIPBOARD]         = "max-clipboard",
+    [VD_AGENT_AUDIO_VOLUME_SYNC]     = "audio-volume-sync",
+#if 0
+    [VD_AGENT_GRAPHICS_DEVICE_INFO]  = "graphics-device-info",
+#endif
+};
+
+#define GET_NAME(_m, _v) \
+    (((_v) < ARRAY_SIZE(_m) && (_m[_v])) ? (_m[_v]) : "???")
+
+/* ------------------------------------------------------------------ */
+/* send messages                                                      */
+
+static void vdagent_send_buf(VDAgentChardev *vd, void *ptr, uint32_t msgsize)
+{
+    uint8_t *msgbuf = ptr;
+    uint32_t len, pos = 0;
+
+    while (pos < msgsize) {
+        len = qemu_chr_be_can_write(CHARDEV(vd));
+        if (len > msgsize - pos) {
+            len = msgsize - pos;
+        }
+        qemu_chr_be_write(CHARDEV(vd), msgbuf + pos, len);
+        pos += len;
+    }
+}
+
+static void vdagent_send_msg(VDAgentChardev *vd, VDAgentMessage *msg)
+{
+    uint8_t *msgbuf = (void *)msg;
+    uint32_t msgsize = sizeof(VDAgentMessage) + msg->size;
+    uint32_t msgoff = 0;
+    VDIChunkHeader chunk;
+
+    trace_vdagent_send(GET_NAME(msg_name, msg->type));
+
+    msg->protocol = VD_AGENT_PROTOCOL;
+
+    while (msgoff < msgsize) {
+        chunk.port = VDP_CLIENT_PORT;
+        chunk.size = msgsize - msgoff;
+        if (chunk.size > 1024) {
+            chunk.size = 1024;
+        }
+        vdagent_send_buf(vd, &chunk, sizeof(chunk));
+        vdagent_send_buf(vd, msgbuf + msgoff, chunk.size);
+        msgoff += chunk.size;
+    }
+}
+
+static void vdagent_send_caps(VDAgentChardev *vd)
+{
+    g_autofree VDAgentMessage *msg = g_malloc0(sizeof(VDAgentMessage) +
+                                               sizeof(VDAgentAnnounceCapabilities) +
+                                               sizeof(uint32_t));
+
+    msg->type = VD_AGENT_ANNOUNCE_CAPABILITIES;
+    msg->size = sizeof(VDAgentAnnounceCapabilities) + sizeof(uint32_t);
+
+    vdagent_send_msg(vd, msg);
+}
+
+/* ------------------------------------------------------------------ */
+/* chardev backend                                                    */
+
+static void vdagent_chr_open(Chardev *chr,
+                             ChardevBackend *backend,
+                             bool *be_opened,
+                             Error **errp)
+{
+    *be_opened = true;
+}
+
+static void vdagent_chr_recv_caps(VDAgentChardev *vd, VDAgentMessage *msg)
+{
+    VDAgentAnnounceCapabilities *caps = (void *)msg->data;
+    int i;
+
+    if (msg->size < (sizeof(VDAgentAnnounceCapabilities) +
+                     sizeof(uint32_t))) {
+        return;
+    }
+
+    for (i = 0; i < ARRAY_SIZE(cap_name); i++) {
+        if (caps->caps[0] & (1 << i)) {
+            trace_vdagent_peer_cap(GET_NAME(cap_name, i));
+        }
+    }
+
+    vd->caps = caps->caps[0];
+    if (caps->request) {
+        vdagent_send_caps(vd);
+    }
+}
+
+static void vdagent_chr_recv(VDAgentChardev *vd)
+{
+    VDAgentMessage *msg = (void *)vd->msgbuf;
+
+    if (vd->msgsize < sizeof(*msg)) {
+        fprintf(stderr, "%s: message too small: %d < %zd\n", __func__,
+                vd->msgsize, sizeof(*msg));
+        return;
+    }
+    if (vd->msgsize != msg->size + sizeof(*msg)) {
+        /* FIXME: handle parse messages splitted into multiple chunks */
+        fprintf(stderr, "%s: size mismatch: chunk %d, msg %d (+%zd)\n",
+                __func__, vd->msgsize, msg->size, sizeof(*msg));
+        return;
+    }
+
+    trace_vdagent_recv_msg(GET_NAME(msg_name, msg->type));
+
+    switch (msg->type) {
+    case VD_AGENT_ANNOUNCE_CAPABILITIES:
+        vdagent_chr_recv_caps(vd, msg);
+        break;
+    default:
+        break;
+    }
+}
+
+static void vdagent_reset_bufs(VDAgentChardev *vd)
+{
+    memset(&vd->chunk, 0, sizeof(vd->chunk));
+    vd->chunksize = 0;
+    g_free(vd->msgbuf);
+    vd->msgbuf = NULL;
+    vd->msgsize = 0;
+}
+
+static int vdagent_chr_write(Chardev *chr, const uint8_t *buf, int len)
+{
+    VDAgentChardev *vd = VDAGENT_CHARDEV(chr);
+    uint32_t copy, ret = len;
+
+    while (len) {
+        if (vd->chunksize < sizeof(vd->chunk)) {
+            copy = sizeof(vd->chunk) - vd->chunksize;
+            if (copy > len) {
+                copy = len;
+            }
+            memcpy((void *)(&vd->chunk) + vd->chunksize, buf, copy);
+            vd->chunksize += copy;
+            buf += copy;
+            len -= copy;
+            if (vd->chunksize < sizeof(vd->chunk)) {
+                break;
+            }
+
+            assert(vd->msgbuf == NULL);
+            vd->msgbuf = g_malloc0(vd->chunk.size);
+        }
+
+        copy = vd->chunk.size - vd->msgsize;
+        if (copy > len) {
+            copy = len;
+        }
+        memcpy(vd->msgbuf + vd->msgsize, buf, copy);
+        vd->msgsize += copy;
+        buf += copy;
+        len -= copy;
+
+        if (vd->msgsize == vd->chunk.size) {
+            trace_vdagent_recv_chunk(vd->chunk.size);
+            vdagent_chr_recv(vd);
+            vdagent_reset_bufs(vd);
+        }
+    }
+
+    return ret;
+}
+
+static void vdagent_chr_set_fe_open(struct Chardev *chr, int fe_open)
+{
+    VDAgentChardev *vd = VDAGENT_CHARDEV(chr);
+
+    if (!fe_open) {
+        trace_vdagent_close();
+        /* reset state */
+        vdagent_reset_bufs(vd);
+        vd->caps = 0;
+        return;
+    }
+
+    trace_vdagent_open();
+}
+
+/* ------------------------------------------------------------------ */
+
+static void vdagent_chr_class_init(ObjectClass *oc, void *data)
+{
+    ChardevClass *cc = CHARDEV_CLASS(oc);
+
+    cc->open             = vdagent_chr_open;
+    cc->chr_write        = vdagent_chr_write;
+    cc->chr_set_fe_open  = vdagent_chr_set_fe_open;
+}
+
+static const TypeInfo vdagent_chr_type_info = {
+    .name = TYPE_CHARDEV_VDAGENT,
+    .parent = TYPE_CHARDEV,
+    .instance_size = sizeof(VDAgentChardev),
+    .class_init = vdagent_chr_class_init,
+};
+
+static void register_types(void)
+{
+    type_register_static(&vdagent_chr_type_info);
+}
+
+type_init(register_types);
diff --git a/qapi/char.json b/qapi/char.json
index 6413970fa73b..6e565ce42753 100644
--- a/qapi/char.json
+++ b/qapi/char.json
@@ -390,6 +390,17 @@ 
   'data': { '*size': 'int' },
   'base': 'ChardevCommon' }
 
+##
+# @ChardevVDAgent:
+#
+# Configuration info for vdagent.
+#
+# Since: 6.0
+##
+{ 'struct': 'ChardevVDAgent',
+  'data': { },
+  'base': 'ChardevCommon' }
+
 ##
 # @ChardevBackend:
 #
@@ -417,6 +428,8 @@ 
                           'if': 'defined(CONFIG_SPICE)' },
             'spiceport': { 'type': 'ChardevSpicePort',
                            'if': 'defined(CONFIG_SPICE)' },
+            'vdagent': { 'type': 'ChardevVDAgent',
+                         'if': 'defined(CONFIG_SPICE)' },
             'vc': 'ChardevVC',
             'ringbuf': 'ChardevRingbuf',
             # next one is just for compatibility
diff --git a/ui/meson.build b/ui/meson.build
index fc4fb75c2869..3d382b3b9019 100644
--- a/ui/meson.build
+++ b/ui/meson.build
@@ -14,6 +14,7 @@  softmmu_ss.add(files(
   'qemu-pixman.c',
 ))
 softmmu_ss.add([spice_headers, files('spice-module.c')])
+softmmu_ss.add(when: spice_headers, if_true: files('vdagent.c'))
 
 softmmu_ss.add(when: 'CONFIG_LINUX', if_true: files('input-linux.c'))
 softmmu_ss.add(when: cocoa, if_true: files('cocoa.m'))
diff --git a/ui/trace-events b/ui/trace-events
index 5d1da6f23668..c286065f1a94 100644
--- a/ui/trace-events
+++ b/ui/trace-events
@@ -124,3 +124,11 @@  xkeymap_extension(const char *name) "extension '%s'"
 xkeymap_vendor(const char *name) "vendor '%s'"
 xkeymap_keycodes(const char *name) "keycodes '%s'"
 xkeymap_keymap(const char *name) "keymap '%s'"
+
+# vdagent.c
+vdagent_open(void) ""
+vdagent_close(void) ""
+vdagent_send(const char *name) "msg %s"
+vdagent_recv_chunk(uint32_t size) "size %d"
+vdagent_recv_msg(const char *name) "msg %s"
+vdagent_peer_cap(const char *name) "cap %s"