qemu-devel
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[Qemu-devel] PATCH: RFC: Support SASL authentication in VNC


From: Daniel P. Berrange
Subject: [Qemu-devel] PATCH: RFC: Support SASL authentication in VNC
Date: Wed, 4 Feb 2009 17:16:25 +0000
User-agent: Mutt/1.4.1i

Previously I provided patches for QEMU's VNC server to support SSL/TLS
and x509 certificates. This provides good encryption capabilities for
the VNC session. It doesn't really address the authentication problem
though.

I have been working to  create a new authentication type in the RFB
protocol to address this need in a generic, extendable way, by mapping
the SASL API into the RFB protocol. Since SASL is a generic plugin 
based API, this will allow use of a huge range of auth mechanims over 
VNC, without us having to add any more auth code. For example, PAM,
Digest-MD5, GSSAPI/Kerberos, One-time key/password, LDAP password
lookup, SQL db password lookup, and more.

I have got a VNC auth type assigned by the RFB spec maintainers:

  http://realvnc.com/pipermail/vnc-list/2008-December/059463.html

With the full current spec  for the SASL extension currently documented 
here:

  http://realvnc.com/pipermail/vnc-list/2008-December/059462.html

This patch provides an initial implementation of this extension for the
QEMU VNC server. I am not requesting it is merged just yet, but I'd like
get it all finished in time of the suggested QEMU release at the end
of the month, so I'm sending the patch out now for early comments

In most circumstances, it is neccessary to combine use of SASL, with
the TLS + x509 support, since most  SASL mechanisms only provide for
authentication.  In this case you would launch QEMU with

   qemu ...   -vnc localhost:1,tls,x509,sasl


The Kerberos GSSAPI mechanism for SASL is unusual in that it can also
provide encryption of the data session (so called SSF layer in SASL
terminology). If using this, QEMU can be launched with just

   qemu ...    -vnc localhost:1,sasl

The actual choice of SASL mechanism is controlled by the SASL service
configuration file. THis patch integrates with Cyrus-SASL library and
thus the config is in /etc/sasl2/qemu.conf.  This patch does not yet 
support it, but I intend too add support for overriding this config
with $HOME/.sasl2/qemu.conf, since most people use QEMU in their regular
user accounts and may not have access to the system SASL configs. The
updates to qemu-doc.texi show how to config SASL, or the Cyrus-SASL docs
can be referred to.

As I mentioned, this uses Cyrus-SASL libraries. The configure script
probes for this, and disables SASL  support if not found, so it should
not cause problems for places without Cyrus-SASL like Windows.

The main missing points in this patch

 - Authorization. Once we've authenticated the user, how do we 
   decide whether they're allow to use VNC. eg, just because a
   user has a valid Kerberos principle, does not imply we should
   allow access. 

   We really need an access control list, listing the allowed
   SASL usernames and/or x509 client certificate CNAMEs which
   are authorized. This should probably live in an external
   file, perhaps allowing for ACLs against multiple different
   QEMU network based devices. eg I could just add a arg

        -acl  /path/to/aclfile

   And invent a format like

    eg 1

       vnc.allow: address@hidden
       vnc.allow: address@hidden
        vnc.deny:  *

    eg 2

         vnc.deny: address@hidden
         vnc.deny: address@hidden
        vnc.allow:  address@hidden
         vnc.deny: *
 

 - Possible monitor commands to adding/removing ACL entires
   at runtime

        acl add vnc.allow address@hidden
        acl remove vnc.allow address@hidden

 - Per-user SASL config files, eg $HOME/.sasl2/qemu.conf
   to allow override of SASL mechanims choice by unprivileged
   users running QEMU

 - Extend 'info vnc' command to report the authenticated username
   and x509 client certificate CNAME



Finally, I have equivalent patches available for GTK-VNC supporting
this SASL authentication protocol on the client end. I would also
like to add it to something like TightVNC and/or VINO server
to make it a broadly acceptable auth extension for VNC,
since VNC is sorely lacking in a good auth standard like SASL.


 Makefile.target |    5 
 configure       |   34 ++
 qemu-doc.texi   |   94 ++++++
 qemu.sasl       |   34 ++
 vnc.c           |  800 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 vnc.h           |   16 -
 6 files changed, 964 insertions(+), 19 deletions(-)

Regards,
Daniel


Index: Makefile.target
===================================================================
--- Makefile.target     (revision 6511)
+++ Makefile.target     (working copy)
@@ -554,6 +554,11 @@
 LIBS += $(CONFIG_VNC_TLS_LIBS)
 endif
 
+ifdef CONFIG_VNC_SASL
+CPPFLAGS += $(CONFIG_VNC_SASL_CFLAGS)
+LIBS += $(CONFIG_VNC_SASL_LIBS)
+endif
+
 ifdef CONFIG_BLUEZ
 LIBS += $(CONFIG_BLUEZ_LIBS)
 endif
Index: vnc.c
===================================================================
--- vnc.c       (revision 6511)
+++ vnc.c       (working copy)
@@ -43,8 +43,12 @@
 #include <gnutls/x509.h>
 #endif /* CONFIG_VNC_TLS */
 
-// #define _VNC_DEBUG 1
+#ifdef CONFIG_VNC_SASL
+#include <sasl/sasl.h>
+#endif
 
+#define _VNC_DEBUG 1
+
 #ifdef _VNC_DEBUG
 #define VNC_DEBUG(fmt, ...) do { fprintf(stderr, fmt, ## __VA_ARGS__); } while 
(0)
 
@@ -123,6 +127,32 @@
     char *x509cert;
     char *x509key;
 #endif
+
+#ifdef CONFIG_VNC_SASL
+    sasl_conn_t *saslconn;
+    /* If we want to negotiate an SSF layer with client */
+    int saslWantSSF :1;
+    /* If we are now running the SSF layer */
+    int saslRunSSF :1;
+    /*
+     * If this is non-zero, then wait for that many bytes
+     * to be written plain, before switching to SSF encoding
+     * This allows the VNC auth result to finish being
+     * written in plain.
+     */
+    unsigned int saslWaitWriteSSF;
+
+    /*
+     * Buffering encoded data to allow more clear data
+     * to be stuffed onto the output buffer
+     */
+    const uint8_t *saslEncoded;
+    unsigned int saslEncodedLength;
+    unsigned int saslEncodedOffset;
+    char *saslUsername;
+    char *saslMechlist;
+#endif
+
     char challenge[VNC_AUTH_CHALLENGE_SIZE];
 
 #ifdef CONFIG_VNC_TLS
