The brand new channel-ssh has been created in order to do the communication between the guest and the client's ssh agent. With the new agent and a little bit of configuration on the guest we are able to forward the ssh-agent through a spice channel. The configuration needed on the guest side (part of OpenSSH 7.3 release) is quite simple, just add in the guest's ssh config file: Host: hostname IdentityAgent: /var/run/spice-ssh-agentd/spice-ssh-agent-sock It will forward the agent when connecting to that specify hostname. Signed-off-by: Fabiano Fidêncio <fidencio@xxxxxxxxxx> --- configure.ac | 26 ++ doc/reference/spice-gtk-sections.txt | 17 ++ doc/reference/spice-gtk.types | 2 + src/Makefile.am | 3 + src/channel-ssh.c | 454 +++++++++++++++++++++++++++++++++++ src/channel-ssh.h | 68 ++++++ src/map-file | 1 + src/spice-channel.c | 10 + src/spice-client.h | 1 + src/spice-glib-sym-file | 1 + 10 files changed, 583 insertions(+) create mode 100644 src/channel-ssh.c create mode 100644 src/channel-ssh.h diff --git a/configure.ac b/configure.ac index f3e7f8d..5beb5c9 100644 --- a/configure.ac +++ b/configure.ac @@ -188,6 +188,31 @@ PKG_CHECK_MODULES(CAIRO, cairo >= 1.2.0) PKG_CHECK_MODULES(GTHREAD, gthread-2.0) +AC_ARG_ENABLE([ssh-agent-forward], + AS_HELP_STRING([--enable-ssh-agent-forward=@<:@auto/yes/no@:>@], + [Enable ssh-agent-forward support @<:@default=auto@:>@]), + [], + [enable_ssh_agent_forward="auto"]) + + +if test "x$enable_ssh_agent_forward" = "xno"; then + have_ssh_agent_forward="no" +else + PKG_CHECK_MODULES(GIO, + [glib-2.0 >= 2.43.90 gio-2.0 > 2.43.90], + [have_ssh_agent_forward=yes], + [have_ssh_agent_forward=no]) + + if test "x$have_ssh_agent_forward" = "xno" && test "x$enable_ssh_agent_forward" = "xyes"; then + AC_MSG_ERROR([ssh-agent-forward support explicitly requested, but some required packages are not available]) + fi +fi + +AS_IF([test "x$have_ssh_agent_forward" = "xyes"], + AC_DEFINE([USE_SSH_AGENT_FORWARD], [1], [Define if supporting ssh-agent-forward])) + +AM_CONDITIONAL([WITH_SSH_AGENT_FORWARD], [test "x$have_ssh_agent_forward" = "xyes"]) + AC_ARG_ENABLE([webdav], AS_HELP_STRING([--enable-webdav=@<:@auto/yes/no@:>@], [Enable webdav support @<:@default=auto@:>@]), @@ -618,6 +643,7 @@ AC_MSG_NOTICE([ DBus: ${have_dbus} WebDAV support: ${have_phodav} LZ4 support: ${have_lz4} + ssh-agent forward support: ${have_ssh_agent_forward} Now type 'make' to build $PACKAGE diff --git a/doc/reference/spice-gtk-sections.txt b/doc/reference/spice-gtk-sections.txt index 386e737..98be852 100644 --- a/doc/reference/spice-gtk-sections.txt +++ b/doc/reference/spice-gtk-sections.txt @@ -527,3 +527,20 @@ SpiceFileTransferTask SpiceFileTransferTaskClass SpiceFileTransferTaskPrivate </SECTION> + +<SECTION> +<FILE>channel-ssh</FILE> +<TITLE>SpiceSshChannel</TITLE> +SpiceSshChannel +SpiceSshChannelClass +<SUBSECTION Standard> +SPICE_IS_SSH_CHANNEL +SPICE_IS_SSH_CHANNEL_CLASS +SPICE_TYPE_SSH_CHANNEL +SPICE_SSH_CHANNEL +SPICE_SSH_CHANNEL_CLASS +SPICE_SSH_CHANNEL_GET_CLASS +spice_ssh_channel_get_type +<SUBSECTION Private> +SpiceSshChannelPrivate +</SECTION> diff --git a/doc/reference/spice-gtk.types b/doc/reference/spice-gtk.types index e14ae1b..ea07d5b 100644 --- a/doc/reference/spice-gtk.types +++ b/doc/reference/spice-gtk.types @@ -12,6 +12,7 @@ #include "channel-playback.h" #include "channel-record.h" #include "channel-smartcard.h" +#include "channel-ssh.h" #include "channel-usbredir.h" #include "channel-webdav.h" #include "spice-gtk-session.h" @@ -40,6 +41,7 @@ spice_session_verify_get_type spice_smartcard_channel_get_type spice_smartcard_manager_get_type spice_session_verify_get_type +spice_ssh_channel_get_type spice_usbredir_channel_get_type spice_usb_device_get_type spice_usb_device_manager_get_type diff --git a/src/Makefile.am b/src/Makefile.am index 7542580..87fdfe9 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -254,6 +254,7 @@ libspice_client_glib_2_0_la_SOURCES = \ channel-port.c \ channel-record.c \ channel-smartcard.c \ + channel-ssh.c \ channel-usbredir.c \ channel-usbredir-priv.h \ smartcard-manager.c \ @@ -306,6 +307,7 @@ libspice_client_glibinclude_HEADERS = \ channel-port.h \ channel-record.h \ channel-smartcard.h \ + channel-ssh.h \ channel-usbredir.h \ channel-webdav.h \ usb-device-manager.h \ @@ -599,6 +601,7 @@ glib_introspection_files = \ channel-port.c \ channel-record.c \ channel-smartcard.c \ + channel-ssh.c \ channel-usbredir.c \ smartcard-manager.c \ usb-device-manager.c \ diff --git a/src/channel-ssh.c b/src/channel-ssh.c new file mode 100644 index 0000000..0d3c304 --- /dev/null +++ b/src/channel-ssh.c @@ -0,0 +1,454 @@ +/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */ +/* + Copyright (C) 2015 Red Hat, Inc. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see <http://www.gnu.org/licenses/>. +*/ + +#include "config.h" + +#include "spice-channel-priv.h" +#include "spice-session-priv.h" +#include "vmcstream.h" + +#include <fcntl.h> +#include <poll.h> +#include <sys/socket.h> +#include <sys/types.h> +#include <sys/un.h> + +/** + * SECTION:channel-ssh + * @short_description: forwards the client ssh-agent + * @title: Ssh Channel + * @section_id: + * @see_also: #SpiceChannel + * @stability: Stable + * @include: channel-ssh.h + * + * The "ssh" channel forwards the client ssh-agent to a guest. + * The underlying protocol implemented is: + * https://tools.ietf.org/html/draft-ietf-secsh-agent-02 + * + * Since: 0.33 + */ + +#define SPICE_SSH_CHANNEL_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE((obj), SPICE_TYPE_SSH_CHANNEL, SpiceSshChannelPrivate)) + +typedef struct _msg { + guint32 len; + guint8 *payload; +} msg; + +struct _SpiceSshChannelPrivate { + SpiceVmcStream *stream; + int auth_socket; + GCancellable *cancellable; + msg msg_in; + msg msg_out; + gboolean demuxing; +}; + +G_DEFINE_TYPE (SpiceSshChannel, spice_ssh_channel, SPICE_TYPE_PORT_CHANNEL) + +#ifdef USE_SSH_AGENT_FORWARD +static void start_virtio_read (SpiceSshChannel *self); + +static void +agent_put_u32( void *vp, guint32 v) { + guint8 *p = vp; + + p[0] = (guint8) (v >> 24) & 0xff; + p[1] = (guint8) (v >> 16) & 0xff; + p[2] = (guint8) (v >> 8) & 0xff; + p[3] = (guint8) v & 0xff; +} + +static +guint32 agent_get_u32 (const void *vp) { + const guint8 *p = vp; + guint32 v; + + v = (guint32) p[0] << 24; + v |= (guint32) p[1] << 16; + v |= (guint32) p[2] << 8; + v |= (guint32) p[3]; + + return v; +} + +#ifndef POLLIN +#define POLLIN 0x001 /* There is data to read. */ +#endif +#ifndef POLLOUT +#define POLLOUT 0x004 /* Writing now will not block. */ +#endif + +static gsize +atomicio (int fd, void *buf, gsize n, int do_read) { + gchar *b = buf; + gsize pos = 0; + gssize res; + struct pollfd pfd; + + pfd.fd = fd; + pfd.events = do_read ? POLLIN : POLLOUT; + + while (n > pos) { + if (do_read) { + res = read (fd, b + pos, n - pos); + } else { + res = write (fd, b + pos, n - pos); + } + switch (res) { + case -1: + if (errno == EINTR) { + continue; + } + if (errno == EAGAIN) { + poll (&pfd, 1, -1); + continue; + } + return 0; + case 0: + /* read returns 0 on end-of-file */ + errno = do_read ? 0 : EPIPE; + return pos; + default: + pos += (gsize) res; + } + } + + return pos; +} + +static void +payload_write_cb (GObject *source_object, GAsyncResult *res, gpointer data) +{ + GOutputStream *ostream = G_OUTPUT_STREAM (source_object); + SpiceSshChannel *self = data; + SpiceSshChannelPrivate *c = self->priv; + gsize size; + GError *error = NULL; + + g_output_stream_write_all_finish (ostream, res, &size, &error); + + g_free (c->msg_out.payload); + c->msg_out.payload = NULL; + + if (error != NULL) { + g_warning ("error: %s", error->message); + g_clear_error (&error); + + return; + } + + g_return_if_fail (c->msg_out.len == size); + + c->demuxing = FALSE; + + start_virtio_read (self); +} + +static void +size_write_cb (GObject *source_object, GAsyncResult *res, gpointer data) +{ + GOutputStream *ostream = G_OUTPUT_STREAM (source_object); + SpiceSshChannel *self = data; + SpiceSshChannelPrivate *c = self->priv; + gsize size; + GError *error = NULL; + + g_output_stream_write_all_finish (ostream, res, &size, &error); + if (error != NULL) { + g_warning ("error: %s", error->message); + g_clear_error (&error); + + return; + } + + g_return_if_fail (size == sizeof (guint32)); + + /* payload must be freed inside payload_write_cb () */ + c->msg_out.payload = g_malloc0 (c->msg_out.len * sizeof (guint8)); + + if (atomicio (c->auth_socket, c->msg_out.payload, c->msg_out.len, 1) != c->msg_out.len) { + g_warning ("Error reading response from authentication socket."); + return; + } + + g_output_stream_write_all_async ( + ostream, + c->msg_out.payload, + c->msg_out.len, + G_PRIORITY_DEFAULT, + c->cancellable, + payload_write_cb, + self); +} + +static void +agent_talk (SpiceSshChannel *self) +{ + SpiceSshChannelPrivate *c = self->priv; + guint32 payload; + GOutputStream *ostream = NULL; + + agent_put_u32(&payload, c->msg_in.len); + + /* send length and then the request packet */ + if (atomicio (c->auth_socket, &payload, 4, 0) == 4) { + if (atomicio (c->auth_socket, c->msg_in.payload, c->msg_in.len, 0) != c->msg_in.len) { + g_warning ("atomicio sending request failed: %s", g_strerror (errno)); + return; + } + } else { + g_warning ("atomicio sending request length failed: %s", g_strerror (errno)); + return; + } + + /* wait for response, read the length of the response packet */ + if (atomicio (c->auth_socket, &payload, 4, 1) != 4) { + g_warning ("atomicio read response length failed: %s", g_strerror (errno)); + return; + } + + c->msg_out.len = agent_get_u32(&payload); + if (c->msg_out.len > 256 * 1024) { + g_warning ("Authentication response is too long: %"PRIu32"\n", c->msg_out.len); + return; + } + + ostream = g_io_stream_get_output_stream (G_IO_STREAM (c->stream)); + g_output_stream_write_all_async ( + ostream, + &c->msg_out.len, + sizeof (guint32), + G_PRIORITY_DEFAULT, + c->cancellable, + size_write_cb, + self); +} + +static void +payload_read_cb (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + SpiceSshChannel *self = user_data; + SpiceSshChannelPrivate *c = self->priv; + GInputStream *istream = G_INPUT_STREAM (source_object); + gssize size; + GError *error = NULL; + + size = spice_vmc_input_stream_read_all_finish (istream, res, &error); + if (error != NULL) { + g_warning ("error: %s\n", error->message); + g_clear_error (&error); + + return; + } + + g_return_if_fail (size == c->msg_in.len); + + agent_talk (self); + + g_clear_pointer(&c->msg_in.payload, g_free); + c->msg_in.len = 0; +} + +static void +size_read_cb (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + SpiceSshChannel *self = user_data; + SpiceSshChannelPrivate *c = self->priv; + GInputStream *istream = G_INPUT_STREAM (source_object); + gssize size; + GError *error = NULL; + + size = spice_vmc_input_stream_read_all_finish (istream, res, &error); + if (error != NULL) { + g_warning ("error: %s\n", error->message); + g_clear_error (&error); + + return; + } + + g_return_if_fail (size == sizeof (guint32)); + + /* payload must be freed inside payload_read_cb () */ + c->msg_in.payload = g_malloc0 (c->msg_in.len * sizeof (guint8)); + + spice_vmc_input_stream_read_all_async ( + istream, + c->msg_in.payload, + c->msg_in.len, + G_PRIORITY_DEFAULT, + c->cancellable, + payload_read_cb, + self); +} +#endif + +static void +start_virtio_read (SpiceSshChannel *self) +{ +#ifdef USE_SSH_AGENT_FORWARD + SpiceSshChannelPrivate *c = self->priv; + GInputStream *istream = g_io_stream_get_input_stream (G_IO_STREAM (c->stream)); + + if (c->demuxing) + return; + + c->demuxing = TRUE; + + CHANNEL_DEBUG (self, "start virtio read"); + spice_vmc_input_stream_read_all_async ( + istream, + &c->msg_in.len, + sizeof (guint32), + G_PRIORITY_DEFAULT, + c->cancellable, + size_read_cb, + self); +#endif +} + +static void +port_event (SpiceSshChannel *self, gint event) +{ + SpiceSshChannelPrivate *c = self->priv; + + CHANNEL_DEBUG (self, "port event: %d", event); + if (event == SPICE_PORT_EVENT_OPENED) { + g_clear_object (&c->cancellable); + c->cancellable = g_cancellable_new(); + start_virtio_read (self); + } else { + g_cancellable_cancel (c->cancellable); + + c->demuxing = FALSE; + } +} + +static gint +spice_ssh_channel_get_auth_socket (void) +{ + const gchar *authsocket; + gint sock; + struct sockaddr_un sunaddr; + + authsocket = g_getenv ("SSH_AUTH_SOCK"); + if (authsocket == NULL) + return -1; + + memset (&sunaddr, 0, sizeof (sunaddr)); + sunaddr.sun_family = AF_UNIX; + strncpy (sunaddr.sun_path, authsocket, sizeof (sunaddr.sun_path)); + + if ((sock = socket (AF_UNIX, SOCK_STREAM, 0)) < 0) + return -1; + + if (fcntl (sock, F_SETFD, FD_CLOEXEC) == -1 || + connect (sock, (struct sockaddr *)&sunaddr, sizeof (sunaddr)) < 0) { + close (sock); + return -1; + } + + return sock; +} + +static void +spice_ssh_channel_init (SpiceSshChannel *channel) +{ + SpiceSshChannelPrivate *c = SPICE_SSH_CHANNEL_GET_PRIVATE (channel); + + channel->priv = c; + c->stream = spice_vmc_stream_new (SPICE_CHANNEL (channel)); + + c->auth_socket = spice_ssh_channel_get_auth_socket (); + c->cancellable = g_cancellable_new (); +} + +static void +spice_ssh_channel_dispose (GObject *object) +{ + SpiceSshChannelPrivate *c = SPICE_SSH_CHANNEL (object)->priv; + + if (!g_cancellable_is_cancelled(c->cancellable)) + g_cancellable_cancel (c->cancellable); + g_clear_object (&c->cancellable); + g_clear_object (&c->stream); + close (c->auth_socket); + + G_OBJECT_CLASS (spice_ssh_channel_parent_class)->dispose (object); +} + +/* coroutine context */ +static void +ssh_handle_msg (SpiceChannel *channel, SpiceMsgIn *in) +{ + SpiceSshChannel *self = SPICE_SSH_CHANNEL (channel); + SpiceSshChannelPrivate *c = self->priv; + gint size; + guint8 *buf; + + buf = spice_msg_in_raw (in, &size); + CHANNEL_DEBUG (channel, "len:%d buf:%p", size, buf); + + spice_vmc_input_stream_co_data ( + SPICE_VMC_INPUT_STREAM ( + g_io_stream_get_input_stream (G_IO_STREAM (c->stream))), + buf, + size); +} + +/* coroutine context */ +static void +spice_ssh_handle_msg (SpiceChannel *channel, SpiceMsgIn *msg) +{ + gint type = spice_msg_in_type (msg); + SpiceChannelClass *parent_class; + + parent_class = SPICE_CHANNEL_CLASS (spice_ssh_channel_parent_class); + + if (type == SPICE_MSG_SPICEVMC_DATA) + ssh_handle_msg (channel, msg); + else if (parent_class->handle_msg) + parent_class->handle_msg (channel, msg); + else + g_return_if_reached (); +} + +static void +spice_ssh_channel_up (SpiceChannel *channel) +{ + CHANNEL_DEBUG (channel, "up"); +} + +static void +spice_ssh_channel_class_init (SpiceSshChannelClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + SpiceChannelClass *channel_class = SPICE_CHANNEL_CLASS (klass); + + gobject_class->dispose = spice_ssh_channel_dispose; + channel_class->handle_msg = spice_ssh_handle_msg; + channel_class->channel_up = spice_ssh_channel_up; + + g_signal_override_class_handler ("port-event", + SPICE_TYPE_SSH_CHANNEL, + G_CALLBACK (port_event)); + + g_type_class_add_private (klass, sizeof (SpiceSshChannelPrivate)); +} diff --git a/src/channel-ssh.h b/src/channel-ssh.h new file mode 100644 index 0000000..a5e358a --- /dev/null +++ b/src/channel-ssh.h @@ -0,0 +1,68 @@ +/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */ +/* + Copyright (C) 2015 Red Hat, Inc. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see <http://www.gnu.org/licenses/>. +*/ +#ifndef __SPICE_SSH_CHANNEL_H__ +#define __SPICE_SSH_CHANNEL_H__ + +#include <gio/gio.h> +#include "spice-client.h" +#include "channel-port.h" + +G_BEGIN_DECLS + +#define SPICE_TYPE_SSH_CHANNEL (spice_ssh_channel_get_type()) +#define SPICE_SSH_CHANNEL(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), SPICE_TYPE_SSH_CHANNEL, SpiceSshChannel)) +#define SPICE_SSH_CHANNEL_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), SPICE_TYPE_SSH_CHANNEL, SpiceSshChannelClass)) +#define SPICE_IS_SSH_CHANNEL(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), SPICE_TYPE_SSH_CHANNEL)) +#define SPICE_IS_SSH_CHANNEL_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), SPICE_TYPE_SSH_CHANNEL)) +#define SPICE_SSH_CHANNEL_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), SPICE_TYPE_SSH_CHANNEL, SpiceSshChannelClass)) + +typedef struct _SpiceSshChannel SpiceSshChannel; +typedef struct _SpiceSshChannelClass SpiceSshChannelClass; +typedef struct _SpiceSshChannelPrivate SpiceSshChannelPrivate; + +/** + * SpiceSshChannel: + * + * The #SpiceSshChannel struct is opaque and should not be accessed directly. + */ +struct _SpiceSshChannel { + SpicePortChannel parent; + + /*< private >*/ + SpiceSshChannelPrivate *priv; + /* Do not add fields to this struct */ +}; + +/** + * SpiceSshChannelClass: + * @parent_class: Parent class. + * + * Class structure for #SpiceSshChannel. + */ +struct _SpiceSshChannelClass { + SpicePortChannelClass parent_class; + + /*< private >*/ + /* Do not add fields to this struct */ +}; + +GType spice_ssh_channel_get_type(void); + +G_END_DECLS + +#endif /* __SPICE_SSH_CHANNEL_H__ */ diff --git a/src/map-file b/src/map-file index 3d92153..9091362 100644 --- a/src/map-file +++ b/src/map-file @@ -113,6 +113,7 @@ spice_smartcard_reader_get_type; spice_smartcard_reader_insert_card; spice_smartcard_reader_is_software; spice_smartcard_reader_remove_card; +spice_ssh_channel_get_type; spice_uri_get_hostname; spice_uri_get_password; spice_uri_get_port; diff --git a/src/spice-channel.c b/src/spice-channel.c index 95662f3..41faa5a 100644 --- a/src/spice-channel.c +++ b/src/spice-channel.c @@ -2035,6 +2035,7 @@ static const char *to_string[] = { [ SPICE_CHANNEL_USBREDIR ] = "usbredir", [ SPICE_CHANNEL_PORT ] = "port", [ SPICE_CHANNEL_WEBDAV ] = "webdav", + [ SPICE_CHANNEL_SSH ] = "ssh", }; /** @@ -2098,6 +2099,9 @@ gchar *spice_channel_supported_string(void) #ifdef USE_PHODAV spice_channel_type_to_string(SPICE_CHANNEL_WEBDAV), #endif +#ifdef USE_SSH_AGENT_FORWARD + spice_channel_type_to_string(SPICE_CHANNEL_SSH), +#endif NULL); } @@ -2168,6 +2172,12 @@ SpiceChannel *spice_channel_new(SpiceSession *s, int type, int id) break; } #endif +#ifdef USE_SSH_AGENT_FORWARD + case SPICE_CHANNEL_SSH: { + gtype = SPICE_TYPE_SSH_CHANNEL; + break; + } +#endif case SPICE_CHANNEL_PORT: gtype = SPICE_TYPE_PORT_CHANNEL; break; diff --git a/src/spice-client.h b/src/spice-client.h index 32b79ea..904fa18 100644 --- a/src/spice-client.h +++ b/src/spice-client.h @@ -46,6 +46,7 @@ #include "channel-usbredir.h" #include "channel-port.h" #include "channel-webdav.h" +#include "channel-ssh.h" #include "smartcard-manager.h" #include "usb-device-manager.h" diff --git a/src/spice-glib-sym-file b/src/spice-glib-sym-file index 473c5ca..a9c4f73 100644 --- a/src/spice-glib-sym-file +++ b/src/spice-glib-sym-file @@ -92,6 +92,7 @@ spice_smartcard_reader_get_type spice_smartcard_reader_insert_card spice_smartcard_reader_is_software spice_smartcard_reader_remove_card +spice_ssh_channel_get_type spice_uri_get_hostname spice_uri_get_password spice_uri_get_port -- 2.7.4 _______________________________________________ Spice-devel mailing list Spice-devel@xxxxxxxxxxxxxxxxxxxxx https://lists.freedesktop.org/mailman/listinfo/spice-devel