---
.gitignore | 1 +
TODO | 10 +--
docs/nbdkit.pod | 38 +++++++-
src/connections.c | 235 ++++++++++++++++++++++++++++++++++++++++++++++++--
src/internal.h | 1 +
src/main.c | 15 +++-
src/protocol.h | 49 ++++++++++-
tests/Makefile.am | 8 ++
tests/test-newstyle.c | 102 ++++++++++++++++++++++
9 files changed, 440 insertions(+), 19 deletions(-)
create mode 100644 tests/test-newstyle.c
diff --git a/.gitignore b/.gitignore
index b1a8850..9ea072f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,6 +43,7 @@ Makefile.in
/tests/test-connect
/tests/test-file
/tests/test-gzip
+/tests/test-newstyle
/tests/test-ocaml
/tests/test-ocaml-plugin.so
/tests/test-perl
diff --git a/TODO b/TODO
index a05aa5b..d39e64c 100644
--- a/TODO
+++ b/TODO
@@ -17,9 +17,9 @@
* Performance - measure and improve it.
-* Implement the new protocol and export names. With export names it
- should be possible to have multiple plugins on the command line
- (each responding to a different export of course):
+* Implement export names. With export names it should be possible to
+ have multiple plugins on the command line (each responding to a
+ different export of course):
nbdkit --export /foo plugin.so --export /bar another-plugin.so
@@ -27,8 +27,8 @@
default that accepts all exportnames, or to divide the export name
"space" up using regexps or wildcards.
- Annoyingly nbd-client dropped support for the oldstyle protocol (see
-
https://bugzilla.redhat.com/1297100).
+ Export names are not actually paths (although that is how they are
+ often used), but arbitrary UTF-8 text strings.
* Implement true parallel request handling. Currently
NBDKIT_THREAD_MODEL_SERIALIZE_REQUESTS and
diff --git a/docs/nbdkit.pod b/docs/nbdkit.pod
index 9ce75d3..28ac490 100644
--- a/docs/nbdkit.pod
+++ b/docs/nbdkit.pod
@@ -7,8 +7,8 @@ nbdkit - A toolkit for creating NBD servers
=head1 SYNOPSIS
nbdkit [--dump-config] [-f] [-g GROUP] [-i IPADDR]
- [-P PIDFILE] [-p PORT] [-r] [--run CMD] [-s]
- [-U SOCKET] [-u USER] [-v] [-V]
+ [--new-style] [-P PIDFILE] [-p PORT] [-r]
+ [--run CMD] [-s] [-U SOCKET] [-u USER] [-v] [-V]
PLUGIN [key=value [key=value [...]]]
=head1 DESCRIPTION
@@ -103,6 +103,15 @@ See also I<-u>.
Listen on the specified interface. The default is to listen on all
interfaces. See also I<-p>.
+=item B<-n>
+
+=item B<--new-style>
+
+=item B<--newstyle>
+
+Use the newstyle NBD protocol instead of the default (oldstyle)
+protocol. See L</NEW STYLE VS OLD STYLE PROTOCOL> below.
+
=item B<-P> PIDFILE
=item B<--pid-file> PIDFILE
@@ -280,6 +289,31 @@ Unix socket, like this:
nbdkit -U - plugin [args] --run '...'
+=head1 NEW STYLE VS OLD STYLE PROTOCOL
+
+The NBD protocol comes in two incompatible forms that we call
+"oldstyle" and "newstyle".
+
+Unfortunately which protocol you should use depends on the client and
+cannot be known in advance (despite claims to the contrary).
+
+nbdkit defaults to the oldstyle protocol for compatibility with qemu
+and qemu-nbd. Use the I<-n> or I<--new-style> flag on the command
+line to use the newstyle protocol.
+
+If you want to claim compatibility with what the NBD proto.txt
+document says should be the case (which isn't in reality), then you
+should always use I<-n> when using port 10809, and use oldstyle
+otherwise.
+
+If the client is nbd-client E<ge> 3.10 then you must use the newstyle
+protocol (add the I<-n> flag on the command line), else you will see
+this error:
+
+ Error: It looks like you're trying to connect to an oldstyle server.
+
+nbdkit ignores export names at present (see also the C<TODO> file).
+
=head1 SIGNALS
C<nbdkit> responds to the following signals:
diff --git a/src/connections.c b/src/connections.c
index 15f416b..6bdf4ef 100644
--- a/src/connections.c
+++ b/src/connections.c
@@ -1,5 +1,5 @@
/* nbdkit
- * Copyright (C) 2013 Red Hat Inc.
+ * Copyright (C) 2013-2016 Red Hat Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -52,6 +52,12 @@
/* Maximum read or write request that we will handle. */
#define MAX_REQUEST_SIZE (64 * 1024 * 1024)
+/* Maximum number of client options we allow before giving up. */
+#define MAX_NR_OPTIONS 32
+
+/* Maximum length of any option data (bytes). */
+#define MAX_OPTION_LENGTH 4096
+
static struct connection *new_connection (int sockin, int sockout);
static void free_connection (struct connection *conn);
static int negotiate_handshake (struct connection *conn);
@@ -143,11 +149,8 @@ free_connection (struct connection *conn)
free (conn);
}
-/* XXX Note because we don't support multiple plugins or export names,
- * we are using the old-style handshake. This will be fixed.
- */
static int
-_negotiate_handshake (struct connection *conn)
+_negotiate_handshake_oldstyle (struct connection *conn)
{
struct old_handshake handshake;
int64_t r;
@@ -201,7 +204,8 @@ _negotiate_handshake (struct connection *conn)
conn->can_trim = 1;
}
- debug ("flags: global 0x%x export 0x%x", gflags, eflags);
+ debug ("oldstyle negotiation: flags: global 0x%x export 0x%x",
+ gflags, eflags);
memset (&handshake, 0, sizeof handshake);
memcpy (handshake.nbdmagic, "NBDMAGIC", 8);
@@ -218,13 +222,230 @@ _negotiate_handshake (struct connection *conn)
return 0;
}
+/* Receive newstyle options.
+ *
+ * Currently we ignore NBD_OPT_EXPORT_NAME (see TODO), we close the
+ * connection if sent NBD_OPT_ABORT, we send a canned list of
+ * options for NBD_OPT_LIST, and we send NBD_REP_ERR_UNSUP for
+ * everything else.
+ */
+static int
+send_newstyle_option_reply (struct connection *conn,
+ uint32_t option, uint32_t reply)
+{
+ struct fixed_new_option_reply fixed_new_option_reply;
+
+ fixed_new_option_reply.magic = htobe64 (NEW_OPTION_REPLY);
+ fixed_new_option_reply.option = htobe32 (option);
+ fixed_new_option_reply.reply = htobe32 (reply);
+ fixed_new_option_reply.replylen = htobe32 (0);
+
+ if (xwrite (conn->sockout,
+ &fixed_new_option_reply, sizeof fixed_new_option_reply) == -1) {
+ nbdkit_error ("write: %m");
+ return -1;
+ }
+
+ return 0;
+}
+
+static int
+_negotiate_handshake_newstyle_options (struct connection *conn)
+{
+ struct new_option new_option;
+ size_t nr_options;
+ uint64_t version;
+ uint32_t option;
+ uint32_t optlen;
+ char data[MAX_OPTION_LENGTH+1];
+
+ for (nr_options = 0; nr_options < MAX_NR_OPTIONS; ++nr_options) {
+ if (xread (conn->sockin, &new_option, sizeof new_option) == -1) {
+ nbdkit_error ("read: %m");
+ return -1;
+ }
+
+ version = be64toh (new_option.version);
+ if (version != NEW_VERSION) {
+ nbdkit_error ("unknown option version %" PRIx64
+ ", expecting %" PRIx64,
+ version, NEW_VERSION);
+ return -1;
+ }
+
+ /* There is a maximum option length we will accept, regardless
+ * of the option type.
+ */
+ optlen = be32toh (new_option.optlen);
+ if (optlen > MAX_OPTION_LENGTH) {
+ nbdkit_error ("client option data too long (%" PRIu32 ")",
optlen);
+ return -1;
+ }
+
+ option = be32toh (new_option.option);
+ switch (option) {
+ case NBD_OPT_EXPORT_NAME:
+ if (xread (conn->sockin, data, optlen) == -1) {
+ nbdkit_error ("read: %m");
+ return -1;
+ }
+ /* Apart from printing it, ignore the export name. */
+ data[optlen] = '\0';
+ debug ("newstyle negotiation: client requested export '%s'
(ignored)",
+ data);
+ break;
+
+ case NBD_OPT_ABORT:
+ if (send_newstyle_option_reply (conn, option, NBD_REP_ACK) == -1)
+ return -1;
+ nbdkit_error ("client sent NBD_OPT_ABORT to abort the connection");
+ return -1;
+
+ case NBD_OPT_LIST:
+ if (optlen != 0) {
+ if (send_newstyle_option_reply (conn, option, NBD_REP_ERR_INVALID)
+ == -1)
+ return -1;
+ continue;
+ }
+
+ /* Since we don't support export names, there is nothing to list. */
+ if (send_newstyle_option_reply (conn, option, NBD_REP_ACK) == -1)
+ return -1;
+ break;
+
+ default:
+ /* Unknown option. */
+ if (send_newstyle_option_reply (conn, option, NBD_REP_ERR_UNSUP) == -1)
+ return -1;
+ }
+
+ /* Note, since it's not very clear from the protocol doc, that the
+ * client must send NBD_OPT_EXPORT_NAME last, and that ends option
+ * negotiation.
+ */
+ if (option == NBD_OPT_EXPORT_NAME)
+ break;
+ }
+
+ if (nr_options >= MAX_NR_OPTIONS) {
+ nbdkit_error ("client exceeded maximum number of options (%d)",
+ MAX_NR_OPTIONS);
+ return -1;
+ }
+
+ return 0;
+}
+
+static int
+_negotiate_handshake_newstyle (struct connection *conn)
+{
+ struct new_handshake handshake;
+ uint16_t gflags;
+ uint32_t cflags;
+ struct new_handshake_finish handshake_finish;
+ int64_t r;
+ uint64_t exportsize;
+ uint16_t eflags;
+ int fl;
+
+ gflags = NBD_FLAG_FIXED_NEWSTYLE;
+
+ debug ("newstyle negotiation: flags: global 0x%x", gflags);
+
+ memcpy (handshake.nbdmagic, "NBDMAGIC", 8);
+ handshake.version = htobe64 (NEW_VERSION);
+ handshake.gflags = htobe16 (gflags);
+
+ if (xwrite (conn->sockout, &handshake, sizeof handshake) == -1) {
+ nbdkit_error ("write: %m");
+ return -1;
+ }
+
+ /* Client now sends us its 32 bit flags word ... */
+ if (xread (conn->sockin, &cflags, sizeof cflags) == -1) {
+ nbdkit_error ("read: %m");
+ return -1;
+ }
+ cflags = be32toh (cflags);
+ /* ... which other than printing out, we ignore. */
+ debug ("newstyle negotiation: client flags: 0x%x", cflags);
+
+ /* Receive newstyle options. */
+ if (_negotiate_handshake_newstyle_options (conn) == -1)
+ return -1;
+
+ /* Finish the newstyle handshake. */
+ r = plugin_get_size (conn);
+ if (r == -1)
+ return -1;
+ if (r < 0) {
+ nbdkit_error (".get_size function returned invalid value "
+ "(%" PRIi64 ")", r);
+ return -1;
+ }
+ exportsize = (uint64_t) r;
+ conn->exportsize = exportsize;
+
+ eflags = NBD_FLAG_HAS_FLAGS;
+
+ fl = plugin_can_write (conn);
+ if (fl == -1)
+ return -1;
+ if (readonly || !fl) {
+ eflags |= NBD_FLAG_READ_ONLY;
+ conn->readonly = 1;
+ }
+
+ fl = plugin_can_flush (conn);
+ if (fl == -1)
+ return -1;
+ if (fl) {
+ eflags |= NBD_FLAG_SEND_FLUSH | NBD_FLAG_SEND_FUA;
+ conn->can_flush = 1;
+ }
+
+ fl = plugin_is_rotational (conn);
+ if (fl == -1)
+ return -1;
+ if (fl) {
+ eflags |= NBD_FLAG_ROTATIONAL;
+ conn->is_rotational = 1;
+ }
+
+ fl = plugin_can_trim (conn);
+ if (fl == -1)
+ return -1;
+ if (fl) {
+ eflags |= NBD_FLAG_SEND_TRIM;
+ conn->can_trim = 1;
+ }
+
+ debug ("newstyle negotiation: flags: export 0x%x", eflags);
+
+ memset (&handshake_finish, 0, sizeof handshake_finish);
+ handshake_finish.exportsize = htobe64 (exportsize);
+ handshake_finish.eflags = htobe16 (eflags);
+
+ if (xwrite (conn->sockout,
+ &handshake_finish, sizeof handshake_finish) == -1) {
+ nbdkit_error ("write: %m");
+ return -1;
+ }
+
+ return 0;
+}
+
static int
negotiate_handshake (struct connection *conn)
{
int r;
plugin_lock_request (conn);
- r = _negotiate_handshake (conn);
+ if (!newstyle)
+ r = _negotiate_handshake_oldstyle (conn);
+ else
+ r = _negotiate_handshake_newstyle (conn);
plugin_unlock_request (conn);
return r;
diff --git a/src/internal.h b/src/internal.h
index c834b14..0603779 100644
--- a/src/internal.h
+++ b/src/internal.h
@@ -49,6 +49,7 @@
/* main.c */
extern const char *ipaddr;
+extern int newstyle;
extern const char *port;
extern int readonly;
extern char *unixsocket;
diff --git a/src/main.c b/src/main.c
index 1248a8e..21b0595 100644
--- a/src/main.c
+++ b/src/main.c
@@ -1,5 +1,5 @@
/* nbdkit
- * Copyright (C) 2013-2014 Red Hat Inc.
+ * Copyright (C) 2013-2016 Red Hat Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -67,6 +67,7 @@ static gid_t parsegroup (const char *);
int foreground; /* -f */
const char *ipaddr; /* -i */
+int newstyle; /* -n */
int listen_stdin; /* -s */
char *pidfile; /* -P */
const char *port; /* -p */
@@ -83,7 +84,7 @@ static char *random_fifo = NULL;
enum { HELP_OPTION = CHAR_MAX + 1 };
-static const char *short_options = "fg:i:p:P:rsu:U:vV";
+static const char *short_options = "fg:i:np:P:rsu:U:vV";
static const struct option long_options[] = {
{ "help", 0, NULL, HELP_OPTION },
{ "dump-config",0, NULL, 0 },
@@ -92,6 +93,8 @@ static const struct option long_options[] = {
{ "group", 1, NULL, 'g' },
{ "ip-addr", 1, NULL, 'i' },
{ "ipaddr", 1, NULL, 'i' },
+ { "new-style", 1, NULL, 'n' },
+ { "newstyle", 1, NULL, 'n' },
{ "pid-file", 1, NULL, 'P' },
{ "pidfile", 1, NULL, 'P' },
{ "port", 1, NULL, 'p' },
@@ -111,8 +114,8 @@ static void
usage (void)
{
printf ("nbdkit [--dump-config] [-f] [-g GROUP] [-i IPADDR]\n"
- " [-P PIDFILE] [-p PORT] [-r] [--run CMD] [-s]\n"
- " [-U SOCKET] [-u USER] [-v] [-V]\n"
+ " [--new-style] [-P PIDFILE] [-p PORT] [-r]\n"
+ " [--run CMD] [-s] [-U SOCKET] [-u USER] [-v] [-V]\n"
" PLUGIN [key=value [key=value [...]]]\n"
"\n"
"Please read the nbdkit(1) manual page for full usage.\n");
@@ -180,6 +183,10 @@ main (int argc, char *argv[])
ipaddr = optarg;
break;
+ case 'n':
+ newstyle = 1;
+ break;
+
case 'P':
pidfile = nbdkit_absolute_path (optarg);
if (pidfile == NULL)
diff --git a/src/protocol.h b/src/protocol.h
index 4449171..2f9a341 100644
--- a/src/protocol.h
+++ b/src/protocol.h
@@ -36,7 +36,7 @@
#include <stdint.h>
-/* Old-style handshake */
+/* Old-style handshake. */
struct old_handshake {
char nbdmagic[8]; /* "NBDMAGIC" */
uint64_t version; /* OLD_VERSION, in network byte order */
@@ -48,6 +48,41 @@ struct old_handshake {
#define OLD_VERSION UINT64_C(0x420281861253)
+/* New-style handshake. */
+struct new_handshake {
+ char nbdmagic[8]; /* "NBDMAGIC" */
+ uint64_t version; /* NEW_VERSION, in network byte order */
+ uint16_t gflags; /* global flags, in network byte order */
+} __attribute__((packed));
+
+#define NEW_VERSION UINT64_C(0x49484156454F5054)
+
+/* New-style handshake option (sent by the client to us). */
+struct new_option {
+ uint64_t version; /* NEW_VERSION, in network byte order */
+ uint32_t option; /* NBD_OPT_* */
+ uint32_t optlen; /* option data length */
+ /* option data follows */
+} __attribute__((packed));
+
+/* Fixed newstyle handshake reply message. */
+struct fixed_new_option_reply {
+ uint64_t magic; /* NEW_OPTION_REPLY, network byte order */
+ uint32_t option; /* option we are replying to */
+ uint32_t reply; /* NBD_REP_* */
+ uint32_t replylen; /* we always send zero at the moment */
+ /* reply data follows, but we currently never send any */
+};
+
+#define NEW_OPTION_REPLY UINT64_C(0x3e889045565a9)
+
+/* New-style handshake server reply. */
+struct new_handshake_finish {
+ uint64_t exportsize; /* in network byte order */
+ uint16_t eflags; /* per-export flags, in network byte order */
+ char zeroes[124]; /* must be sent as zero bytes */
+} __attribute__((packed));
+
/* Global flags. */
#define NBD_FLAG_FIXED_NEWSTYLE 1
@@ -59,6 +94,18 @@ struct old_handshake {
#define NBD_FLAG_ROTATIONAL 16
#define NBD_FLAG_SEND_TRIM 32
+/* NBD options (new style handshake only). */
+#define NBD_OPT_EXPORT_NAME 1
+#define NBD_OPT_ABORT 2
+#define NBD_OPT_LIST 3
+
+#define NBD_REP_ACK 1
+#define NBD_REP_SERVER 2
+#define NBD_REP_ERR_UNSUP 0x80000001
+#define NBD_REP_ERR_POLICY 0x80000002
+#define NBD_REP_ERR_INVALID 0x80000003
+#define NBD_REP_ERR_PLATFORM 0x80000004
+
/* Request (client -> server). */
struct request {
uint32_t magic; /* NBD_REQUEST_MAGIC. */
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 511c39c..8d27032 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -103,6 +103,14 @@ file-data:
for f in `seq 1 512`; do echo -ne '\x01\x02\x03\x04\x05\x06\x07\x08'; done >
$@-t
mv $@-t $@
+# newstyle protocol test.
+check_PROGRAMS += test-newstyle
+TESTS += test-newstyle
+
+test_newstyle_SOURCES = test-newstyle.c test.h
+test_newstyle_CFLAGS = $(WARNINGS_CFLAGS) $(LIBGUESTFS_CFLAGS)
+test_newstyle_LDADD = libtest.la $(LIBGUESTFS_LIBS)
+
# gzip plugin test.
if HAVE_ZLIB
if HAVE_GUESTFISH
diff --git a/tests/test-newstyle.c b/tests/test-newstyle.c
new file mode 100644
index 0000000..b1d8ce7
--- /dev/null
+++ b/tests/test-newstyle.c
@@ -0,0 +1,102 @@
+/* nbdkit
+ * Copyright (C) 2013-2016 Red Hat Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of Red Hat nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY RED HAT AND CONTRIBUTORS ''AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL RED HAT OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+ * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+ * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <inttypes.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <guestfs.h>
+
+#include "test.h"
+
+int
+main (int argc, char *argv[])
+{
+ guestfs_h *g;
+ int r;
+ char *data;
+ size_t i, size;
+
+ if (test_start_nbdkit ("-n", NBDKIT_PLUGIN ("file"),
"file=file-data",
+ NULL) == -1)
+ exit (EXIT_FAILURE);
+
+ g = guestfs_create ();
+ if (g == NULL) {
+ perror ("guestfs_create");
+ exit (EXIT_FAILURE);
+ }
+
+ /* Using any exportname causes qemu to use the newstyle protocol. */
+ r = guestfs_add_drive_opts (g, "/" /* exportname */,
+ GUESTFS_ADD_DRIVE_OPTS_FORMAT, "raw",
+ GUESTFS_ADD_DRIVE_OPTS_PROTOCOL, "nbd",
+ GUESTFS_ADD_DRIVE_OPTS_SERVER, server,
+ -1);
+ if (r == -1)
+ exit (EXIT_FAILURE);
+
+ if (guestfs_launch (g) == -1)
+ exit (EXIT_FAILURE);
+
+ /* Check the data in the file is \x01-\x08 repeated 512 times. */
+ data = guestfs_pread_device (g, "/dev/sda", 8 * 512, 0, &size);
+ if (!data)
+ exit (EXIT_FAILURE);
+ if (size != 8 * 512) {
+ fprintf (stderr, "%s FAILED: unexpected size (actual: %zu, expected:
512)\n",
+ program_name, size);
+ exit (EXIT_FAILURE);
+ }
+
+ for (i = 0; i < 512 * 8; i += 8) {
+ if (data[i] != 1 || data[i+1] != 2 ||
+ data[i+2] != 3 || data[i+3] != 4 ||
+ data[i+4] != 5 || data[i+5] != 6 ||
+ data[i+6] != 7 || data[i+7] != 8) {
+ fprintf (stderr, "%s FAILED: unexpected data returned at offset %zu\n",
+ program_name, i);
+ exit (EXIT_FAILURE);
+ }
+ }
+
+ free (data);
+
+ guestfs_close (g);
+ exit (EXIT_SUCCESS);
+}
--
2.5.0