@@ -829,6 +859,18 @@
        }
        vs->wiremode = VNC_WIREMODE_CLEAR;
 #endif /* CONFIG_VNC_TLS */
+#ifdef CONFIG_VNC_SASL
+        if (vs->saslconn) {
+            vs->saslRunSSF = vs->saslWaitWriteSSF = vs->saslWantSSF = 0;
+            vs->saslEncodedLength = vs->saslEncodedOffset = 0;
+            vs->saslEncoded = NULL;
+            free(vs->saslUsername);
+            free(vs->saslMechlist);
+            vs->saslUsername = vs->saslMechlist = NULL;
+            sasl_dispose(&vs->saslconn);
+            vs->saslconn = NULL;
+        }
+#endif /* CONFIG_VNC_SASL */
         audio_del(vs);
        return 0;
     }
@@ -840,14 +882,12 @@
     vnc_client_io_error(vs, -1, EINVAL);
 }
 
-static void vnc_client_write(void *opaque)
+static long vnc_client_write_wire(VncState *vs, const uint8_t *data, size_t 
datalen)
 {
     long ret;
-    VncState *vs = opaque;
-
 #ifdef CONFIG_VNC_TLS
     if (vs->tls_session) {
-       ret = gnutls_write(vs->tls_session, vs->output.buffer, 
vs->output.offset);
+       ret = gnutls_write(vs->tls_session, data, datalen);
        if (ret < 0) {
            if (ret == GNUTLS_E_AGAIN)
                errno = EAGAIN;
@@ -857,35 +897,117 @@
        }
     } else
 #endif /* CONFIG_VNC_TLS */
-       ret = send(vs->csock, vs->output.buffer, vs->output.offset, 0);
-    ret = vnc_client_io_error(vs, ret, socket_error());
+       ret = send(vs->csock, data, datalen, 0);
+    VNC_DEBUG("Wrote wire %p %d -> %ld\n", data, datalen, ret);
+    return vnc_client_io_error(vs, ret, socket_error());
+}
+
+#ifdef CONFIG_VNC_SASL
+static long vnc_client_write_sasl(VncState *vs)
+{
+    long ret;
+
+    VNC_DEBUG("Write SASL: Pending output %p size %d offset %d Encoded: %p 
size %d offset %d\n",
+              vs->output.buffer, vs->output.capacity, vs->output.offset,
+              vs->saslEncoded, vs->saslEncodedLength, vs->saslEncodedOffset);
+
+    if (!vs->saslEncoded) {
+        int err;
+        err = sasl_encode(vs->saslconn,
+                          (char *)vs->output.buffer,
+                          vs->output.offset,
+                          (const char **)&vs->saslEncoded,
+                          &vs->saslEncodedLength);
+        if (err != SASL_OK)
+            return vnc_client_io_error(vs, -1, EIO);
+
+        vs->output.offset = 0;
+        vs->saslEncodedOffset = 0;
+    }
+
+    ret = vnc_client_write_wire(vs,
+                                vs->saslEncoded + vs->saslEncodedOffset,
+                                vs->saslEncodedLength - vs->saslEncodedOffset);
     if (!ret)
-       return;
+        return 0;
 
+    vs->saslEncodedOffset += ret;
+    if (vs->saslEncodedOffset == vs->saslEncodedLength) {
+        vs->saslEncoded = NULL;
+        vs->saslEncodedOffset = vs->saslEncodedLength = 0;
+    }
+
+    /* Can't merge this block with one above, because
+     * someone might have written more unencrypted
+     * data in vs->output while we were processing
+     * SASL encoded output
+     */
+    if (vs->output.offset == 0) {
+       qemu_set_fd_handler2(vs->csock, NULL, vnc_client_read, NULL, vs);
+    }
+
+    return ret;
+}
+#endif /* CONFIG_VNC_SASL */
+
+static long vnc_client_write_plain(VncState *vs)
+{
+    long ret;
+
+#ifdef CONFIG_VNC_SASL
+    VNC_DEBUG("Write Plain: Pending output %p size %d offset %d. Wait SSF 
%d\n",
+              vs->output.buffer, vs->output.capacity, vs->output.offset,
+              vs->saslWaitWriteSSF);
+
+    if (vs->saslconn &&
+        vs->saslRunSSF &&
+        vs->saslWaitWriteSSF) {
+        ret = vnc_client_write_wire(vs, vs->output.buffer, 
vs->saslWaitWriteSSF);
+        if (ret)
+            vs->saslWaitWriteSSF -= ret;
+    } else
+#endif /* CONFIG_VNC_SASL */
+        ret = vnc_client_write_wire(vs, vs->output.buffer, vs->output.offset);
+    if (!ret)
+        return 0;
+
     memmove(vs->output.buffer, vs->output.buffer + ret, (vs->output.offset - 
ret));
     vs->output.offset -= ret;
 
     if (vs->output.offset == 0) {
        qemu_set_fd_handler2(vs->csock, NULL, vnc_client_read, NULL, vs);
     }
+
+    return ret;
 }
 
+static void vnc_client_write(void *opaque)
+{
+    long ret;
+    VncState *vs = opaque;
+
+#ifdef CONFIG_VNC_SASL
+    if (vs->saslconn &&
+        vs->saslRunSSF &&
+        !vs->saslWaitWriteSSF)
+        ret = vnc_client_write_sasl(vs);
+    else
+#endif /* CONFIG_VNC_SASL */
+        ret = vnc_client_write_plain(vs);
+}
+
 static void vnc_read_when(VncState *vs, VncReadEvent *func, size_t expecting)
 {
     vs->read_handler = func;
     vs->read_handler_expect = expecting;
 }
 
