[Libguestfs] [nbdkit PATCH 1/5] api: Add .default_export

Eric Blake
2020-08-25T15:46:55Z

I'm about to add an 'exportname' filter, and in the process, I
noticed
a few shortcomings in our API.  Time to fix those before the 1.22
release locks our API in stone.  Overloading .list_exports in order to
determine a canonical export name at .open time is awkward; the two
uses (answering NBD_OPT_LIST for a full list, vs. remapping a client's
"" into a canonical name during .open) are orthogonal enough to
warrant separate plugin callbacks.  This will also make it easier to
express the notion of no default export (connecting to "" is an error)
at the same time as listing other exports.  Another consideration is
that when tls=1, the choice of export to expose pre-TLS vs. post-TLS
may differ, but without a call to .open yet, our just-added
nbdkit_is_tls() does not fit our preferred lifecycle, so this has to
be a parameter to the new .default_export.  We will alter the
signature of .list_exports soon; in the meantime, the bool
default_only parameter is now ignored.

Adding .default_export support to sh/eval is big enough for a separate
patch.

The ondemand plugin continues to advertise "" (and not
"default") in
its list of exports, but now reports a canonical name of "default"
when connecting to it.

The tls-fallback filter can now avoid leaking a default export name
through an insecure connection.

The test-layers.c changes demonstrate how fragile it is to maintain
our own naive client for probing which aspects of the plugin have been
reached; the test could probably be improved by using libnbd 1.4 APIs.
---
 docs/nbdkit-filter.pod               | 28 ++++++++++++-----
 docs/nbdkit-plugin.pod               | 37 +++++++++++++++++++++++
 include/nbdkit-filter.h              |  9 ++++--
 include/nbdkit-plugin.h              |  1 +
 server/internal.h                    |  7 +++--
 server/backend.c                     | 45 ++++++++++++++++++----------
 server/exports.c                     |  8 +----
 server/filters.c                     | 12 ++++++++
 server/plugins.c                     | 22 ++++++++++++--
 server/protocol-handshake-newstyle.c |  9 +++++-
 plugins/ondemand/ondemand.c          | 11 +++++--
 filters/tls-fallback/tls-fallback.c  | 12 +++++++-
 tests/test-eval-exports.sh           |  3 +-
 tests/test-layers-filter.c           |  9 ++++++
 tests/test-layers-plugin.c           |  8 +++++
 tests/test-layers.c                  | 24 ++++++++-------
 16 files changed, 192 insertions(+), 53 deletions(-)

diff --git a/docs/nbdkit-filter.pod b/docs/nbdkit-filter.pod
index 94cebccd..dd667c0c 100644
--- a/docs/nbdkit-filter.pod
+++ b/docs/nbdkit-filter.pod
@@ -137,9 +137,9 @@ C<nxdata> passed to C<.open> has a stable
lifetime that lasts to the
 corresponding C<.close>, with all intermediate functions (such as
 C<.pread>) receiving the same value for convenience; the only
 exceptions where C<nxdata> is not reused are C<.config>,
-C<.config_complete>, C<.get_ready>, C<.after_fork>,
C<.preconnect> and
-C<.list_exports>, which are called outside the lifetime of a
-connection.
+C<.config_complete>, C<.get_ready>, C<.after_fork>,
C<.preconnect>,
+C<.list_exports>, and C<.default_export>, which are called outside
+the lifetime of a connection.

 =head2 Next config, open and close

@@ -355,7 +355,7 @@ from the layer below.  Without error checking it would look
like this:
    struct nbdkit_export e;
    char *name, *desc;

