diff mbox

block: Add support for Secure Shell (ssh) block device.

Message ID 1363873138-30568-2-git-send-email-rjones@redhat.com
State New
Headers show

Commit Message

Richard W.M. Jones March 21, 2013, 1:38 p.m. UTC
From: "Richard W.M. Jones" <rjones@redhat.com>

  qemu-system-x86_64 -drive file=ssh://hostname/some/image

QEMU will ssh into 'hostname' and open '/some/image' which is made
available as a standard block device.

You can specify a username (ssh://user@host/...) and/or a port number
(ssh://host:port/...).

Current limitations:

- Authentication must be done without passwords or passphrases, using
  ssh-agent.  Other authentication methods are not supported. (*)

- Does not check host key. (*)

- New remote files cannot be created. (*)

- Uses coroutine read/write, instead of true AIO.  (libssh2 supports
  non-blocking access, so this could be fixed with some effort).

- Blocks during connection and authentication.

(*) = potentially easy fix

This is implemented using libssh2 on the client side.  The server just
requires a regular ssh daemon with sftp-server support.  Most ssh
daemons on Unix/Linux systems will work out of the box.
---
 block/Makefile.objs |   1 +
 block/ssh.c         | 514 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 configure           |  47 +++++
 qemu-doc.texi       |  28 +++
 4 files changed, 590 insertions(+)
 create mode 100644 block/ssh.c

Comments

Stefan Hajnoczi March 21, 2013, 3:26 p.m. UTC | #1
On Thu, Mar 21, 2013 at 01:38:58PM +0000, Richard W.M. Jones wrote:
> From: "Richard W.M. Jones" <rjones@redhat.com>
> 
>   qemu-system-x86_64 -drive file=ssh://hostname/some/image
> 
> QEMU will ssh into 'hostname' and open '/some/image' which is made
> available as a standard block device.
> 
> You can specify a username (ssh://user@host/...) and/or a port number
> (ssh://host:port/...).

I can see this being handy for qemu-img since it gives you the ability
to work with remote image files.

> Current limitations:
> 
> - Authentication must be done without passwords or passphrases, using
>   ssh-agent.  Other authentication methods are not supported. (*)
> 
> - Does not check host key. (*)
> 
> - New remote files cannot be created. (*)

Would be important to fix these limitations.  Authentication methods to
make this more usable.  Host key check for security.  File creation for
qemu-img.

> - Uses coroutine read/write, instead of true AIO.  (libssh2 supports
>   non-blocking access, so this could be fixed with some effort).

This patch does not really use coroutines - the SSH I/O is blocking!

Coroutines must submit the SSH I/O and then yield so the QEMU event loop
can get on with other work.  When SSH I/O finishes the request's
coroutine is re-entered and the request gets completed.

> - Blocks during connection and authentication.

Right now the code also blocks while SSH I/O takes place.

> (*) = potentially easy fix
> 
> This is implemented using libssh2 on the client side.  The server just
> requires a regular ssh daemon with sftp-server support.  Most ssh
> daemons on Unix/Linux systems will work out of the box.

How much of a win over sshfs is this?

sshfs can be mounted by unprivileged users and QEMU accesses it like a
regular file.  So the sshfs approach already works today.

Stefan
Richard W.M. Jones March 21, 2013, 3:39 p.m. UTC | #2
On Thu, Mar 21, 2013 at 04:26:23PM +0100, Stefan Hajnoczi wrote:
> On Thu, Mar 21, 2013 at 01:38:58PM +0000, Richard W.M. Jones wrote:
> > From: "Richard W.M. Jones" <rjones@redhat.com>
> > 
> >   qemu-system-x86_64 -drive file=ssh://hostname/some/image
> > 
> > QEMU will ssh into 'hostname' and open '/some/image' which is made
> > available as a standard block device.
> > 
> > You can specify a username (ssh://user@host/...) and/or a port number
> > (ssh://host:port/...).
> 
> I can see this being handy for qemu-img since it gives you the ability
> to work with remote image files.
> 
> > Current limitations:
> > 
> > - Authentication must be done without passwords or passphrases, using
> >   ssh-agent.  Other authentication methods are not supported. (*)
> > 
> > - Does not check host key. (*)
> > 
> > - New remote files cannot be created. (*)
> 
> Would be important to fix these limitations.  Authentication methods to
> make this more usable.  Host key check for security.  File creation for
> qemu-img.

I agree.

> > - Uses coroutine read/write, instead of true AIO.  (libssh2 supports
> >   non-blocking access, so this could be fixed with some effort).
> 
> This patch does not really use coroutines - the SSH I/O is blocking!
> 
> Coroutines must submit the SSH I/O and then yield so the QEMU event loop
> can get on with other work.  When SSH I/O finishes the request's
> coroutine is re-entered and the request gets completed.

Hmm OK.  Is there any documentation at all on how coroutines are
supposed to work?  Or AIO for that matter?  For example, do coroutines
really replace all the read/write syscalls deep inside a library
(libssh2) so that these calls context switch, or am I missing the
point of how this works entirely?

[...]
> > This is implemented using libssh2 on the client side.  The server just
> > requires a regular ssh daemon with sftp-server support.  Most ssh
> > daemons on Unix/Linux systems will work out of the box.
> 
> How much of a win over sshfs is this?
> 
> sshfs can be mounted by unprivileged users and QEMU accesses it like a
> regular file.  So the sshfs approach already works today.

Sure, but compared to having to install and set up sshfs and FUSE,
using this is a lot simpler.  It's also potentially faster since it
cuts out FUSE and context switching to the sshfs process.

BTW I looked into the implementation of sshfs before starting this,
and what it does is to run an 'ssh' client subprocess, then implements
the sftp protocol by hand on top.  So using libssh2 cuts out *two*
external processes (plus FUSE).

Rich.
Stefan Hajnoczi March 21, 2013, 7:29 p.m. UTC | #3
On Thu, Mar 21, 2013 at 4:39 PM, Richard W.M. Jones <rjones@redhat.com> wrote:
> On Thu, Mar 21, 2013 at 04:26:23PM +0100, Stefan Hajnoczi wrote:
>> On Thu, Mar 21, 2013 at 01:38:58PM +0000, Richard W.M. Jones wrote:
>> > From: "Richard W.M. Jones" <rjones@redhat.com>
>> >
>> >   qemu-system-x86_64 -drive file=ssh://hostname/some/image
>> >
>> > QEMU will ssh into 'hostname' and open '/some/image' which is made
>> > available as a standard block device.
>> >
>> > You can specify a username (ssh://user@host/...) and/or a port number
>> > (ssh://host:port/...).
>>
>> I can see this being handy for qemu-img since it gives you the ability
>> to work with remote image files.
>>
>> > Current limitations:
>> >
>> > - Authentication must be done without passwords or passphrases, using
>> >   ssh-agent.  Other authentication methods are not supported. (*)
>> >
>> > - Does not check host key. (*)
>> >
>> > - New remote files cannot be created. (*)
>>
>> Would be important to fix these limitations.  Authentication methods to
>> make this more usable.  Host key check for security.  File creation for
>> qemu-img.
>
> I agree.
>
>> > - Uses coroutine read/write, instead of true AIO.  (libssh2 supports
>> >   non-blocking access, so this could be fixed with some effort).
>>
>> This patch does not really use coroutines - the SSH I/O is blocking!
>>
>> Coroutines must submit the SSH I/O and then yield so the QEMU event loop
>> can get on with other work.  When SSH I/O finishes the request's
>> coroutine is re-entered and the request gets completed.
>
> Hmm OK.  Is there any documentation at all on how coroutines are
> supposed to work?  Or AIO for that matter?  For example, do coroutines
> really replace all the read/write syscalls deep inside a library
> (libssh2) so that these calls context switch, or am I missing the
> point of how this works entirely?

Before we go into coroutines, take a look at AIO since it may fit
libssh2's non-blocking mode of operation better.

Doing AIO means implementing .bdrv_aio_readv()/.bdrv_aio_writev().
These functions cannot block so you need to use non-blocking libssh2
interfaces and let QEMU's event loop notify us of
readability/writability (see qemu_aio_set_fd_handler()).

Look at block/iscsi.c:iscsi_aio_readv() for an example of how to
interface with an asynchronous library.

Back to coroutines.  Coroutines are just a primitive, like threads,
that your code can use.  They aren't a framework for how to do block
I/O.

If you want an example, take a look at block/sheepdog.c:

static coroutine_fn void do_co_req(void *opaque)
{
    int ret;
    Coroutine *co;
[...]
    co = qemu_coroutine_self();
    qemu_aio_set_fd_handler(sockfd, NULL, restart_co_req, have_co_req, co);

    ret = send_co_req(sockfd, hdr, data, wlen);

Using qemu_aio_set_fd_handler() we tell the event loop to call
restart_co_req() when the file descriptor becomes writable.

Then we call send_co_req() which yields if send(2) returns EAGAIN.

This is an example of setting up an event handler, invoking a
non-blocking syscall, and yielding on EAGAIN.  Here is what
restart_co_req() looks like:

static void restart_co_req(void *opaque)
{
    Coroutine *co = opaque;

    qemu_coroutine_enter(co, NULL);
}

If you want to make the code clean and reusable it's best to put the
event handler, non-blocking I/O, and yield/re-enter into a function
that can be reused.  That way each callers don't duplicate this
low-level code.

Hope this gives you an idea of how to use coroutines for block drivers.

>> > This is implemented using libssh2 on the client side.  The server just
>> > requires a regular ssh daemon with sftp-server support.  Most ssh
>> > daemons on Unix/Linux systems will work out of the box.
>>
>> How much of a win over sshfs is this?
>>
>> sshfs can be mounted by unprivileged users and QEMU accesses it like a
>> regular file.  So the sshfs approach already works today.
>
> Sure, but compared to having to install and set up sshfs and FUSE,
> using this is a lot simpler.  It's also potentially faster since it
> cuts out FUSE and context switching to the sshfs process.
>
> BTW I looked into the implementation of sshfs before starting this,
> and what it does is to run an 'ssh' client subprocess, then implements
> the sftp protocol by hand on top.  So using libssh2 cuts out *two*
> external processes (plus FUSE).

I see.  Benchmarks would be interesting once AIO or coroutines is implemented.

Stefan
Stefan Hajnoczi March 21, 2013, 7:35 p.m. UTC | #4
On Thu, Mar 21, 2013 at 2:38 PM, Richard W.M. Jones <rjones@redhat.com> wrote:
> From: "Richard W.M. Jones" <rjones@redhat.com>
>
>   qemu-system-x86_64 -drive file=ssh://hostname/some/image
>
> QEMU will ssh into 'hostname' and open '/some/image' which is made
> available as a standard block device.
>
> You can specify a username (ssh://user@host/...) and/or a port number
> (ssh://host:port/...).
>
> Current limitations:
>
> - Authentication must be done without passwords or passphrases, using
>   ssh-agent.  Other authentication methods are not supported. (*)
>
> - Does not check host key. (*)
>
> - New remote files cannot be created. (*)
>
> - Uses coroutine read/write, instead of true AIO.  (libssh2 supports
>   non-blocking access, so this could be fixed with some effort).
>
> - Blocks during connection and authentication.
>
> (*) = potentially easy fix
>
> This is implemented using libssh2 on the client side.  The server just
> requires a regular ssh daemon with sftp-server support.  Most ssh
> daemons on Unix/Linux systems will work out of the box.
> ---
>  block/Makefile.objs |   1 +
>  block/ssh.c         | 514 ++++++++++++++++++++++++++++++++++++++++++++++++++++
>  configure           |  47 +++++
>  qemu-doc.texi       |  28 +++
>  4 files changed, 590 insertions(+)
>  create mode 100644 block/ssh.c

Just noticed that libcurl supports sftp.

Did you try enabling sftp support in block/curl.c?  I think you just
need to add CURLPROTO_SFTP to #define PROTOCOLS.

Stefan
Richard W.M. Jones March 21, 2013, 8:31 p.m. UTC | #5
On Thu, Mar 21, 2013 at 08:35:29PM +0100, Stefan Hajnoczi wrote:
> On Thu, Mar 21, 2013 at 2:38 PM, Richard W.M. Jones <rjones@redhat.com> wrote:
> > From: "Richard W.M. Jones" <rjones@redhat.com>
> >
> >   qemu-system-x86_64 -drive file=ssh://hostname/some/image
> >
> > QEMU will ssh into 'hostname' and open '/some/image' which is made
> > available as a standard block device.
> >
> > You can specify a username (ssh://user@host/...) and/or a port number
> > (ssh://host:port/...).
> >
> > Current limitations:
> >
> > - Authentication must be done without passwords or passphrases, using
> >   ssh-agent.  Other authentication methods are not supported. (*)
> >
> > - Does not check host key. (*)
> >
> > - New remote files cannot be created. (*)
> >
> > - Uses coroutine read/write, instead of true AIO.  (libssh2 supports
> >   non-blocking access, so this could be fixed with some effort).
> >
> > - Blocks during connection and authentication.
> >
> > (*) = potentially easy fix
> >
> > This is implemented using libssh2 on the client side.  The server just
> > requires a regular ssh daemon with sftp-server support.  Most ssh
> > daemons on Unix/Linux systems will work out of the box.
> > ---
> >  block/Makefile.objs |   1 +
> >  block/ssh.c         | 514 ++++++++++++++++++++++++++++++++++++++++++++++++++++
> >  configure           |  47 +++++
> >  qemu-doc.texi       |  28 +++
> >  4 files changed, 590 insertions(+)
> >  create mode 100644 block/ssh.c
> 
> Just noticed that libcurl supports sftp.
> 
> Did you try enabling sftp support in block/curl.c?  I think you just
> need to add CURLPROTO_SFTP to #define PROTOCOLS.

Interestingly curl's sftp support is implemented using libssh2.  I'll
take a look at how easy this will be.

Rich.
Kevin Wolf March 25, 2013, 2:36 p.m. UTC | #6
Am 21.03.2013 um 14:38 hat Richard W.M. Jones geschrieben:
> From: "Richard W.M. Jones" <rjones@redhat.com>
> 
>   qemu-system-x86_64 -drive file=ssh://hostname/some/image
> 
> QEMU will ssh into 'hostname' and open '/some/image' which is made
> available as a standard block device.
> 
> You can specify a username (ssh://user@host/...) and/or a port number
> (ssh://host:port/...).
> 
> Current limitations:
> 
> - Authentication must be done without passwords or passphrases, using
>   ssh-agent.  Other authentication methods are not supported. (*)

Maybe you can just expose it as an encrypted image in order to allow
password authentication?

> - Does not check host key. (*)
> 
> - New remote files cannot be created. (*)
> 
> - Uses coroutine read/write, instead of true AIO.  (libssh2 supports
>   non-blocking access, so this could be fixed with some effort).
> 
> - Blocks during connection and authentication.
> 
> (*) = potentially easy fix
> 
> This is implemented using libssh2 on the client side.  The server just
> requires a regular ssh daemon with sftp-server support.  Most ssh
> daemons on Unix/Linux systems will work out of the box.

> --- /dev/null
> +++ b/block/ssh.c
> @@ -0,0 +1,514 @@
> +/*
> + * Secure Shell (ssh) backend for QEMU.
> + *
> + * Copyright (C) 2013 Red Hat Inc., Richard W.M. Jones <rjones@redhat.com>
> + *
> + * This work is licensed under the terms of the GNU GPL, version 2 or later.
> + * See the COPYING file in the top-level directory.
> + */

Please consider the MIT license or LGPL for block layer code; otherwise
we may have to compile the driver out for a future libqblock.

> +
> +#define SECTOR_SIZE 512

BDRV_SECTOR_SIZE exists.

> +
> +static void __attribute__((constructor)) ssh_init(void)
> +{
> +    int r;
> +
> +    r = libssh2_init(0);
> +    if (r != 0) {
> +        fprintf(stderr, "libssh2 initialization failed, %d\n", r);
> +        exit(EXIT_FAILURE);
> +    }
> +}

Why don't you include this in bdrv_ssh_init()?

> +static int parse_uri(BDRVSSHState *s, const char *filename)
> +{
> +    URI *uri;
> +
> +    uri = uri_parse(filename);
> +    if (!uri)
> +        return -EINVAL;
> +
> +    if (strcmp(uri->scheme, "ssh") != 0) {
> +        error_report("URI scheme must be 'ssh'");
> +        goto err;
> +    }
> +
> +    if (!uri->server || strcmp(uri->server, "") == 0) {
> +        error_report("missing hostname in URI");
> +        goto err;
> +    }
> +
> +    if (!uri->path || strcmp(uri->path, "") == 0) {
> +        error_report("missing remote path in URI");
> +        goto err;
> +    }
> +
> +    /* If user is defined in the URI, use it.  Else use local username. */
> +    if(uri->user && strcmp(uri->user, "") != 0)
> +        s->user = g_strdup(uri->user);
> +    else {
> +        s->user = get_username();
> +        if (!s->user)
> +            goto err;
> +    }
> +
> +    /* Construct the hostname:port string for inet_connect. */
> +    if(uri->port == 0)
> +        uri->port = 22;
> +    s->host = g_strdup_printf("%s:%d", uri->server, uri->port);
> +
> +    s->path = g_strdup(uri->path);
> +
> +    return 0;
> +
> + err:
> +    uri_free(uri);
> +    return -EINVAL;
> +}

You could implement this similar to what I just changed in NBD, using
the new .bdrv_parse_filename callback and driver-specific options, so
that instead of specifying a URL you can use something like:

    -drive file.driver=ssh,file.host=localhost,file.user=test

This should turn out much more elegant than management tools encoding
everything in a single filename string and qemu parsing it again,
especially once QMP support passing this as JSON objects.

> +static int ssh_file_open(BlockDriverState *bs, const char *filename,
> +                         int bdrv_flags)

After rebasing, this will have a new parameter for driver-specific
options.

> +    ret = parse_uri(s, filename);
> +    if (ret < 0)
> +        goto err;

I only see it now, but it seems to be repeated all over the place: The
qemu coding style requires braces here.

> +
> +    /* Open the socket and connect. */
> +    s->sock = inet_connect(s->host, &err);
> +    if (err != NULL) {
> +        ret = -errno;
> +        qerror_report_err(err);
> +        error_free(err);
> +        goto err;
> +    }
> +
> +    /* Start up SSH. */
> +    ret = connect_to_ssh(s);
> +    if (ret < 0)
> +        goto err;
> +
> +#if 0 /* Enable this when AIO callbacks are implemented. */
> +    /* Go non-blocking. */
> +    libssh2_session_set_blocking(s->session, 0);
> +#endif
> +
> +    qemu_co_mutex_init(&s->lock);
> +
> +    return 0;
> +
> + err:
> +    if (s->sock >= 0)
> +        close(s->sock);
> +    s->sock = -1;
> +
> +    g_free(s->path);
> +    s->path = NULL;
> +    g_free(s->host);
> +    s->host = NULL;
> +    g_free(s->user);
> +    s->user = NULL;

Resetting the fields isn't really necessary. s will be freed anyway.

> +static BlockDriver bdrv_ssh = {
> +    .format_name                  = "ssh",
> +    .protocol_name                = "ssh",
> +    .instance_size                = sizeof(BDRVSSHState),
> +    .bdrv_file_open               = ssh_file_open,
> +    .bdrv_close                   = ssh_close,
> +    .bdrv_read                    = ssh_co_read,
> +    .bdrv_write                   = ssh_co_write,
> +    .bdrv_getlength               = ssh_getlength,
> +#if 0
> +    .bdrv_aio_readv               = ssh_aio_readv,
> +    .bdrv_aio_writev              = ssh_aio_writev,
> +    .bdrv_aio_flush               = ssh_aio_flush,
> +#endif
> +};

You don't have a bdrv_co_flush_to_disk, what does this mean? Is every
write immediately flushed to the disk, is the driver unsafe by design,
or is this just a missing implementation detail?

Kevin
Richard W.M. Jones March 25, 2013, 3:11 p.m. UTC | #7
On Mon, Mar 25, 2013 at 03:36:22PM +0100, Kevin Wolf wrote:
> You don't have a bdrv_co_flush_to_disk, what does this mean? Is every
> write immediately flushed to the disk, is the driver unsafe by design,
> or is this just a missing implementation detail?

Initially there is no .bdrv_co_flush_to_disk because this patch is
just a proof of concept.

However it does seem likely that a true flush-to-disk operation is not
possible with sftp.  Assuming this is supposed to guarantee that the
bits have been committed to disk, I don't see anything in the sftp
protocol that supports that guarantee.  It seems to be left up to the
implementation to do whatever it wants.

Here's the protocol v3 as implemented by OpenSSH:

  http://tools.ietf.org/html/draft-ietf-secsh-filexfer-02

and here's the latest version v6 (not implemented by anyone AFAIK):

  http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13

if you think you can see anything that I've missed ...

Also I grepped over the source of OpenSSH and there is no call to
sync(2), fsync(2) or fdatasync(2).

Rich.
Stefan Hajnoczi March 26, 2013, 9:37 a.m. UTC | #8
On Mon, Mar 25, 2013 at 03:11:37PM +0000, Richard W.M. Jones wrote:
> On Mon, Mar 25, 2013 at 03:36:22PM +0100, Kevin Wolf wrote:
> > You don't have a bdrv_co_flush_to_disk, what does this mean? Is every
> > write immediately flushed to the disk, is the driver unsafe by design,
> > or is this just a missing implementation detail?
> 
> Initially there is no .bdrv_co_flush_to_disk because this patch is
> just a proof of concept.
> 
> However it does seem likely that a true flush-to-disk operation is not
> possible with sftp.  Assuming this is supposed to guarantee that the
> bits have been committed to disk, I don't see anything in the sftp
> protocol that supports that guarantee.  It seems to be left up to the
> implementation to do whatever it wants.
> 
> Here's the protocol v3 as implemented by OpenSSH:
> 
>   http://tools.ietf.org/html/draft-ietf-secsh-filexfer-02
> 
> and here's the latest version v6 (not implemented by anyone AFAIK):
> 
>   http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13
> 
> if you think you can see anything that I've missed ...
> 
> Also I grepped over the source of OpenSSH and there is no call to
> sync(2), fsync(2) or fdatasync(2).

I looked at OpenSSH sftp-server.c to see if open, close, or write do
anything special.  They don't.

There's also no 'sync' command in the SFTP protocol.

So unless the underlying filesystem is mounted -o sync, there is no
guarantee that data is safely on disk.

Stefan
diff mbox

Patch

diff --git a/block/Makefile.objs b/block/Makefile.objs
index c067f38..6c4b5bc 100644
--- a/block/Makefile.objs
+++ b/block/Makefile.objs
@@ -13,6 +13,7 @@  block-obj-$(CONFIG_LIBISCSI) += iscsi.o
 block-obj-$(CONFIG_CURL) += curl.o
 block-obj-$(CONFIG_RBD) += rbd.o
 block-obj-$(CONFIG_GLUSTERFS) += gluster.o
+block-obj-$(CONFIG_LIBSSH2) += ssh.o
 endif
 
 common-obj-y += stream.o
diff --git a/block/ssh.c b/block/ssh.c
new file mode 100644
index 0000000..7071cfd
--- /dev/null
+++ b/block/ssh.c
@@ -0,0 +1,514 @@ 
+/*
+ * Secure Shell (ssh) backend for QEMU.
+ *
+ * Copyright (C) 2013 Red Hat Inc., Richard W.M. Jones <rjones@redhat.com>
+ *
+ * This work is licensed under the terms of the GNU GPL, version 2 or later.
+ * See the COPYING file in the top-level directory.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+
+#ifndef _WIN32
+#include <pwd.h>
+#endif
+
+#include <libssh2.h>
+#include <libssh2_sftp.h>
+
+#include "block/block_int.h"
+#include "qemu/sockets.h"
+#include "qemu/uri.h"
+
+#define SECTOR_SIZE 512
+
+static void __attribute__((constructor)) ssh_init(void)
+{
+    int r;
+
+    r = libssh2_init(0);
+    if (r != 0) {
+        fprintf(stderr, "libssh2 initialization failed, %d\n", r);
+        exit(EXIT_FAILURE);
+    }
+}
+
+typedef struct BDRVSSHState {
+    CoMutex lock;               /* coroutine lock */
+
+    /* configuration */
+    char *user;                 /* remote username */
+    char *host;                 /* hostname and port */
+    char *path;                 /* path to remote disk image */
+
+    /* ssh connection */
+    int sock;                         /* socket */
+    LIBSSH2_SESSION *session;         /* ssh session */
+    LIBSSH2_SFTP *sftp;               /* sftp session */
+    LIBSSH2_SFTP_HANDLE *sftp_handle; /* sftp remote file handle */
+
+    /* file attributes (at open) */
+    LIBSSH2_SFTP_ATTRIBUTES attrs;
+} BDRVSSHState;
+
+/* Wrappers around error_report which make sure to dump as much
+ * information from libssh2 as possible.
+ */
+static void
+session_error_report(BDRVSSHState *s, const char *fs, ...)
+{
+    va_list args;
+
+    va_start(args, fs);
+
+    if ((s)->session) {
+        char *ssh_err;
+        int ssh_err_len = sizeof ssh_err;
+        int ssh_err_code;
+
+        libssh2_session_last_error((s)->session,
+                                   &ssh_err, &ssh_err_len, 0);
+        /* This is not an errno.  See <libssh2.h>. */
+        ssh_err_code = libssh2_session_last_errno((s)->session);
+
+        error_vprintf(fs, args);
+        error_printf(": %s (libssh2 error code: %d)", ssh_err, ssh_err_code);
+    }
+    else {
+        error_vprintf(fs, args);
+    }
+
+    va_end(args);
+    error_printf("\n");
+}
+
+static void
+sftp_error_report(BDRVSSHState *s, const char *fs, ...)
+{
+    va_list args;
+
+    va_start(args, fs);
+
+    if ((s)->sftp) {
+        char *ssh_err;
+        int ssh_err_len = sizeof ssh_err;
+        int ssh_err_code;
+        unsigned long sftp_err_code;
+
+        libssh2_session_last_error((s)->session,
+                                   &ssh_err, &ssh_err_len, 0);
+        /* This is not an errno.  See <libssh2.h>. */
+        ssh_err_code = libssh2_session_last_errno((s)->session);
+        /* See <libssh2_sftp.h>. */
+        sftp_err_code = libssh2_sftp_last_error((s)->sftp);
+
+        error_vprintf(fs, args);
+        error_printf(": %s (libssh2 error code: %d, sftp error code: %lu)",
+                     ssh_err, ssh_err_code, sftp_err_code);
+    }
+    else {
+        error_vprintf(fs, args);
+    }
+
+    va_end(args);
+    error_printf("\n");
+}
+
+#ifndef _WIN32
+static char *get_username(void)
+{
+    struct passwd *pwd;
+
+    pwd = getpwuid(geteuid ());
+    if (!pwd) {
+        error_report("failed to get current user name");
+        return NULL;
+    }
+    return g_strdup(pwd->pw_name);
+}
+#else
+static char *get_username(void)
+{
+    error_report("on Windows, specify a remote user name in the URI");
+    return NULL;
+}
+#endif
+
+static int parse_uri(BDRVSSHState *s, const char *filename)
+{
+    URI *uri;
+
+    uri = uri_parse(filename);
+    if (!uri)
+        return -EINVAL;
+
+    if (strcmp(uri->scheme, "ssh") != 0) {
+        error_report("URI scheme must be 'ssh'");
+        goto err;
+    }
+
+    if (!uri->server || strcmp(uri->server, "") == 0) {
+        error_report("missing hostname in URI");
+        goto err;
+    }
+
+    if (!uri->path || strcmp(uri->path, "") == 0) {
+        error_report("missing remote path in URI");
+        goto err;
+    }
+
+    /* If user is defined in the URI, use it.  Else use local username. */
+    if(uri->user && strcmp(uri->user, "") != 0)
+        s->user = g_strdup(uri->user);
+    else {
+        s->user = get_username();
+        if (!s->user)
+            goto err;
+    }
+
+    /* Construct the hostname:port string for inet_connect. */
+    if(uri->port == 0)
+        uri->port = 22;
+    s->host = g_strdup_printf("%s:%d", uri->server, uri->port);
+
+    s->path = g_strdup(uri->path);
+
+    return 0;
+
+ err:
+    uri_free(uri);
+    return -EINVAL;
+}
+
+static int connect_to_ssh(BDRVSSHState *s)
+{
+    int r, ret;
+    const char *userauthlist;
+    LIBSSH2_AGENT *agent = NULL;
+    struct libssh2_agent_publickey *identity;
+    struct libssh2_agent_publickey *prev_identity = NULL;
+
+    s->session = libssh2_session_init();
+    if (!s->session) {
+        ret = -EINVAL;
+        session_error_report(s, "failed to initialize libssh2 session");
+        goto err;
+    }
+
+    r = libssh2_session_handshake(s->session, s->sock);
+    if (r != 0) {
+        ret = -EINVAL;
+        session_error_report(s, "failed to establish SSH session");
+        goto err;
+    }
+
+#if 0
+    /* Check the remote host's key against known_hosts. */
+    ret = check_host_key (s);
+    if (ret < 0)
+        goto err;
+#endif
+
+    /* Authenticate. */
+    userauthlist = libssh2_userauth_list(s->session, s->user, strlen(s->user));
+    if (strstr(userauthlist, "publickey") == NULL) {
+        ret = -EPERM;
+        error_report("remote server does not support \"publickey\" authentication");
+        goto err;
+    }
+
+    /* Connect to ssh-agent and try each identity in turn. */
+    agent = libssh2_agent_init(s->session);
+    if (!agent) {
+        ret = -EINVAL;
+        session_error_report(s, "failed to initialize ssh-agent support");
+        goto err;
+    }
+    if (libssh2_agent_connect(agent)) {
+        ret = -ECONNREFUSED;
+        session_error_report(s, "failed to connect to ssh-agent");
+        goto err;
+    }
+    if (libssh2_agent_list_identities(agent)) {
+        ret = -EINVAL;
+        session_error_report(s, "failed requesting identities from ssh-agent");
+        goto err;
+    }
+
+    for(;;) {
+        r = libssh2_agent_get_identity(agent, &identity, prev_identity);
+        if (r == 1)             /* end of list */
+            break;
+        if (r < 0) {
+            ret = -EINVAL;
+            session_error_report(s, "failed to obtain identity from ssh-agent");
+            goto err;
+        }
+        r = libssh2_agent_userauth(agent, s->user, identity);
+        if (r == 0)
+            goto authenticated;
+        /* Failed to authenticate with this identity, try the next one. */
+        prev_identity = identity;
+    }
+
+    ret = -EPERM;
+    error_report("failed to authenticate using publickey authentication "
+                 "and the identities held by your ssh-agent");
+    goto err;
+
+ authenticated:
+    libssh2_agent_disconnect(agent);
+    libssh2_agent_free(agent);
+    agent = NULL;
+
+    /* Start SFTP. */
+    s->sftp = libssh2_sftp_init(s->session);
+    if (!s->sftp) {
+        session_error_report(s, "failed to initialize sftp handle");
+        ret = -EINVAL;
+        goto err;
+    }
+
+    s->sftp_handle = libssh2_sftp_open(s->sftp, s->path,
+                                       LIBSSH2_FXF_READ|LIBSSH2_FXF_WRITE,
+                                       0);
+    if (!s->sftp_handle) {
+        session_error_report(s, "failed to open remote file '%s'", s->path);
+        ret = -EINVAL;
+        goto err;
+    }
+
+    r = libssh2_sftp_fstat(s->sftp_handle, &s->attrs);
+    if (r < 0) {
+        sftp_error_report(s, "failed to read file attributes of '%s'",
+                          s->path);
+        ret = -EINVAL;
+        goto err;
+    }
+
+    return 0;
+
+ err:
+    if (s->sftp_handle)
+        libssh2_sftp_close(s->sftp_handle);
+    s->sftp_handle = NULL;
+    if (s->sftp)
+        libssh2_sftp_shutdown(s->sftp);
+    s->sftp = NULL;
+    if (s->session) {
+        libssh2_session_disconnect(s->session,
+                                   "from qemu ssh client: "
+                                   "error opening connection");
+        libssh2_session_free(s->session);
+    }
+    s->session = NULL;
+
+    if (agent) {
+        libssh2_agent_disconnect(agent);
+        libssh2_agent_free(agent);
+    }
+
+    return ret;
+}
+
+static int ssh_file_open(BlockDriverState *bs, const char *filename,
+                         int bdrv_flags)
+{
+    BDRVSSHState *s = bs->opaque;
+    int ret;
+    Error *err = NULL;
+
+    s->sock = -1;
+
+    ret = parse_uri(s, filename);
+    if (ret < 0)
+        goto err;
+
+    /* Open the socket and connect. */
+    s->sock = inet_connect(s->host, &err);
+    if (err != NULL) {
+        ret = -errno;
+        qerror_report_err(err);
+        error_free(err);
+        goto err;
+    }
+
+    /* Start up SSH. */
+    ret = connect_to_ssh(s);
+    if (ret < 0)
+        goto err;
+
+#if 0 /* Enable this when AIO callbacks are implemented. */
+    /* Go non-blocking. */
+    libssh2_session_set_blocking(s->session, 0);
+#endif
+
+    qemu_co_mutex_init(&s->lock);
+
+    return 0;
+
+ err:
+    if (s->sock >= 0)
+        close(s->sock);
+    s->sock = -1;
+
+    g_free(s->path);
+    s->path = NULL;
+    g_free(s->host);
+    s->host = NULL;
+    g_free(s->user);
+    s->user = NULL;
+
+    return ret;
+}
+
+static void ssh_close(BlockDriverState *bs)
+{
+    BDRVSSHState *s = bs->opaque;
+
+    if (s->sftp_handle)
+        libssh2_sftp_close(s->sftp_handle);
+    if (s->sftp)
+        libssh2_sftp_shutdown(s->sftp);
+    if (s->session) {
+        libssh2_session_disconnect(s->session,
+                                   "from qemu ssh client: "
+                                   "user closed the connection");
+        libssh2_session_free(s->session);
+    }
+    if (s->sock >= 0)
+        close(s->sock);
+
+    g_free(s->path);
+    g_free(s->host);
+    g_free(s->user);
+}
+
+static coroutine_fn int ssh_read(BDRVSSHState *s,
+                                int64_t offset, char *buf, size_t size)
+{
+    ssize_t r;
+    size_t got;
+
+    /* According to the docs, this just updates a field in the
+     * sftp_handle structure, so there is no network traffic and it
+     * cannot fail.
+     */
+    libssh2_sftp_seek64(s->sftp_handle, offset);
+
+    /* libssh2 has a hard-coded limit of 2000 bytes per request,
+     * although it will also do readahead behind our backs.  Therefore
+     * we may have to do repeated reads here until we have read 'size'
+     * bytes.
+     */
+    for (got = 0; got < size; ) {
+    again:
+        r = libssh2_sftp_read(s->sftp_handle, buf + got, size - got);
+
+        if (r == LIBSSH2_ERROR_EAGAIN || r == LIBSSH2_ERROR_TIMEOUT)
+            goto again;
+        if (r < 0) {
+            sftp_error_report(s, "%s: read failed", s->path);
+            return -EIO;
+        }
+        if (r == 0) {
+            error_report("%s: read failed (EOF)", s->path);
+            return -EIO;
+        }
+
+        got += r;
+    }
+
+    return 0;
+}
+
+static coroutine_fn int ssh_co_read(BlockDriverState *bs,
+                                    int64_t sector_num,
+                                    uint8_t *buf, int nb_sectors)
+{
+    BDRVSSHState *s = bs->opaque;
+    int ret;
+
+    qemu_co_mutex_lock(&s->lock);
+    ret = ssh_read(s, sector_num * SECTOR_SIZE, (char *) buf,
+                   nb_sectors * SECTOR_SIZE);
+    qemu_co_mutex_unlock(&s->lock);
+
+    return ret;
+}
+
+static int ssh_write(BDRVSSHState *s,
+                     int64_t offset, const char *buf, size_t size)
+{
+    ssize_t r;
+    size_t written;
+
+    /* According to the docs, this just updates a field in the
+     * sftp_handle structure, so there is no network traffic and it
+     * cannot fail.
+     */
+    libssh2_sftp_seek64(s->sftp_handle, offset);
+
+    for (written = 0; written < size; ) {
+    again:
+        r = libssh2_sftp_write(s->sftp_handle, buf + written, size - written);
+
+        if (r == LIBSSH2_ERROR_EAGAIN || r == LIBSSH2_ERROR_TIMEOUT)
+            goto again;
+        if (r < 0) {
+            sftp_error_report(s, "%s: write failed", s->path);
+            return -EIO;
+        }
+
+        written += r;
+    }
+
+    return 0;
+}
+
+static coroutine_fn int ssh_co_write(BlockDriverState *bs,
+                                     int64_t sector_num,
+                                     const uint8_t *buf, int nb_sectors)
+{
+    BDRVSSHState *s = bs->opaque;
+    int ret;
+
+    qemu_co_mutex_lock(&s->lock);
+    ret = ssh_write(s, sector_num * SECTOR_SIZE, (const char *)buf,
+                    nb_sectors * SECTOR_SIZE);
+
+    qemu_co_mutex_unlock(&s->lock);
+
+    return ret;
+}
+
+static int64_t ssh_getlength(BlockDriverState *bs)
+{
+    BDRVSSHState *s = bs->opaque;
+    return (int64_t) s->attrs.filesize;
+}
+
+static BlockDriver bdrv_ssh = {
+    .format_name                  = "ssh",
+    .protocol_name                = "ssh",
+    .instance_size                = sizeof(BDRVSSHState),
+    .bdrv_file_open               = ssh_file_open,
+    .bdrv_close                   = ssh_close,
+    .bdrv_read                    = ssh_co_read,
+    .bdrv_write                   = ssh_co_write,
+    .bdrv_getlength               = ssh_getlength,
+#if 0
+    .bdrv_aio_readv               = ssh_aio_readv,
+    .bdrv_aio_writev              = ssh_aio_writev,
+    .bdrv_aio_flush               = ssh_aio_flush,
+#endif
+};
+
+static void bdrv_ssh_init(void)
+{
+    bdrv_register(&bdrv_ssh);
+}
+
+block_init(bdrv_ssh_init);
diff --git a/configure b/configure
index 46a7594..4ea1b39 100755
--- a/configure
+++ b/configure
@@ -229,6 +229,7 @@  virtio_blk_data_plane=""
 gtk=""
 gtkabi="2.0"
 tpm="no"
+libssh2=""
 
 # parse CC options first
 for opt do
@@ -908,6 +909,10 @@  for opt do
   ;;
   --enable-tpm) tpm="yes"
   ;;
+  --disable-libssh2) libssh2="no"
+  ;;
+  --enable-libssh2) libssh2="yes"
+  ;;
   *) echo "ERROR: unknown option $opt"; show_help="yes"
   ;;
   esac
@@ -1164,6 +1169,7 @@  echo "  --disable-glusterfs      disable GlusterFS backend"
 echo "  --enable-gcov            enable test coverage analysis with gcov"
 echo "  --gcov=GCOV              use specified gcov [$gcov_tool]"
 echo "  --enable-tpm             enable TPM support"
+echo "  --enable-libssh2         enable ssh block device support"
 echo ""
 echo "NOTE: The object files are built at the place where configure is launched"
 exit 1
@@ -2328,6 +2334,42 @@  EOF
 fi
 
 ##########################################
+# libssh2 probe
+if test "$libssh2" != "no" ; then
+  cat > $TMPC <<EOF
+#include <stdio.h>
+#include <libssh2.h>
+#include <libssh2_sftp.h>
+int main(void) {
+    LIBSSH2_SESSION *session;
+    session = libssh2_session_init ();
+    (void) libssh2_sftp_init (session);
+    return 0;
+}
+EOF
+
+  if $pkg_config libssh2 --modversion >/dev/null 2>&1; then
+    libssh2_cflags=`$pkg_config libssh2 --cflags`
+    libssh2_libs=`$pkg_config libssh2 --libs`
+  else
+    libssh2_cflags=
+    libssh2_libs="-lssh2"
+  fi
+
+  if compile_prog "$libssh2_cflags" "$libssh2_libs" ; then
+    libssh2=yes
+    libs_tools="$libssh2_libs $libs_tools"
+    libs_softmmu="$libssh2_libs $libs_softmmu"
+    QEMU_CFLAGS="$QEMU_CFLAGS $libssh2_cflags"
+  else
+    if test "$libssh2" = "yes" ; then
+      feature_not_found "libssh2"
+    fi
+    libssh2=no
+  fi
+fi
+
+##########################################
 # linux-aio probe
 
 if test "$linux_aio" != "no" ; then
@@ -3439,6 +3481,7 @@  echo "virtio-blk-data-plane $virtio_blk_data_plane"
 echo "gcov              $gcov_tool"
 echo "gcov enabled      $gcov"
 echo "TPM support       $tpm"
+echo "libssh2 support   $libssh2"
 
 if test "$sdl_too_old" = "yes"; then
 echo "-> Your SDL version is too old - please upgrade to have SDL support"
@@ -3804,6 +3847,10 @@  if test "$glusterfs" = "yes" ; then
   echo "CONFIG_GLUSTERFS=y" >> $config_host_mak
 fi
 
+if test "$libssh2" = "yes" ; then
+  echo "CONFIG_LIBSSH2=y" >> $config_host_mak
+fi
+
 if test "$virtio_blk_data_plane" = "yes" ; then
   echo "CONFIG_VIRTIO_BLK_DATA_PLANE=y" >> $config_host_mak
 fi
diff --git a/qemu-doc.texi b/qemu-doc.texi
index af84bef..004b2ff 100644
--- a/qemu-doc.texi
+++ b/qemu-doc.texi
@@ -423,6 +423,7 @@  snapshots.
 * disk_images_sheepdog::      Sheepdog disk images
 * disk_images_iscsi::         iSCSI LUNs
 * disk_images_gluster::       GlusterFS disk images
+* disk_images_ssh::           Secure Shell (ssh) disk images
 @end menu
 
 @node disk_images_quickstart
@@ -1038,6 +1039,33 @@  qemu-system-x86_64 -drive file=gluster+unix:///testvol/dir/a.img?socket=/tmp/glu
 qemu-system-x86_64 -drive file=gluster+rdma://1.2.3.4:24007/testvol/a.img
 @end example
 
+@node disk_images_ssh
+@subsection Secure Shell (ssh) disk images
+
+You can access disk images located on a remote ssh server
+by using the ssh protocol:
+@example
+qemu-system-x86_64 -drive file=ssh://[@var{user}@@]@var{server}[:@var{port}]/@var{path}
+@end example
+
+@var{ssh} is the protocol.
+
+@var{user} is the remote user.  If not specified, then the local
+username is tried.
+
+@var{server} specifies the remote ssh server.  Any ssh server can be
+used, but it must implement the sftp-server protocol.  Most Unix/Linux
+systems should work without requiring any extra configuration.
+
+@var{port} is the port number on which sshd is listening.  By default
+the standard ssh port (22) is used.
+
+@var{path} is the path to the disk image.
+
+Authentication must currently be done without requiring a password or
+passphrase.  Set up ssh-agent with authorized keys, or Kerberos or
+similar.
+
 @node pcsys_network
 @section Network emulation