-static void vnc_client_read(void *opaque)
+static long vnc_client_read_wire(VncState *vs, uint8_t *data, size_t datalen)
 {
-    VncState *vs = opaque;
     long ret;
-
-    buffer_reserve(&vs->input, 4096);
-
 #ifdef CONFIG_VNC_TLS
     if (vs->tls_session) {
-       ret = gnutls_read(vs->tls_session, buffer_end(&vs->input), 4096);
+       ret = gnutls_read(vs->tls_session, data, datalen);
        if (ret < 0) {
            if (ret == GNUTLS_E_AGAIN)
                errno = EAGAIN;
@@ -895,13 +1017,65 @@
        }
     } else
 #endif /* CONFIG_VNC_TLS */
-       ret = recv(vs->csock, buffer_end(&vs->input), 4096, 0);
-    ret = vnc_client_io_error(vs, ret, socket_error());
+       ret = recv(vs->csock, data, datalen, 0);
+    VNC_DEBUG("Read wire %p %d -> %ld\n", data, datalen, ret);
+    return vnc_client_io_error(vs, ret, socket_error());
+}
+
+#ifdef CONFIG_VNC_SASL
+static long vnc_client_read_sasl(VncState *vs)
+{
+    long ret;
+    uint8_t encoded[4096];
+    const char *decoded;
+    unsigned int decodedLen;
+    int err;
+
+    ret = vnc_client_read_wire(vs, encoded, sizeof(encoded));
     if (!ret)
-       return;
+        return 0;
 
+    err = sasl_decode(vs->saslconn,
+                      (char *)encoded, ret,
+                      &decoded, &decodedLen);
+
+    if (err != SASL_OK)
+        return vnc_client_io_error(vs, -1, -EIO);
+    VNC_DEBUG("Read SASL Encoded %p size %ld Decoded %p size %d\n",
+              encoded, ret, decoded, decodedLen);
+    buffer_reserve(&vs->input, decodedLen);
+    buffer_append(&vs->input, decoded, decodedLen);
+    return decodedLen;
+}
+#endif
+
+static long vnc_client_read_plain(VncState *vs)
+{
+    int ret;
+    VNC_DEBUG("Read plain %p size %d offset %d\n",
+              vs->input.buffer, vs->input.capacity, vs->input.offset);
+    buffer_reserve(&vs->input, 4096);
+    ret = vnc_client_read_wire(vs, buffer_end(&vs->input), 4096);
+    if (!ret)
+        return 0;
     vs->input.offset += ret;
+    return ret;
+}
 