-   exports2 = nbdkit_exports_new (default_only);
+   exports2 = nbdkit_exports_new ();
    next_list_exports (nxdata, readonly, default_only, exports);
    for (i = 0; i < nbdkit_exports_count (exports2); ++i) {
      e = nbdkit_get_export (exports2, i);
@@ -376,11 +376,9 @@ an error message and return C<-1>.
 Two functions are provided to filters only for allocating and freeing
 the list:

- struct nbdkit_exports *nbdkit_exports_new (int default_only);
+ struct nbdkit_exports *nbdkit_exports_new (void);

-Allocates and returns a new, empty exports list.  The C<default_only>
-parameter should match whether the list is intended to grab the
-canonical name of the default export, or all exports.
+Allocates and returns a new, empty exports list.

 On error this function can return C<NULL>.  In this case it calls
 C<nbdkit_error> as required.  C<errno> will be set to a suitable
@@ -408,6 +406,20 @@ Returns the number of exports in the list.

Returns a copy of the C'th export.

+=head2 C<.default_export>
+
+ const char *default_export (nbdkit_next_default_export *next, void *nxdata,
+                             int readonly, int is_tls)
+
+This intercepts the plugin C<.default_export> method and can be used to
+alter the canonical export name used in place of the default
C<"">.
+
+The C<readonly> parameter matches what is passed to <.preconnect>
and
+C<.open>, and may be changed by the filter when calling into the
+plugin.  The C<is_tls> parameter informs the filter whether TLS
+negotiation has been completed by the client, but is not passed on to
+C<next> because it cannot be altered.
+
 =head2 C<.open>

  void * (*open) (nbdkit_next_open *next, void *nxdata,
diff --git a/docs/nbdkit-plugin.pod b/docs/nbdkit-plugin.pod
index 5c641e83..64e3197b 100644
--- a/docs/nbdkit-plugin.pod
+++ b/docs/nbdkit-plugin.pod
@@ -245,6 +245,12 @@ Early in option negotiation the client may try to list the
exports
 served by the plugin, and plugins can optionally implement this
 callback to answer the client.  See L</EXPORT NAME> below.

+=item C<.default_export>
+
+During option negotiation, if the client requests the default export
+name (C<"">), this optional callback provides a canonical name
to
+use in its place prior to calling C<.open>.
+
 =item C<.open>

 A new client has connected and finished the NBD handshake.  TLS
@@ -709,6 +715,37 @@ Returning C<0> will send the list of exports back to
the client.  If
 there is an error, C<.list_exports> should call C<nbdkit_error>
with
 an error message and return C<-1>.

+=head2 C<.default_export>
+
+ const char *default_export (int readonly, int is_tls);
+
+This optional callback is called if the client tries to connect to
+the default export C<"">.  If the plugin does not supply this
callback,
+the connection continues with the empty name; if the plugin returns
+a string, nbdkit returns that name to clients that support it (see
+the C<NBD_INFO_NAME> response to C<NBD_OPT_GO>), and behaves as if
+the client had passed that string instead of an empty name.  Similarly,
+if the plugin does not supply a C<.list_exports> callback, the result
+of this callback determines what export name to advertise to a client
+requesting an export list.
+
+The C<readonly> flag informs the plugin that the server was started
+with the I<-r> flag on the command line, which is the same value
+passed to C<.preconnect> and C<.open>.  However, the NBD protocol
does
+not yet have a way to let the client advertise an intent to be
+read-only even when the server allows writes, so this parameter may
+not be as useful as it appears.
+
+The C<is_tls> flag informs the plugin whether the canonical name for
+the default export is being requested after the client has completed
+TLS negotiation.  When running the server in a mode that permits but
+not requires TLS, be careful that a default export name does not leak
+unintended information.
+
+If the plugin returns C<NULL>, the client is not permitted to connect
+to the default export.  However, this is not an error in the protocol,
+so it is not necessary to call C<nbdkit_error>.
+
 =head2 C<.open>

  void *open (int readonly);
diff --git a/include/nbdkit-filter.h b/include/nbdkit-filter.h
index b4024ae5..2c5b36be 100644
--- a/include/nbdkit-filter.h
+++ b/include/nbdkit-filter.h
@@ -66,8 +66,10 @@ typedef int nbdkit_next_get_ready (nbdkit_backend *nxdata);
 typedef int nbdkit_next_after_fork (nbdkit_backend *nxdata);
 typedef int nbdkit_next_preconnect (nbdkit_backend *nxdata, int readonly);
 typedef int nbdkit_next_list_exports (nbdkit_backend *nxdata, int readonly,
-                                      int default_only,
+                                      int ignored,
                                       struct nbdkit_exports *exports);
+typedef const char *nbdkit_next_default_export (nbdkit_backend *nxdata,
+                                                int readonly);
 typedef int nbdkit_next_open (nbdkit_backend *nxdata,
                               int readonly, const char *exportname);

@@ -136,7 +138,7 @@ struct nbdkit_export {
 };

 NBDKIT_EXTERN_DECL (struct nbdkit_exports *, nbdkit_exports_new,
-                    (int default_only));
+                    (void));
 NBDKIT_EXTERN_DECL (void, nbdkit_exports_free, (struct nbdkit_exports *));
 NBDKIT_EXTERN_DECL (size_t, nbdkit_exports_count,
                     (const struct nbdkit_exports *));
@@ -179,6 +181,9 @@ struct nbdkit_filter {
   int (*list_exports) (nbdkit_next_list_exports *next, nbdkit_backend *nxdata,
                        int readonly, int default_only,
                        struct nbdkit_exports *exports);
+  const char * (*default_export) (nbdkit_next_default_export *next,
+                                  nbdkit_backend *nxdata,
+                                  int readonly, int is_tls);

   void * (*open) (nbdkit_next_open *next, nbdkit_backend *nxdata,
                   int readonly, const char *exportname, int is_tls);
diff --git a/include/nbdkit-plugin.h b/include/nbdkit-plugin.h
index a5d85411..28e83757 100644
--- a/include/nbdkit-plugin.h
+++ b/include/nbdkit-plugin.h
@@ -142,6 +142,7 @@ struct nbdkit_plugin {

   int (*list_exports) (int readonly, int default_only,
                        struct nbdkit_exports *exports);
+  const char * (*default_export) (int readonly, int is_tls);
 };

 NBDKIT_EXTERN_DECL (void, nbdkit_set_error, (int err));
diff --git a/server/internal.h b/server/internal.h
index d043225a..8c8448e6 100644
--- a/server/internal.h
+++ b/server/internal.h
@@ -365,8 +365,9 @@ struct backend {
   void (*get_ready) (struct backend *);
   void (*after_fork) (struct backend *);
   int (*preconnect) (struct backend *, int readonly);
-  int (*list_exports) (struct backend *, int readonly, int default_only,
+  int (*list_exports) (struct backend *, int readonly, int ignored,
                        struct nbdkit_exports *exports);
+  const char *(*default_export) (struct backend *, int readonly, int is_tls);
   void *(*open) (struct backend *, int readonly, const char *exportname,
                  int is_tls);
   int (*prepare) (struct backend *, void *handle, int readonly);
@@ -413,9 +414,11 @@ extern void backend_unload (struct backend *b, void
(*unload) (void))
   __attribute__((__nonnull__ (1)));

 extern int backend_list_exports (struct backend *b, int readonly,
-                                 int default_only,
+                                 int ignored,
                                  struct nbdkit_exports *exports)
   __attribute__((__nonnull__ (1, 4)));
+extern const char *backend_default_export (struct backend *b, int readonly)
+  __attribute__((__nonnull__ (1)));
 /* exportname is only valid for this call and almost certainly will be
  * freed on return of this function, so backends must save the
  * exportname if they need to refer to it later.
diff --git a/server/backend.c b/server/backend.c
index 2ca40d61..c5eef456 100644
--- a/server/backend.c
+++ b/server/backend.c
@@ -166,13 +166,12 @@ backend_list_exports (struct backend *b, int readonly, int
default_only,
   struct handle *h = get_handle (conn, b->i);
   int r;

+  assert (!default_only); /* XXX Switch to is_tls... */
   controlpath_debug ("%s: list_exports readonly=%d default_only=%d",
                      b->name, readonly, default_only);

   assert (h->handle == NULL);
   assert ((h->state & HANDLE_OPEN) == 0);
-  if (default_only && h->default_exportname)
-    return nbdkit_add_export (exports, h->default_exportname, NULL);

   r = b->list_exports (b, readonly, default_only, exports);
   if (r == -1)
@@ -180,13 +179,34 @@ backend_list_exports (struct backend *b, int readonly, int
default_only,
   else {
     size_t count = nbdkit_exports_count (exports);
     controlpath_debug ("%s: list_exports returned %zu names",
b->name, count);
-    /* Best effort caching of default export name */
-    if (!h->default_exportname && count)
-      h->default_exportname = strdup (nbdkit_get_export (exports, 0).name);
   }
   return r;
 }

+const char *
+backend_default_export (struct backend *b, int readonly)
+{
+  GET_CONN;
+  struct handle *h = get_handle (conn, b->i);
+  const char *s;
+
+  controlpath_debug ("%s: default_export readonly=%d tls=%d",
+                     b->name, readonly, conn->using_tls);
+
+  if (h->default_exportname == NULL) {
+    assert (h->handle == NULL);
+    assert ((h->state & HANDLE_OPEN) == 0);
+    s = b->default_export (b, readonly, conn->using_tls);
+    if (s) {
+      /* Best effort caching */
+      h->default_exportname = strdup (s);
+      if (h->default_exportname == NULL)
+        return s;
+    }
+  }
+  return h->default_exportname;
+}
+
 int
 backend_open (struct backend *b, int readonly, const char *exportname)
 {
@@ -202,18 +222,13 @@ backend_open (struct backend *b, int readonly, const char
*exportname)
   if (readonly)
     h->can_write = 0;

-  /* Best-effort determination of the canonical name for default export */
+  /* Determine the canonical name for default export */
   if (!*exportname) {
-    if (!h->default_exportname) {
-      CLEANUP_EXPORTS_FREE struct nbdkit_exports *exps = NULL;
-
-      exps = nbdkit_exports_new (true);
-      if (exps && b->list_exports (b, readonly, true, exps) == 0
&&
-          nbdkit_exports_count (exps))
-        h->default_exportname = strdup (nbdkit_get_export (exps, 0).name);
+    exportname = backend_default_export (b, readonly);
+    if (exportname == NULL) {
+      nbdkit_error ("default export (\"\") not permitted");
+      return -1;
     }
-    if (h->default_exportname)
-      exportname = h->default_exportname;
   }

   /* Most filters will call next_open first, resulting in
diff --git a/server/exports.c b/server/exports.c
index 3f819622..8d3faec4 100644
--- a/server/exports.c
+++ b/server/exports.c
@@ -52,12 +52,10 @@ DEFINE_VECTOR_TYPE(exports, struct nbdkit_export);

 struct nbdkit_exports {
   exports exports;
-
-  bool default_only;
 };

 struct nbdkit_exports *
-nbdkit_exports_new (int default_only)
+nbdkit_exports_new (void)
 {
   struct nbdkit_exports *r;

@@ -67,7 +65,6 @@ nbdkit_exports_new (int default_only)
     return NULL;
   }
   r->exports = (exports) empty_vector;
-  r->default_only = default_only != 0;
   return r;
 }

@@ -107,9 +104,6 @@ nbdkit_add_export (struct nbdkit_exports *exps,
 {
   struct nbdkit_export e = { NULL, NULL };

-  if (exps->default_only && exps->exports.size == 1)
-    return 0;
-
   if (exps->exports.size == MAX_EXPORTS) {
     nbdkit_error ("nbdkit_add_export: too many exports");
     errno = EINVAL;
diff --git a/server/filters.c b/server/filters.c
index 0cfae344..bb22be76 100644
--- a/server/filters.c
+++ b/server/filters.c
@@ -249,6 +249,17 @@ filter_list_exports (struct backend *b, int readonly, int
default_only,
   return backend_list_exports (b->next, readonly, default_only, exports);
 }

+static const char *
+filter_default_export (struct backend *b, int readonly, int is_tls)
+{
+  struct backend_filter *f = container_of (b, struct backend_filter, backend);
+
+  if (f->filter.default_export)
+    return f->filter.default_export (backend_default_export, b->next,
+                                     readonly, is_tls);
+  return backend_default_export (b->next, readonly);
+}
+
 static void *
 filter_open (struct backend *b, int readonly, const char *exportname,
              int is_tls)
@@ -555,6 +566,7 @@ static struct backend filter_functions = {
   .after_fork = filter_after_fork,
   .preconnect = filter_preconnect,
   .list_exports = filter_list_exports,
+  .default_export = filter_default_export,
   .open = filter_open,
   .prepare = filter_prepare,
   .finalize = filter_finalize,
diff --git a/server/plugins.c b/server/plugins.c
index 218764da..924533cb 100644
--- a/server/plugins.c
+++ b/server/plugins.c
@@ -162,6 +162,7 @@ plugin_dump_fields (struct backend *b)
   HAS (after_fork);
   HAS (preconnect);
   HAS (list_exports);
+  HAS (default_export);
   HAS (open);
   HAS (close);
   HAS (get_size);
@@ -282,15 +283,29 @@ static int
 plugin_list_exports (struct backend *b, int readonly, int default_only,
                      struct nbdkit_exports *exports)
 {
-  GET_CONN;
   struct backend_plugin *p = container_of (b, struct backend_plugin, backend);

-  if (!p->plugin.list_exports)
-    return nbdkit_add_export (exports, "", NULL);
+  if (!p->plugin.list_exports) {
+    const char *def = backend_default_export (b, readonly);
+    if (def == NULL)
+      return 0;
+    return nbdkit_add_export (exports, def, NULL);
+  }

   return p->plugin.list_exports (readonly, default_only, exports);
 }

+static const char *
+plugin_default_export (struct backend *b, int readonly, int is_tls)
+{
+  struct backend_plugin *p = container_of (b, struct backend_plugin, backend);
+
+  if (!p->plugin.default_export)
+    return "";
+
+  return p->plugin.default_export (readonly, is_tls);
+}
+
 static void *
 plugin_open (struct backend *b, int readonly, const char *exportname,
              int is_tls)
@@ -757,6 +772,7 @@ static struct backend plugin_functions = {
   .after_fork = plugin_after_fork,
   .preconnect = plugin_preconnect,
   .list_exports = plugin_list_exports,
+  .default_export = plugin_default_export,
   .open = plugin_open,
   .prepare = plugin_prepare,
   .finalize = plugin_finalize,
diff --git a/server/protocol-handshake-newstyle.c
b/server/protocol-handshake-newstyle.c
index 8f41c7a8..6e5f3822 100644
--- a/server/protocol-handshake-newstyle.c
+++ b/server/protocol-handshake-newstyle.c
@@ -85,7 +85,7 @@ send_newstyle_option_reply_exportnames (uint32_t option)
   size_t i;
   CLEANUP_EXPORTS_FREE struct nbdkit_exports *exps = NULL;

-  exps = nbdkit_exports_new (false);
+  exps = nbdkit_exports_new ();
   if (exps == NULL)
     return send_newstyle_option_reply (option, NBD_REP_ERR_TOO_BIG);
   if (backend_list_exports (top, read_only, false, exps) == -1)
@@ -301,6 +301,7 @@ negotiate_handshake_newstyle_options (void)
   struct nbd_export_name_option_reply handshake_finish;
   const char *optname;
   uint64_t exportsize;
+  struct backend *b;

   for (nr_options = 0; nr_options < MAX_NR_OPTIONS; ++nr_options) {
     CLEANUP_FREE char *data = NULL;
@@ -445,6 +446,12 @@ negotiate_handshake_newstyle_options (void)
           return -1;
         conn->using_tls = true;
         debug ("using TLS on this connection");
+        /* Wipe out any cached default export name. */
+        for_each_backend (b) {
+          struct handle *h = get_handle (conn, b->i);
+          free (h->default_exportname);
+          h->default_exportname = NULL;
+        }
       }
       break;

diff --git a/plugins/ondemand/ondemand.c b/plugins/ondemand/ondemand.c
index f9d73ca6..5bd0ee70 100644
--- a/plugins/ondemand/ondemand.c
+++ b/plugins/ondemand/ondemand.c
@@ -219,6 +219,13 @@ ondemand_list_exports (int readonly, int default_only,
   return 0;
 }

+static const char *
+ondemand_default_export (int readonly, int is_tls)
+{
+  /* We always accept "" as an export name; canonicalize it to
"default". */
+  return "default";
+}
+
 struct handle {
   int fd;
   int64_t size;
@@ -362,8 +369,7 @@ ondemand_open (int readonly)
     nbdkit_error ("internal error: expected nbdkit_export_name () !=
NULL");
     goto error;
   }
-  if (strcmp (h->exportname, "") == 0)
-    h->exportname = "default";
+  assert (strcmp (h->exportname, "") != 0); /* see .default_export
*/

   /* Verify that the export name is valid. */
   if (strlen (h->exportname) > NAME_MAX ||
@@ -621,6 +627,7 @@ static struct nbdkit_plugin plugin = {
   .get_ready         = ondemand_get_ready,

   .list_exports      = ondemand_list_exports,
+  .default_export    = ondemand_default_export,

   .can_multi_conn    = ondemand_can_multi_conn,
   .can_trim          = ondemand_can_trim,
diff --git a/filters/tls-fallback/tls-fallback.c
b/filters/tls-fallback/tls-fallback.c
index 822748b4..0fcc2bcf 100644
--- a/filters/tls-fallback/tls-fallback.c
+++ b/filters/tls-fallback/tls-fallback.c
@@ -76,6 +76,15 @@ tls_fallback_get_ready (nbdkit_next_get_ready *next, void
*nxdata,

 /* TODO: list_exports needs is_tls parameter */

+static const char *
+tls_fallback_default_export (nbdkit_next_default_export *next, void *nxdata,
+                             int readonly, int is_tls)
+{
+  if (!is_tls)
+    return "";
+  return next (nxdata, readonly);
+}
+
 /* Helper for determining if this connection is insecure.  This works
  * because we can treat all handles on a binary basis: secure or
  * insecure, which lets .open get away without allocating a more
@@ -183,7 +192,8 @@ static struct nbdkit_filter filter = {
   .config            = tls_fallback_config,
   .config_help       = tls_fallback_config_help,
   .get_ready         = tls_fallback_get_ready,
-  /* XXX .init_exports needs is_tls parameter */
+  /* XXX .list_exports needs is_tls parameter */
+  .default_export    = tls_fallback_default_export,
   .open              = tls_fallback_open,
   .get_size          = tls_fallback_get_size,
   .can_write         = tls_fallback_can_write,
diff --git a/tests/test-eval-exports.sh b/tests/test-eval-exports.sh
index aa694aaa..7c946378 100755
--- a/tests/test-eval-exports.sh
+++ b/tests/test-eval-exports.sh
@@ -67,7 +67,8 @@ do_nbdkit ()
     # Check how the default export name is handled
     # nbdinfo currently makes multiple connections, so we can't use the
     # long-running server for validating default export name.
-    nbdkit -U - -v eval list_exports="cat
'$PWD/eval-exports.list'" \
+    # XXX FIXME: requires .default_export in eval
+    : || nbdkit -U - -v eval list_exports="cat
'$PWD/eval-exports.list'" \
       open='[ "$3" = "'"$1"'" ] || {
echo EINVAL wrong export >&2; exit 1; }' \
       get_size='echo 0' --run 'nbdsh -u "$uri" -c
"exit()"'
     # Check what exports are listed
diff --git a/tests/test-layers-filter.c b/tests/test-layers-filter.c
index 3f295588..f6acb448 100644
--- a/tests/test-layers-filter.c
+++ b/tests/test-layers-filter.c
@@ -115,6 +115,14 @@ test_layers_filter_list_exports (nbdkit_next_list_exports
*next, void *nxdata,
   return next (nxdata, readonly, default_only, exports);
 }

+static const char *
+test_layers_filter_default_export (nbdkit_next_default_export *next,
+                                   void *nxdata, int readonly, int is_tls)
+{
+  DEBUG_FUNCTION;
+  return next (nxdata, readonly);
+}
+
 static void *
 test_layers_filter_open (nbdkit_next_open *next, void *nxdata,
                          int readonly, const char *exportname, int is_tls)
@@ -380,6 +388,7 @@ static struct nbdkit_filter filter = {
   .after_fork        = test_layers_filter_after_fork,
   .preconnect        = test_layers_filter_preconnect,
   .list_exports      = test_layers_filter_list_exports,
+  .default_export    = test_layers_filter_default_export,
   .open              = test_layers_filter_open,
   .close             = test_layers_filter_close,
   .prepare           = test_layers_filter_prepare,
diff --git a/tests/test-layers-plugin.c b/tests/test-layers-plugin.c
index 1dfd069e..254244eb 100644
--- a/tests/test-layers-plugin.c
+++ b/tests/test-layers-plugin.c
@@ -101,6 +101,13 @@ test_layers_plugin_list_exports (int readonly, int
default_only,
   return nbdkit_add_export (exports, "", NULL);
 }

+static const char *
+test_layers_plugin_default_export (int readonly, int is_tls)
+{
+  DEBUG_FUNCTION;
+  return "";
+}
+
 static void *
 test_layers_plugin_open (int readonly)
 {
@@ -257,6 +264,7 @@ static struct nbdkit_plugin plugin = {
   .after_fork        = test_layers_plugin_after_fork,
   .preconnect        = test_layers_plugin_preconnect,
   .list_exports      = test_layers_plugin_list_exports,
+  .default_export    = test_layers_plugin_default_export,
   .open              = test_layers_plugin_open,
   .close             = test_layers_plugin_close,
   .get_size          = test_layers_plugin_get_size,
diff --git a/tests/test-layers.c b/tests/test-layers.c
index dd826f36..fa7730e6 100644
--- a/tests/test-layers.c
+++ b/tests/test-layers.c
@@ -326,19 +326,21 @@ main (int argc, char *argv[])
      "test_layers_plugin_preconnect",
      NULL);

-  /* list_exports methods called in outer-to-inner order, complete
-   * in inner-to-outer order.  But since we didn't send NBD_OPT_LIST,
-   * the outer filter does not expose a list; rather, the rest of the
-   * chain is used to resolve the canonical name of the default export.
+  /* XXX We should test NBD_OPT_INFO here for coverage of
+   * .list_exports.  However it would be easier to do by using libnbd
+   * than open-coding a naive client.
    */
+
+  /* .default_export methods called in outer-to-inner order. */
   log_verify_seen_in_order
-    ("filter3: test_layers_filter_list_exports",
-     "testlayersfilter2: list_exports",
-     "filter2: test_layers_filter_list_exports",
-     "testlayersfilter1: list_exports",
-     "filter1: test_layers_filter_list_exports",
-     "testlayersplugin: list_exports",
-     "test_layers_plugin_list_exports",
+    ("testlayersfilter3: default_export",
+     "filter3: test_layers_filter_default_export",
+     "testlayersfilter2: default_export",
+     "filter2: test_layers_filter_default_export",
+     "testlayersfilter1: default_export",
+     "filter1: test_layers_filter_default_export",
+     "testlayersplugin: default_export",
+     "test_layers_plugin_default_export",
      NULL);

   /* open methods called in outer-to-inner order, but thanks to next
--
2.28.0