+static void vnc_client_read(void *opaque)
+{
+    VncState *vs = opaque;
+    long ret;
+
+#if CONFIG_VNC_SASL
+    if (vs->saslconn && vs->saslRunSSF)
+        ret = vnc_client_read_sasl(vs);
+    else
+#endif /* CONFIG_VNC_SASL */
+        ret = vnc_client_read_plain(vs);
+    if (!ret)
+       return;
+
     while (vs->read_handler && vs->input.offset >= vs->read_handler_expect) {
        size_t len = vs->read_handler_expect;
        int ret;
@@ -1681,8 +1855,534 @@
     return 0;
 }
 
+#ifdef CONFIG_VNC_SASL
+/* Max amount of data we send/recv for SASL steps to prevent DOS */
+#define SASL_DATA_MAX_LEN (1024 * 1024)
 
+static char *addr_to_string(struct sockaddr_storage *sa, socklen_t salen) {
+    char host[1024], port[20];
+    char *addr;
+    int err;
+
+    if ((err = getnameinfo((struct sockaddr *)sa, salen,
+                           host, sizeof(host),
+                           port, sizeof(port),
+                           NI_NUMERICHOST | NI_NUMERICSERV)) != 0) {
+        VNC_DEBUG("Cannot resolve address %d: %s\n",
+                  err, gai_strerror(err));
+        return NULL;
+    }
+
+    if ((addr = malloc(strlen(host) + 1 + strlen(port) + 1)) == NULL) {
+        VNC_DEBUG("Out of memory\n");
+        return NULL;
+    }
+
+    strcpy(addr, host);
+    strcat(addr, ";");
+    strcat(addr, port);
+    return addr;
+}
+
+
+static int vnc_auth_sasl_check_ssf(VncState *vs)
+{
+    const void *val;
+    int err, ssf;
+
+    if (!vs->saslWantSSF)
+        return 1;
+
+    err = sasl_getprop(vs->saslconn, SASL_SSF, &val);
+    if (err != SASL_OK)
+        return 0;
+
+    ssf = *(const int *)val;
+    VNC_DEBUG("negotiated an SSF of %d\n", ssf);
+    if (ssf < 56)
+        return 0; /* 56 is good for Kerberos */
+
+    /* Only setup for read initially, because we're about to send an RPC
+     * reply which must be in plain text. When the next incoming RPC
+     * arrives, we'll switch on writes too
+     *
+     * cf qemudClientReadSASL  in qemud.c
+     */
+    vs->saslRunSSF = 1;
+
+    /* We have a SSF that's good enough */
+    return 1;
+}
+
+/*
+ * Step Msg
+ *
+ * Input from client:
+ *
+ * u32 clientin-length
+ * u8-array clientin-string
+ *
+ * Output to client:
+ *
+ * u32 serverout-length
+ * u8-array serverout-strin
+ * u8 continue
+ */
+
+static int protocol_client_auth_sasl_step_len(VncState *vs, uint8_t *data, 
size_t len);
+
+static int protocol_client_auth_sasl_step(VncState *vs, uint8_t *data, size_t 
len)
+{
+    uint32_t datalen = len;
+    const char *serverout;
+    unsigned int serveroutlen;
+    int err;
+    char *clientdata = NULL;
+
+    /* NB, distinction of NULL vs "" is *critical* in SASL */
+    if (datalen) {
+        clientdata = (char*)data;
+        clientdata[datalen-1] = '\0'; /* Wire includes '\0', but make sure */
+        datalen--; /* Don't count NULL byte when passing to _start() */
+    }
+
+    VNC_DEBUG("Step using SASL Data %p (%d bytes)\n",
+              clientdata, datalen);
+    err = sasl_server_step(vs->saslconn,
+                           clientdata,
+                           datalen,
+                           &serverout,
+                           &serveroutlen);
+    if (err != SASL_OK &&
+        err != SASL_CONTINUE) {
+        VNC_DEBUG("sasl step failed %d (%s)\n",
+                  err, sasl_errdetail(vs->saslconn));
+        sasl_dispose(&vs->saslconn);
+        vs->saslconn = NULL;
+        goto authabort;
+    }
+
+    if (serveroutlen > SASL_DATA_MAX_LEN) {
+        VNC_DEBUG("sasl step reply data too long %d\n",
+                  serveroutlen);
+        sasl_dispose(&vs->saslconn);
+        vs->saslconn = NULL;
+        goto authabort;
+    }
+
+    VNC_DEBUG("SASL return data %d bytes, nil; %d\n",
+              serveroutlen, serverout ? 0 : 1);
+
+    if (serveroutlen) {
+        vnc_write_u32(vs, serveroutlen + 1);
+        vnc_write(vs, serverout, serveroutlen + 1);
+    } else {
+        vnc_write_u32(vs, 0);
+    }
+
+    /* Whether auth is complete */
+    vnc_write_u8(vs, err == SASL_CONTINUE ? 0 : 1);
+
+    if (err == SASL_CONTINUE) {
+        VNC_DEBUG("%s", "Authentication must continue\n");
+        /* Wait for step length */
+        vnc_read_when(vs, protocol_client_auth_sasl_step_len, 4);
+    } else {
+        if (!vnc_auth_sasl_check_ssf(vs)) {
+            VNC_DEBUG("Authentication rejected for weak SSF %d\n", vs->csock);
+            goto authreject;
+        }
+
+#if 0
+        /* Check username whitelist ACL */
+        if (remoteSASLCheckAccess(server, vs, rerr) < 0) {
+            VNC_DEBUG("Authentication rejected for ACL %d\n", vs->csock);
+            goto authreject;
+        }
+#endif
+        VNC_DEBUG("Authentication successful %d\n", vs->csock);
+        vnc_write_u32(vs, 0); /* Accept auth */
+        /*
+         * Delay writing in SSF encoded mode until pending output
+         * buffer is written
+         */
+        if (vs->saslRunSSF)
+            vs->saslWaitWriteSSF = vs->output.offset;
+        vnc_read_when(vs, protocol_client_init, 1);
+    }
+
+    return 0;
+
+ authreject:
+    vnc_write_u32(vs, 1); /* Reject auth */
+    vnc_write_u32(vs, sizeof("Authentication failed"));
+    vnc_write(vs, "Authentication failed", sizeof("Authentication failed"));
+    vnc_flush(vs);
+    vnc_client_error(vs);
+    return -1;
+
+ authabort:
+    vnc_client_error(vs);
+    return -1;
+}
+
+static int protocol_client_auth_sasl_step_len(VncState *vs, uint8_t *data, 
size_t len)
+{
+    uint32_t steplen = read_u32(data, 0);
+    VNC_DEBUG("Got client step len %d\n", steplen);
+    if (steplen > SASL_DATA_MAX_LEN) {
+        VNC_DEBUG("Too much SASL data %d\n", steplen);
+        vnc_client_error(vs);
+        return -1;
+    }
+
+    if (steplen == 0)
+        return protocol_client_auth_sasl_step(vs, NULL, 0);
+    else
+        vnc_read_when(vs, protocol_client_auth_sasl_step, steplen);
+    return 0;
+}
+
+/*
+ * Start Msg
+ *
+ * Input from client:
+ *
+ * u32 clientin-length
+ * u8-array clientin-string
+ *
+ * Output to client:
+ *
+ * u32 serverout-length
+ * u8-array serverout-strin
+ * u8 continue
+ */
+
+#define SASL_DATA_MAX_LEN (1024 * 1024)
+
+static int protocol_client_auth_sasl_start(VncState *vs, uint8_t *data, size_t 
len)
+{
+    uint32_t datalen = len;
+    const char *serverout;
+    unsigned int serveroutlen;
+    int err;
+    char *clientdata = NULL;
+
+    /* NB, distinction of NULL vs "" is *critical* in SASL */
+    if (datalen) {
+        clientdata = (char*)data;
+        clientdata[datalen-1] = '\0'; /* Should be on wire, but make sure */
+        datalen--; /* Don't count NULL byte when passing to _start() */
+    }
+
+    VNC_DEBUG("Start SASL auth with mechanism %s. Data %p (%d bytes)\n",
+              vs->saslMechlist, clientdata, datalen);
+    err = sasl_server_start(vs->saslconn,
+                            vs->saslMechlist,
+                            clientdata,
+                            datalen,
+                            &serverout,
+                            &serveroutlen);
+    if (err != SASL_OK &&
+        err != SASL_CONTINUE) {
+        VNC_DEBUG("sasl start failed %d (%s)\n",
+                  err, sasl_errdetail(vs->saslconn));
+        sasl_dispose(&vs->saslconn);
+        vs->saslconn = NULL;
+        goto authabort;
+    }
+    if (serveroutlen > SASL_DATA_MAX_LEN) {
+        VNC_DEBUG("sasl start reply data too long %d\n",
+                  serveroutlen);
+        sasl_dispose(&vs->saslconn);
+        vs->saslconn = NULL;
+        goto authabort;
+    }
+
+    VNC_DEBUG("SASL return data %d bytes, nil; %d\n",
+              serveroutlen, serverout ? 0 : 1);
+
+    if (serveroutlen) {
+        vnc_write_u32(vs, serveroutlen + 1);
+        vnc_write(vs, serverout, serveroutlen + 1);
+    } else {
+        vnc_write_u32(vs, 0);
+    }
+
+    /* Whether auth is complete */
+    vnc_write_u8(vs, err == SASL_CONTINUE ? 0 : 1);
+
+    if (err == SASL_CONTINUE) {
+        VNC_DEBUG("%s", "Authentication must continue\n");
+        /* Wait for step length */
+        vnc_read_when(vs, protocol_client_auth_sasl_step_len, 4);
+    } else {
+        if (!vnc_auth_sasl_check_ssf(vs)) {
+            VNC_DEBUG("Authentication rejected for weak SSF %d\n", vs->csock);
+            goto authreject;
+        }
+
+#if 0
+        /* Check username whitelist ACL */
+        if (remoteSASLCheckAccess(server, vs, rerr) < 0) {
+            VNC_DEBUG("Authentication rejected for ACL %d\n", vs->csock);
+            goto authreject;
+        }
+#endif
+        VNC_DEBUG("Authentication successful %d\n", vs->csock);
+        vnc_write_u32(vs, 0); /* Accept auth */
+        vnc_read_when(vs, protocol_client_init, 1);
+    }
+
+    return 0;
+
+ authreject:
+    vnc_write_u32(vs, 1); /* Reject auth */
+    vnc_write_u32(vs, sizeof("Authentication failed"));
+    vnc_write(vs, "Authentication failed", sizeof("Authentication failed"));
+    vnc_flush(vs);
+    vnc_client_error(vs);
+    return -1;
+
+ authabort:
+    vnc_client_error(vs);
+    return -1;
+}
+
+static int protocol_client_auth_sasl_start_len(VncState *vs, uint8_t *data, 
size_t len)
+{
+    uint32_t startlen = read_u32(data, 0);
+    VNC_DEBUG("Got client start len %d\n", startlen);
+    if (startlen > SASL_DATA_MAX_LEN) {
+        VNC_DEBUG("Too much SASL data %d\n", startlen);
+        vnc_client_error(vs);
+        return -1;
+    }
+
+    if (startlen == 0)
+        return protocol_client_auth_sasl_start(vs, NULL, 0);
+
+    vnc_read_when(vs, protocol_client_auth_sasl_start, startlen);
+    return 0;
+}
+
+static int protocol_client_auth_sasl_mechname(VncState *vs, uint8_t *data, 
size_t len)
+{
+    char *mechname = malloc(len + 1);
+    if (!mechname) {
+        VNC_DEBUG("Out of memory reading mechname\n");
+        vnc_client_error(vs);
+    }
+    strncpy(mechname, (char*)data, len);
+    mechname[len] = '\0';
+    VNC_DEBUG("Got client mechname '%s' check against '%s'\n",
+              mechname, vs->saslMechlist);
+
+    if (strncmp(vs->saslMechlist, mechname, len) == 0) {
+        if (vs->saslMechlist[len] != '\0' &&
+            vs->saslMechlist[len] != ',') {
+            VNC_DEBUG("One %d", vs->saslMechlist[len]);
+            vnc_client_error(vs);
+            return -1;
+        }
+    } else {
+        char *offset = strstr(vs->saslMechlist, mechname);
+        VNC_DEBUG("Two %p\n", offset);
+        if (!offset) {
+            vnc_client_error(vs);
+            return -1;
+        }
+        VNC_DEBUG("Two '%s'\n", offset);
+        if (offset[-1] != ',' ||
+            (offset[len] != '\0'&&
+             offset[len] != ',')) {
+            vnc_client_error(vs);
+            return -1;
+        }
+    }
+
+    free(vs->saslMechlist);
+    vs->saslMechlist = mechname;
+
+    VNC_DEBUG("Validated mechname '%s'\n", mechname);
+    vnc_read_when(vs, protocol_client_auth_sasl_start_len, 4);
+    return 0;
+}
+
+static int protocol_client_auth_sasl_mechname_len(VncState *vs, uint8_t *data, 
size_t len)
+{
+    uint32_t mechlen = read_u32(data, 0);
+    VNC_DEBUG("Got client mechname len %d\n", mechlen);
+    if (mechlen > 100) {
+        VNC_DEBUG("Too long SASL mechname data %d\n", mechlen);
+        vnc_client_error(vs);
+        return -1;
+    }
+    if (mechlen < 1) {
+        VNC_DEBUG("Too short SASL mechname %d\n", mechlen);
+        vnc_client_error(vs);
+        return -1;
+    }
+    vnc_read_when(vs, protocol_client_auth_sasl_mechname,mechlen);
+    return 0;
+}
+
+static int start_auth_sasl(VncState *vs)
+{
+    const char *mechlist = NULL;
+    sasl_security_properties_t secprops;
+    int err;
+    struct sockaddr_storage sa;
+    socklen_t salen;
+    char *localAddr, *remoteAddr;
+    int mechlistlen;
+
+    VNC_DEBUG("Initialize SASL auth %d\n", vs->csock);
+
+    /* Get local address in form  IPADDR:PORT */
+    salen = sizeof(sa);
+    if (getsockname(vs->csock, (struct sockaddr*)&sa, &salen) < 0) {
+        VNC_DEBUG("failed to get sock address %d (%s)\n",
+                  errno, strerror(errno));
+        goto authabort;
+    }
+    if ((localAddr = addr_to_string(&sa, salen)) == NULL) {
+        goto authabort;
+    }
+
+    /* Get remote address in form  IPADDR:PORT */
+    salen = sizeof(sa);
+    if (getpeername(vs->csock, (struct sockaddr*)&sa, &salen) < 0) {
+        VNC_DEBUG("failed to get peer address %d (%s)\n",
+                  errno, strerror(errno));
+        free(localAddr);
+        goto authabort;
+    }
+    if ((remoteAddr = addr_to_string(&sa, salen)) == NULL) {
+        free(localAddr);
+        goto authabort;
+    }
+
+    err = sasl_server_new("vnc",
+                          NULL, /* FQDN - just delegates to gethostname */
+                          NULL, /* User realm */
+                          localAddr,
+                          remoteAddr,
+                          NULL, /* Callbacks, not needed */
+                          SASL_SUCCESS_DATA,
+                          &vs->saslconn);
+    free(localAddr);
+    free(remoteAddr);
+    localAddr = remoteAddr = NULL;
+
+    if (err != SASL_OK) {
+        VNC_DEBUG("sasl context setup failed %d (%s)",
+                  err, sasl_errstring(err, NULL, NULL));
+        vs->saslconn = NULL;
+        goto authabort;
+    }
+
 #ifdef CONFIG_VNC_TLS
+    /* Inform SASL that we've got an external SSF layer from TLS */
+    if (vs->auth == VNC_AUTH_VENCRYPT &&
+        vs->subauth != VNC_AUTH_VENCRYPT_PLAIN) { /* XXX fileter out other 
sub-auth ? */
+        gnutls_cipher_algorithm_t cipher;
+        sasl_ssf_t ssf;
+
+        cipher = gnutls_cipher_get(vs->tls_session);
+        if (!(ssf = (sasl_ssf_t)gnutls_cipher_get_key_size(cipher))) {
+            VNC_DEBUG("%s", "cannot TLS get cipher size\n");
+            sasl_dispose(&vs->saslconn);
+            vs->saslconn = NULL;
+            goto authabort;
+        }
+        ssf *= 8; /* tls key size is bytes, sasl wants bits */
+
+        err = sasl_setprop(vs->saslconn, SASL_SSF_EXTERNAL, &ssf);
+        if (err != SASL_OK) {
+            VNC_DEBUG("cannot set SASL external SSF %d (%s)\n",
+                      err, sasl_errstring(err, NULL, NULL));
+            sasl_dispose(&vs->saslconn);
+            vs->saslconn = NULL;
+            goto authabort;
+        }
+    } else
+#endif /* CONFIG_VNC_TLS */
+        vs->saslWantSSF = 1;
+
+    memset (&secprops, 0, sizeof secprops);
+    /* Inform SASL that we've got an external SSF layer from TLS */
+    if (strncmp(vs->display, "unix:", 5) == 0
+#ifdef CONFIG_VNC_TLS
+        || (vs->auth == VNC_AUTH_VENCRYPT &&
+            vs->subauth != VNC_AUTH_VENCRYPT_PLAIN)
+#endif /* CONFIG_VNC_TLS */
+        ) { /* XXX fileter out other sub-auth ? */
+        /* If we've got TLS or UNIX domain sock, we don't care about SSF */
+        secprops.min_ssf = 0;
+        secprops.max_ssf = 0;
+        secprops.maxbufsize = 8192;
+        secprops.security_flags = 0;
+    } else {
+        /* Plain TCP, better get an SSF layer */
+        secprops.min_ssf = 56; /* Good enough to require kerberos */
+        secprops.max_ssf = 100000; /* Arbitrary big number */
+        secprops.maxbufsize = 8192;
+        /* Forbid any anonymous or trivially crackable auth */
+        secprops.security_flags =
+            SASL_SEC_NOANONYMOUS | SASL_SEC_NOPLAINTEXT;
+    }
+
+    err = sasl_setprop(vs->saslconn, SASL_SEC_PROPS, &secprops);
+    if (err != SASL_OK) {
+        VNC_DEBUG("cannot set SASL security props %d (%s)\n",
+                  err, sasl_errstring(err, NULL, NULL));
+        sasl_dispose(&vs->saslconn);
+        vs->saslconn = NULL;
+        goto authabort;
+    }
+
+    err = sasl_listmech(vs->saslconn,
+                        NULL, /* Don't need to set user */
+                        "", /* Prefix */
+                        ",", /* Separator */
+                        "", /* Suffix */
+                        &mechlist,
+                        NULL,
+                        NULL);
+    if (err != SASL_OK) {
+        VNC_DEBUG("cannot list SASL mechanisms %d (%s)\n",
+                  err, sasl_errdetail(vs->saslconn));
+        sasl_dispose(&vs->saslconn);
+        vs->saslconn = NULL;
+        goto authabort;
+    }
+    VNC_DEBUG("Available mechanisms for client: '%s'\n", mechlist);
+
+    if (!(vs->saslMechlist = strdup(mechlist))) {
+        VNC_DEBUG("Out of memory");
+        sasl_dispose(&vs->saslconn);
+        vs->saslconn = NULL;
+        goto authabort;
+    }
+    mechlistlen = strlen(mechlist);
+    vnc_write_u32(vs, mechlistlen);
+    vnc_write(vs, mechlist, mechlistlen);
+    vnc_flush(vs);
+
+    VNC_DEBUG("Wait for client mechname length\n");
+    vnc_read_when(vs, protocol_client_auth_sasl_mechname_len, 4);
+
+    return 0;
+
+ authabort:
+    vnc_client_error(vs);
+    return -1;
+}
+#endif
+
+
+#ifdef CONFIG_VNC_TLS
 #define DH_BITS 1024
 static gnutls_dh_params_t dh_params;
 
@@ -2126,6 +2826,12 @@
            return start_auth_vencrypt(vs);
 #endif /* CONFIG_VNC_TLS */
 
+#ifdef CONFIG_VNC_SASL
+       case VNC_AUTH_SASL:
+           VNC_DEBUG("Accept SASL auth\n");
+           return start_auth_sasl(vs);
+#endif /* CONFIG_VNC_SASL */
+
        default: /* Should not be possible, but just in case */
            VNC_DEBUG("Reject auth %d\n", vs->auth);
            vnc_write_u8(vs, 1);
@@ -2359,6 +3065,18 @@
        }
        vs->wiremode = VNC_WIREMODE_CLEAR;
 #endif /* CONFIG_VNC_TLS */
+#ifdef CONFIG_VNC_SASL
+        if (vs->saslconn) {
+            vs->saslRunSSF = vs->saslWaitWriteSSF = vs->saslWantSSF = 0;
+            vs->saslEncodedLength = vs->saslEncodedOffset = 0;
+            vs->saslEncoded = NULL;
+            free(vs->saslUsername);
+            free(vs->saslMechlist);
+            vs->saslUsername = vs->saslMechlist = NULL;
+            sasl_dispose(&vs->saslconn);
+            vs->saslconn = NULL;
+        }
+#endif /* CONFIG_VNC_SASL */
     }
     vs->auth = VNC_AUTH_INVALID;
 #ifdef CONFIG_VNC_TLS
@@ -2394,6 +3112,10 @@
 #ifdef CONFIG_VNC_TLS
     int tls = 0, x509 = 0;
 #endif
+#ifdef CONFIG_VNC_SASL
+    int sasl = 0;
+    int saslErr;
+#endif
 
     vnc_display_close(ds);
     if (strcmp(display, "none") == 0)
@@ -2411,6 +3133,10 @@
            reverse = 1;
        } else if (strncmp(options, "to=", 3) == 0) {
             to_port = atoi(options+3) + 5900;
+#ifdef CONFIG_VNC_SASL
+       } else if (strncmp(options, "sasl", 4) == 0) {
+           sasl = 1; /* Require SASL auth */
+#endif
 #ifdef CONFIG_VNC_TLS
        } else if (strncmp(options, "tls", 3) == 0) {
            tls = 1; /* Require TLS */
@@ -2466,6 +3192,27 @@
            vs->subauth = VNC_AUTH_INVALID;
        }
 #endif
+#ifdef CONFIG_VNC_SASL
+    } else if (sasl) {
+#ifdef CONFIG_VNC_TLS
+        if (tls) {
+            vs->auth = VNC_AUTH_VENCRYPT;
+            if (x509) {
+               VNC_DEBUG("Initializing VNC server with x509 SASL auth\n");
+                vs->subauth = VNC_AUTH_VENCRYPT_X509SASL;
+            } else {
+               VNC_DEBUG("Initializing VNC server with TLS SASL auth\n");
+                vs->subauth = VNC_AUTH_VENCRYPT_TLSSASL;
+            }
+        } else {
+#endif
+           VNC_DEBUG("Initializing VNC server with SASL auth\n");
+            vs->auth = VNC_AUTH_SASL;
+#ifdef CONFIG_VNC_TLS
+            vs->subauth = VNC_AUTH_INVALID;
+        }
+#endif
+#endif
     } else {
 #ifdef CONFIG_VNC_TLS
        if (tls) {
@@ -2487,6 +3234,16 @@
 #endif
     }
 
+#ifdef CONFIG_VNC_SASL
+    if ((saslErr = sasl_server_init(NULL, "qemu")) != SASL_OK) {
+        fprintf(stderr, "Failed to initialize SASL auth %s",
+                sasl_errstring(saslErr, NULL, NULL));
+        free(vs->display);
+        vs->display = NULL;
+        return -1;
+    }
+#endif
+
     if (reverse) {
         /* connect to viewer */
         if (strncmp(display, "unix:", 5) == 0)
@@ -2525,3 +3282,12 @@
 
     return qemu_set_fd_handler2(vs->lsock, vnc_listen_poll, vnc_listen_read, 
NULL, vs);
 }
+
+/*
+ * Local variables:
+ *  c-indent-level: 4
+ *  c-basic-offset: 4
+ *  tab-width: 8
+ *  indent-tabs-mode: nil
+ * End:
+ */
Index: qemu.sasl
===================================================================
--- qemu.sasl   (revision 0)
+++ qemu.sasl   (revision 0)
@@ -0,0 +1,34 @@
+# If you want to use the non-TLS socket, then you *must* include
+# the GSSAPI or DIGEST-MD5 mechanisms, because they are the only
+# ones that can offer session encryption as well as authentication.
+#
+# If you're only using TLS, then you can turn on any mechanisms
+# you like for authentication, because TLS provides the encryption
+#
+# Default to a simple username+password mechanism
+# NB digest-md5 is no longer considered secure by current standards
+mech_list: digest-md5
+
+# Before you can use GSSAPI, you need a service principle on the
+# KDC server for libvirt, and that to be exported to the keytab
+# file listed below
+#mech_list: gssapi
+#
+# You can also list many mechanisms at once, then the user can choose
+# by adding  '?auth=sasl.gssapi' to their libvirt URI, eg
+#   qemu+tcp://hostname/system?auth=sasl.gssapi
+#mech_list: digest-md5 gssapi
+
+# Some older builds of MIT kerberos on Linux ignore this option &
+# instead need KRB5_KTNAME env var.
+# For modern Linux, and other OS, this should be sufficient
+keytab: /etc/qemu/krb5.tab
+
+# If using digest-md5 for username/passwds, then this is the file
+# containing the passwds. Use 'saslpasswd2 -a qemu [username]'
+# to add entries, and 'sasldblistusers2 -a qemu' to browse it
+sasldb_path: /etc/qemu/passwd.db
+
+
+auxprop_plugin: sasldb
+
Index: vnc.h
===================================================================
--- vnc.h       (revision 6511)
+++ vnc.h       (working copy)
@@ -15,8 +15,9 @@
     VNC_AUTH_RA2NE = 6,
     VNC_AUTH_TIGHT = 16,
     VNC_AUTH_ULTRA = 17,
-    VNC_AUTH_TLS = 18,
-    VNC_AUTH_VENCRYPT = 19
+    VNC_AUTH_TLS = 18, /* Supported in GTK-VNC & VINO */
+    VNC_AUTH_VENCRYPT = 19, /* Supported in GTK-VNC & VeNCrypt */
+    VNC_AUTH_SASL = 20, /* Also implemented by GTK-VNC & VINO */
 };
 
 #ifdef CONFIG_VNC_TLS
@@ -33,6 +34,8 @@
     VNC_AUTH_VENCRYPT_X509NONE = 260,
     VNC_AUTH_VENCRYPT_X509VNC = 261,
     VNC_AUTH_VENCRYPT_X509PLAIN = 262,
+    VNC_AUTH_VENCRYPT_X509SASL = 263,
+    VNC_AUTH_VENCRYPT_TLSSASL = 264,
 };
 
 #define X509_CA_CERT_FILE "ca-cert.pem"
@@ -42,6 +45,15 @@
 
 #endif /* CONFIG_VNC_TLS */
 
+
+#ifdef CONFIG_VNC_SASL
+enum qemu_sasl_ssf {
+  QEMU_SASL_SSF_NONE = 0,
+  QEMU_SASL_SSF_READ = 1,
+  QEMU_SASL_SSF_WRITE = 2,
+};
+#endif /* CONFIG_VNC_SASL */
+
 /*****************************************************************************
  *
  * Encoding types
Index: qemu-doc.texi
===================================================================
--- qemu-doc.texi       (revision 6511)
+++ qemu-doc.texi       (working copy)
@@ -616,6 +616,19 @@
 be loaded from. See the @ref{vnc_security} section for details on generating
 certificates.
 
address@hidden sasl
+
+Require that the client use SASL to authenticate with the VNC server.
+The exact choice of authentication method used is controlled from the
+system / user's SASL configuration file for the 'qemu' service. This
+is typically found in /etc/sasl2/qemu.conf, or $HOME/.sasl2/qemu.conf.
+While some SASL auth methods can also provide data encryption (eg GSSAPI),
+it is recommended that SASL always be combined with the 'tls' and
+'x509' settings to enable use of SSL and server certificates. This
+ensures a data encryption preventing compromise of authentication
+credentials. See the @ref{vnc_security} section for details on using
+SASL authentication.
+
 @end table
 
 @end table
@@ -2045,7 +2058,10 @@
 * vnc_sec_certificate::
 * vnc_sec_certificate_verify::
 * vnc_sec_certificate_pw::
+* vnc_sec_sasl::
+* vnc_sec_certificate_sasl::
 * vnc_generate_cert::
+* vnc_setup_sasl::
 @end menu
 @node vnc_sec_none
 @subsection Without passwords
@@ -2128,6 +2144,41 @@
 (qemu)
 @end example
 
+
address@hidden vnc_sec_sasl
address@hidden With SASL authentication
+
+The SASL authentication method is a VNC extension, that provides an
+easily extendable, pluggable authentication method. This allows for
+integration with a wide range of authentication mechanisms, such as
+PAM, GSSAPI/Kerberos, LDAP, SQL databases, one-time keys and more.
+The strength of the authentication depends on the exact mechanism
+configured. If the chosen mechanism also provides a SSF layer, then
+it will encrypt the datastream as well.
+
+Refer to the later docs on how to choose the exact SASL mechanism
+used for authentication, but assuming use of one supporting SSF,
+then QEMU can be launched with:
+
address@hidden
+qemu [...OPTIONS...] -vnc :1,sasl -monitor stdio
address@hidden example
+
address@hidden vnc_sec_certificate_sasl
address@hidden With x509 certificates and SASL authentication
+
+If the desired SASL authentication mechanism does not supported
+SSF layers, then it is strongly advised to run it in combination
+with TLS and x509 certificates. This provides securely encrypted
+data stream, avoiding risk of compromising of the security
+credentials. This can be enabled, by combining the 'sasl' option
+with the aforementioned TLS + x509 options:
+
address@hidden
+qemu [...OPTIONS...] -vnc :1,tls,x509,sasl -monitor stdio
address@hidden example
+
+
 @node vnc_generate_cert
 @subsection Generating certificates for VNC
 
@@ -2239,6 +2290,49 @@
 The @code{client-key.pem} and @code{client-cert.pem} files should now be 
securely
 copied to the client for which they were generated.
 
+
address@hidden vnc_setup_sasl
+
address@hidden Configuring SASL mechanisms
+
+The following documentation assumes use of the Cyrus SASL implementation on a
+Linux host, but the principals should apply to any other SASL impl. When SASL
+is enabled, the mechanism configuration will be loaded from system default
+SASL service config /etc/sasl2/qemu.conf. If running QEMU unprivileged, then it
+will also look for a user customization in $HOME/.sasl2/qemu.conf
+
+The default configuration might contain
+
address@hidden
+mech_list: digest-md5
+sasldb_path: /etc/qemu/passwd.db
address@hidden example
+
+This says to use the 'Digest MD5' mechanism, which is similar to the HTTP
+Digest-MD5 mechanism. Te list of valid usernames & passwords is maintained
+in the /etc/qemu/passwd.db file, and can be updated using the saslpasswd2
+command. While this mechanism is easy to configure and use, it is not
+considered secure by modern standards, so only suitable for developers /
+ad-hoc testing.
+
+A more serious deployment might use Kerberos, which is done with the 'gssapi'
+mechanism
+
address@hidden
+mech_list: gssapi
+keytab: /etc/qemu/krb5.tab
address@hidden example
+
+For this to work the administrator of your KDC must generate a Kerberos
+principal for the server, with a name of  
'qemu/somehost.example.com@@EXAMPLE.COM'
+replacing 'somehost.example.com' with the fully qualified host name of the
+machine running QEMU, and 'EXAMPLE.COM' with the Keberos Realm.
+
+Other configurations will be left as an exercise for the reader. It should
+be noted that only GSSAPI provides a SSF layer for data encryption. For
+all other mechanisms, VNC should always be configured to use TLS and x509
+certificates to protect security credentials from snooping.
+
 @node gdb_usage
 @section GDB usage
 
Index: configure
===================================================================
--- configure   (revision 6511)
+++ configure   (working copy)
@@ -164,6 +164,7 @@
 fmod_inc=""
 oss_lib=""
 vnc_tls="yes"
+vnc_sasl="yes"
 bsd="no"
 linux="no"
 solaris="no"
@@ -387,6 +388,8 @@
   ;;
   --disable-vnc-tls) vnc_tls="no"
   ;;
+  --disable-vnc-sasl) vnc_sasl="no"
+  ;;
   --disable-slirp) slirp="no"
   ;;
   --disable-vde) vde="no"
@@ -544,6 +547,7 @@
 echo "  --enable-mixemu          enable mixer emulation"
 echo "  --disable-brlapi         disable BrlAPI"
 echo "  --disable-vnc-tls        disable TLS encryption for VNC server"
+echo "  --disable-vnc-sasl       disable SASL encryption for VNC server"
 echo "  --disable-curses         disable curses output"
 echo "  --disable-bluez          disable bluez stack connectivity"
 echo "  --disable-kvm            disable KVM acceleration support"
@@ -823,6 +827,25 @@
 fi
 
 ##########################################
+# VNC SASL detection
+if test "$vnc_sasl" = "yes" ; then
+cat > $TMPC <<EOF
+#include <sasl/sasl.h>
+#include <stdio.h>
+int main(void) { sasl_server_init(NULL, "qemu"); return 0; }
+EOF
+    # Assuming Cyrus-SASL installed in /usr prefix
+    vnc_sasl_cflags=""
+    vnc_sasl_libs="-lsasl2"
+    if $cc $ARCH_CFLAGS -o $TMPE ${OS_CFLAGS} $vnc_sasl_cflags $TMPC \
+           $vnc_sasl_libs 2> /dev/null ; then
+       :
+    else
+       vnc_sasl="no"
+    fi
+fi
+
+##########################################
 # vde libraries probe
 if test "$vde" = "yes" ; then
   cat > $TMPC << EOF
@@ -1130,6 +1153,11 @@
     echo "    TLS CFLAGS    $vnc_tls_cflags"
     echo "    TLS LIBS      $vnc_tls_libs"
 fi
+echo "VNC SASL support   $vnc_sasl"
+if test "$vnc_sasl" = "yes" ; then
+    echo "    SASL CFLAGS    $vnc_sasl_cflags"
+    echo "    SASL LIBS      $vnc_sasl_libs"
+fi
 if test -n "$sparc_cpu"; then
     echo "Target Sparc Arch $sparc_cpu"
 fi
@@ -1371,6 +1399,12 @@
   echo "CONFIG_VNC_TLS_LIBS=$vnc_tls_libs" >> $config_mak
   echo "#define CONFIG_VNC_TLS 1" >> $config_h
 fi
+if test "$vnc_sasl" = "yes" ; then
+  echo "CONFIG_VNC_SASL=yes" >> $config_mak
+  echo "CONFIG_VNC_SASL_CFLAGS=$vnc_sasl_cflags" >> $config_mak
+  echo "CONFIG_VNC_SASL_LIBS=$vnc_sasl_libs" >> $config_mak
+  echo "#define CONFIG_VNC_SASL 1" >> $config_h
+fi
 qemu_version=`head $source_path/VERSION`
 echo "VERSION=$qemu_version" >>$config_mak
 echo "#define QEMU_VERSION \"$qemu_version\"" >> $config_h



-- 
|: Red Hat, Engineering, London   -o-   http://people.redhat.com/berrange/ :|
|: http://libvirt.org  -o-  http://virt-manager.org  -o-  http://ovirt.org :|
|: http://autobuild.org       -o-         http://search.cpan.org/~danberr/ :|
|: GnuPG: 7D3B9505  -o-  F3C9 553F A1DA 4AC2 5648 23C1 B3DF F742 7D3B 9505 :|




reply via email to

[Prev in Thread] Current Thread [Next in Thread]