Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package libnbd for openSUSE:Factory checked 
in at 2026-06-10 15:51:41
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/libnbd (Old)
 and      /work/SRC/openSUSE:Factory/.libnbd.new.2375 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "libnbd"

Wed Jun 10 15:51:41 2026 rev:29 rq:1358246 version:1.25.5

Changes:
--------
--- /work/SRC/openSUSE:Factory/libnbd/libnbd.changes    2026-03-19 
17:39:02.001926428 +0100
+++ /work/SRC/openSUSE:Factory/.libnbd.new.2375/libnbd.changes  2026-06-10 
15:51:59.841844322 +0200
@@ -1,0 +2,47 @@
+Tue Jun 09 16:13:39 UTC 2026 - Charles Arnold <[email protected]>
+
+- Update to version 1.25.5:
+  * Version 1.25.5.
+  * docs: Improve documentation for nbd_add_close_callback(3)
+  * Rename nbd_set_close_callback to nbd_add_close_callback
+  * python: Test nbd.set_close_callback method
+  * ocaml: Add a test of NBD.set_close_callback
+  * tests: Add a test of new nbd_set_close_callback function
+  * lib: Add new API to call a close callback on nbd_close
+  * ocaml: Extend generator to support no-argument closures
+  * docs/nbd_create.pod: Introduce link to libnbd(3) early
+  * Version 1.25.4.
+  * golang: Rename more exported functions
+  * Version 1.25.3.
+  * tests/connect-tcp.c: Add trivial test of keepalive features
+  * lib: Add a new API to allow setting TCP options on the socket
+  * fuse: Add --keepalive flag
+  * ublk: Add --keepalive flag
+  * ublk/nbdublk.pod: Simplify duplicated sections of the synopsis
+  * lib: Add new APIs to set and get keepalives on the socket
+  * ci: Rebuild CI files
+  * ci/manifest.yml: Skip Rust on some platforms
+  * ci: Allow Rust to be skipped by setting variable $RUST == "skip"
+  * ci/manifest.yml: Drop freebsd-13, add freebsd-15
+  * ci/manifest.yml: Add Alpine 3.23
+  * ci/manifest.yml: Drop Alpine 3.21
+  * golang: Rename global freeCallbackId to nbd_internal_freeCallbackId
+  * generator: Compile-time error if a reserved word is used
+  * python/utils.c: Split up the long get_sockaddr function
+  * dump/nbddump.pod: Link to nbdkit_debug_hexdump(3)
+  * Version 1.25.2.
+  * tests/socket-activation-name.c: Fix test for nbdkit >= 1.47.5
+  * lib/utils.c: Fix whitespace
+  * fuse/operations.c: Remove out of date comment
+  * python: Check for URI support before running the new tests
+  * Version 1.25.1.
+  * python: Add new tests to EXTRA_DIST
+  * python: Test h.aio_connect using an IP address
+  * python: Test h.aio_connect with old and new Unix domain socket formats
+  * python/utils.c: Implement parsing of IP addresses for nbd.aio_connect
+  * tests/aio-connect-port.c: Delete this test
+  * tests/aio-connect-port.c: Replace broken link in comment
+  * configure.ac: Remove use of "which" command
+  * info: Add nbdinfo --can open
+
+-------------------------------------------------------------------

Old:
----
  libnbd-1.24.2.tar.bz2

New:
----
  libnbd-1.25.5.tar.bz2

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ libnbd.spec ++++++
--- /var/tmp/diff_new_pack.eWsJBR/_old  2026-06-10 15:52:00.953890405 +0200
+++ /var/tmp/diff_new_pack.eWsJBR/_new  2026-06-10 15:52:00.957890570 +0200
@@ -19,7 +19,7 @@
 %define sover 0
 
 Name:           libnbd
-Version:        1.24.2
+Version:        1.25.5
 Release:        0
 Summary:        NBD client library in userspace
 License:        LGPL-2.1-or-later

++++++ _service ++++++
--- /var/tmp/diff_new_pack.eWsJBR/_old  2026-06-10 15:52:00.993892062 +0200
+++ /var/tmp/diff_new_pack.eWsJBR/_new  2026-06-10 15:52:00.997892228 +0200
@@ -1,7 +1,7 @@
 <services>
   <service name="tar_scm" mode="manual">
     <param name="filename">libnbd</param>
-    <param name="revision">v1.24.2</param>
+    <param name="revision">v1.25.5</param>
     <param name="scm">git</param>
     <param name="submodules">disable</param>
     <param name="url">https://gitlab.com/nbdkit/libnbd.git</param>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.eWsJBR/_old  2026-06-10 15:52:01.025893388 +0200
+++ /var/tmp/diff_new_pack.eWsJBR/_new  2026-06-10 15:52:01.029893554 +0200
@@ -1,6 +1,6 @@
 <servicedata>
 <service name="tar_scm">
                 <param name="url">https://gitlab.com/nbdkit/libnbd.git</param>
-              <param 
name="changesrevision">091731b81e2a9f494db392ad32b1ff6bdca4a471</param></service></servicedata>
+              <param 
name="changesrevision">2a0d2262bfd805c4b35b63eb2fcfffb38dfaed71</param></service></servicedata>
 (No newline at EOF)
 

++++++ libnbd-1.24.2.tar.bz2 -> libnbd-1.25.5.tar.bz2 ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libnbd-1.24.2/.gitignore new/libnbd-1.25.5/.gitignore
--- old/libnbd-1.24.2/.gitignore        2026-03-03 18:12:42.000000000 +0100
+++ new/libnbd-1.25.5/.gitignore        2026-04-30 12:51:52.000000000 +0200
@@ -214,6 +214,7 @@
 /tests/aio-parallel-load-tls
 /tests/aio-parallel-tls
 /tests/can-*-flag
+/tests/close-callback
 /tests/close-null
 /tests/closure-lifetimes
 /tests/compile-c
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libnbd-1.24.2/configure.ac 
new/libnbd-1.25.5/configure.ac
--- old/libnbd-1.24.2/configure.ac      2026-03-03 18:12:42.000000000 +0100
+++ new/libnbd-1.25.5/configure.ac      2026-04-30 12:51:52.000000000 +0200
@@ -15,7 +15,7 @@
 # License along with this library; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
-AC_INIT([libnbd],[1.24.2])
+AC_INIT([libnbd],[1.25.5])
 
 AC_CONFIG_MACRO_DIR([m4])
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libnbd-1.24.2/docs/nbd_create.pod 
new/libnbd-1.25.5/docs/nbd_create.pod
--- old/libnbd-1.24.2/docs/nbd_create.pod       2026-03-03 18:12:42.000000000 
+0100
+++ new/libnbd-1.25.5/docs/nbd_create.pod       2026-04-30 12:51:52.000000000 
+0200
@@ -30,7 +30,8 @@
 =head1 DESCRIPTION
 
 B<struct nbd_handle> is an opaque structure which describes an NBD
-client handle and the connection to an NBD server.
+client handle and the connection to an NBD server.  For an overview of
+the NBD client and NBD handles, see L<libnbd(3)>.
 
 =head2 Creating a libnbd handle
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libnbd-1.24.2/dump/nbddump.pod 
new/libnbd-1.25.5/dump/nbddump.pod
--- old/libnbd-1.24.2/dump/nbddump.pod  2026-03-03 18:12:42.000000000 +0100
+++ new/libnbd-1.25.5/dump/nbddump.pod  2026-04-30 12:51:52.000000000 +0200
@@ -134,6 +134,7 @@
 L<file(1)>,
 L<qemu-img(1)>,
 L<nbdkit(1)>,
+L<nbdkit_debug_hexdump(3)>,
 L<qemu-nbd(8)>.
 
 =head1 AUTHORS
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libnbd-1.24.2/fuse/nbdfuse.c 
new/libnbd-1.25.5/fuse/nbdfuse.c
--- old/libnbd-1.24.2/fuse/nbdfuse.c    2026-03-03 18:12:42.000000000 +0100
+++ new/libnbd-1.25.5/fuse/nbdfuse.c    2026-04-30 12:51:52.000000000 +0200
@@ -42,6 +42,7 @@
 
 handles nbd = empty_vector;
 unsigned connections = 4;
+bool keepalive = false;
 bool readonly;
 bool file_mode = false;
 struct timespec start_t;
@@ -74,7 +75,8 @@
 "Mount NBD server as a virtual file:\n"
 "\n"
 #ifdef HAVE_LIBXML2
-"    nbdfuse [-C N|--connections N] [-d] [-o FUSE-OPTION] [-P PIDFILE]\n"
+"    nbdfuse [-C N|--connections N] [-d] [--keepalive]\n"
+"            [-o FUSE-OPTION] [-P PIDFILE]\n"
 "            [-r] [-s] [-v]\n"
 "            MOUNTPOINT[/FILENAME] URI\n"
 "\n"
@@ -165,6 +167,7 @@
   enum {
     HELP_OPTION = CHAR_MAX + 1,
     FUSE_HELP_OPTION,
+    KEEPALIVE_OPTION,
     LONG_OPTIONS,
     SHORT_OPTIONS,
   };
@@ -176,8 +179,10 @@
   const struct option long_options[] = {
     { "fuse-help",          no_argument,       NULL, FUSE_HELP_OPTION },
     { "help",               no_argument,       NULL, HELP_OPTION },
-    { "long-options",       no_argument,       NULL, LONG_OPTIONS },
     { "connections",        required_argument, NULL, 'C' },
+    { "keepalive",          no_argument,       NULL, KEEPALIVE_OPTION },
+    { "keep-alive",         no_argument,       NULL, KEEPALIVE_OPTION },
+    { "long-options",       no_argument,       NULL, LONG_OPTIONS },
     { "pidfile",            required_argument, NULL, 'P' },
     { "pid-file",           required_argument, NULL, 'P' },
     { "readonly",           no_argument,       NULL, 'r' },
@@ -211,6 +216,10 @@
       fuse_help (argv[0]);
       exit (EXIT_SUCCESS);
 
+    case KEEPALIVE_OPTION:
+      keepalive = true;
+      break;
+
     case LONG_OPTIONS:
       for (i = 0; long_options[i].name != NULL; ++i) {
         if (strcmp (long_options[i].name, "long-options") != 0 &&
@@ -538,6 +547,7 @@
     exit (EXIT_FAILURE);
   }
   nbd_set_debug (h, verbose);
+  nbd_set_keepalive (h, keepalive);
 
   /* Allow ?tls-psk-file and ?tls-certificates */
   nbd_set_uri_allow_local_file (h, true);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libnbd-1.24.2/fuse/nbdfuse.pod 
new/libnbd-1.25.5/fuse/nbdfuse.pod
--- old/libnbd-1.24.2/fuse/nbdfuse.pod  2026-03-03 18:12:42.000000000 +0100
+++ new/libnbd-1.25.5/fuse/nbdfuse.pod  2026-04-30 12:51:52.000000000 +0200
@@ -4,7 +4,8 @@
 
 =head1 SYNOPSIS
 
- nbdfuse [-C N|--connections N] [-d] [-o FUSE-OPTION] [-P PIDFILE]
+ nbdfuse [-C N|--connections N] [-d] [--keepalive]
+         [-o FUSE-OPTION] [-P PIDFILE]
          [-r] [-s] [-v|--verbose]
          MOUNTPOINT[/FILENAME] URI
 
@@ -186,6 +187,11 @@
 
 Display FUSE options and exit.  See I<-o> below.
 
+=item B<--keepalive>
+
+Set the keepalive (C<"SO_KEEPALIVE">) flag on the connection.  See
+L<nbd_set_keepalive(3)> and L<socket(7)>.
+
 =item B<-o> FUSE-OPTION
 
 Pass extra options to FUSE.  To get a list of all the extra options
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libnbd-1.24.2/generator/API.ml 
new/libnbd-1.25.5/generator/API.ml
--- old/libnbd-1.24.2/generator/API.ml  2026-03-03 18:12:42.000000000 +0100
+++ new/libnbd-1.25.5/generator/API.ml  2026-04-30 12:51:52.000000000 +0200
@@ -154,6 +154,10 @@
              CBUInt64 "offset"; CBUInt "status";
              CBMutable (Int "error") ]
 }
+let close_closure = {
+  cbname = "close";
+  cbargs = []
+}
 let completion_closure = {
   cbname = "completion";
   cbargs = [ CBMutable (Int "error") ]
@@ -186,7 +190,7 @@
   cbname = "context";
   cbargs = [ CBString "name" ]
 }
-let all_closures = [ chunk_closure; completion_closure;
+let all_closures = [ chunk_closure; close_closure; completion_closure;
                      debug_closure; extent_closure; extent64_closure;
                      list_closure;
                      context_closure ]
@@ -444,8 +448,9 @@
 
 Libnbd does not use or interpret the value at all (except to return
 it when you call L<nbd_get_private_data(3)>).  In particular, if the
-value is a pointer then it is not freed when the handle is closed.|};
-    see_also = [Link "get_private_data"];
+value is a pointer then it is not freed when the handle is closed.
+If you want that behaviour, see L<nbd_add_close_callback(3)>.|};
+    see_also = [Link "get_private_data"; Link "add_close_callback"];
   };
 
   "get_private_data", {
@@ -4400,6 +4405,116 @@
     see_also = [Link "connect_uri"; Link "aio_connect_uri";
                 Link "supports_uri"; Link "get_uri"];
   };
+
+  "set_keepalive", {
+    default_call with
+    args = [Bool "enable"]; ret = RErr;
+    permitted_states = [ Created ];
+    shortdesc = "set keepalive on the socket";
+    longdesc = {|Set the keepalive (C<SO_KEEPALIVE>) flag on the
+socket after creating it.
+
+On operating systems and protocols that support it, this means the
+OS sends periodic messages to the NBD server to detect if the socket
+is still connected.  If this feature is not supported then setting
+this does nothing.
+
+If using TCP on Linux, keepalive can be tuned by calling
+L<nbd_set_tcp_option(3)>.
+
+This only works if called on the handle before calls such as
+L<nbd_connect_tcp(3)> where libnbd itself creates the socket.
+It does not apply to L<nbd_connect_socket(3)> where a socket
+is created somewhere else and passed to libnbd.|};
+    see_also = [Link "get_keepalive";
+                Link "set_tcp_option";
+                ExternalLink ("socket", 7);
+                ExternalLink ("setsockopt", 2)];
+  };
+
+  "get_keepalive", {
+    default_call with
+    args = []; ret = RBool;
+    shortdesc = "return keepalive status";
+    longdesc = {|Return the keepalive status.
+
+If the handle is not connected, this returns the keepalive flag
+from the handle as set by L<nbd_set_keepalive(3)>.
+
+If the handle is connected, this attempts to read the keepalive
+(C<SO_KEEPALIVE>) flag from the socket itself, telling you if
+keepalive was successfully enabled or not.|};
+    see_also = [Link "set_keepalive";
+                ExternalLink ("socket", 7);
+                ExternalLink ("getsockopt", 2)];
+  };
+
+  "set_tcp_option", {
+    default_call with
+    args = [Int "option"; Int "v"]; ret = RErr;
+    permitted_states = [ Created ];
+    shortdesc = "set TCP options on the socket";
+    longdesc = {|If making a TCP connection then you can call
+this function to set a subset of TCP options on the socket.
+
+Currently the C<option> parameter may be one of:
+
+=over 4
+
+=item *
+
+TCP_KEEPCNT
+
+=item *
+
+TCP_KEEPIDLE
+
+=item *
+
+TCP_KEEPINTVL
+
+=back
+
+These options may not work on non-Linux systems.
+
+For the C<TCP_KEEP*> options, keepalive must also be enabled
+by calling L<nbd_set_keepalive(3)>).|};
+    see_also = [Link "set_keepalive";
+                ExternalLink ("tcp", 7);
+                ExternalLink ("setsockopt", 2)];
+  };
+
+  "add_close_callback", {
+    default_call with
+    args = [ Closure close_closure ];
+    ret = RErr;
+    shortdesc = "add a callback which is called on close";
+    longdesc = {|libnbd handles maintain a list of callbacks
+which are called when the handle is closed.  This adds a
+callback to this list.
+
+Callbacks cannot be removed from the list once added.
+Callbacks might not necessarily be called in all circumstances
+(eg. if the program crashes).
+
+The callback function is declared like this (in the C bindings):
+
+ int callback (void *user_data);
+
+where the parameter is the C<user_data> passed to this function,
+and an C<int> is returned.  The return value is ignored because
+L<nbd_close(3)> returns C<void> and cannot be cancelled.
+
+The callback should not call C<nbd_*> APIs on the same handle since it can
+be called while holding the handle lock and will cause a deadlock.
+
+If there are multiple close callbacks the order in which they
+are called is not defined.  But all the callbacks are called
+first followed by all the free functions.|};
+    see_also = [Link "set_private_data"; Link "close";
+                ExternalLink ("atexit", 3)];
+};
+
 ]
 
 (* The first stable version that the symbol appeared in, for
@@ -4587,6 +4702,12 @@
   "get_tls_priority", (1, 24);
   "set_uri_allow_tls_priority", (1, 24);
 
+  (* Added in 1.25.x development cycle, will be stable and supported in 1.26 *)
+  "set_keepalive", (1, 26);
+  "get_keepalive", (1, 26);
+  "set_tcp_option", (1, 26);
+  "add_close_callback", (1, 26);
+
   (* These calls are proposed for a future version of libnbd, but
    * have not been added to any released version so far.
   "get_tls_certificates", (1, ??);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libnbd-1.24.2/generator/OCaml.ml 
new/libnbd-1.25.5/generator/OCaml.ml
--- old/libnbd-1.24.2/generator/OCaml.ml        2026-03-03 18:12:42.000000000 
+0100
+++ new/libnbd-1.25.5/generator/OCaml.ml        2026-04-30 12:51:52.000000000 
+0200
@@ -79,9 +79,13 @@
      sprintf "?%s:(%s)" cbname (ocaml_closuredecl_to_string cbargs)
   | OFlags (n, { flag_prefix }, _) -> sprintf "?%s:%s.t list" n flag_prefix
 
-and ocaml_closuredecl_to_string cbargs =
-  let cbargs = List.map ocaml_cbarg_to_string cbargs in
-  String.concat " -> " (cbargs @ ["int"])
+and ocaml_closuredecl_to_string = function
+  | [] ->
+     (* For no-argument closures we need to add an implicit unit arg. *)
+     "unit -> int"
+  | cbargs ->
+     let cbargs = List.map ocaml_cbarg_to_string cbargs in
+     String.concat " -> " (cbargs @ ["int"])
 
 and ocaml_cbarg_to_string = function
   | CBArrayAndLen (arg, _) ->
@@ -574,13 +578,15 @@
   pr "\n";
   pr "{\n";
   pr "  CAMLparam0 ();\n";
-  assert (List.length argnames <= 5);
-  pr "  CAMLlocal%d (%s);\n" (List.length argnames)
-    (String.concat ", " argnames);
+  let argnames_len = List.length argnames in
+  assert (argnames_len <= 5);
+  if argnames_len > 0 then
+    pr "  CAMLlocal%d (%s);\n" argnames_len (String.concat ", " argnames);
   pr "  CAMLlocal2 (exn, rv);\n";
   pr "  const struct user_data *data = user_data;\n";
   pr "  int r;\n";
-  pr "  value args[%d];\n" (List.length argnames);
+  if argnames_len > 0 then
+    pr "  value args[%d];\n" argnames_len;
   pr "\n";
 
   List.iter (
@@ -619,8 +625,12 @@
 
   List.iteri (fun i n -> pr "  args[%d] = %s;\n" i n) argnames;
 
-  pr "  rv = caml_callbackN_exn (data->fnv, %d, args);\n"
-    (List.length argnames);
+  if argnames_len > 0 then
+    pr "  rv = caml_callbackN_exn (data->fnv, %d, args);\n"
+      (List.length argnames)
+  else
+    (* If no callback arguments, call with unit parameter. *)
+    pr "  rv = caml_callback_exn (data->fnv, Val_unit);\n";
 
   List.iter (
     function
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libnbd-1.24.2/generator/states-connect.c 
new/libnbd-1.25.5/generator/states-connect.c
--- old/libnbd-1.24.2/generator/states-connect.c        2026-03-03 
18:12:42.000000000 +0100
+++ new/libnbd-1.25.5/generator/states-connect.c        2026-04-30 
12:51:52.000000000 +0200
@@ -83,6 +83,44 @@
 #endif
 }
 
+/* Set SO_KEEPALIVE on the socket.  This is best effort so don't fail. */
+static void
+set_keepalive (struct nbd_handle *h, int sock)
+{
+  if (h->keepalive) {
+    int opt = 1;
+    if (setsockopt (sock, SOL_SOCKET, SO_KEEPALIVE, &opt, sizeof opt) == -1) {
+      debug (h, "setsockopt: SOL_SOCKET: SO_KEEPALIVE: %s (ignored)",
+             strerror (errno));
+      return;
+    }
+  }
+}
+
+static void
+set_tcp_option (struct nbd_handle *h, int sock,
+                const char *optname, int option, int *v)
+{
+  if (*v >= 0 && setsockopt (sock, IPPROTO_TCP, option, v, sizeof *v) == -1) {
+    debug (h, "setsockopt: IPPROTO_TCP: %s: %s (ignored)",
+           optname, strerror (errno));
+  }
+}
+
+static void
+set_tcp_options (struct nbd_handle *h, int sock)
+{
+#ifdef TCP_KEEPCNT
+  set_tcp_option (h, sock, "TCP_KEEPCNT", TCP_KEEPCNT, &h->tcp_keepcnt);
+#endif
+#ifdef TCP_KEEPIDLE
+  set_tcp_option (h, sock, "TCP_KEEPIDLE", TCP_KEEPIDLE, &h->tcp_keepidle);
+#endif
+#ifdef TCP_KEEPINTVL
+  set_tcp_option (h, sock, "TCP_KEEPINTVL", TCP_KEEPINTVL, &h->tcp_keepintvl);
+#endif
+}
+
 STATE_MACHINE {
  CONNECT.START:
   sa_family_t family;
@@ -106,6 +144,8 @@
   disable_sigpipe (fd);
   if (family == AF_UNIX)
     set_buffers (fd);
+  set_keepalive (h, fd);
+  set_tcp_options (h, fd);
 
   r = connect (fd, (struct sockaddr *)&h->connaddr, h->connaddrlen);
   if (r == 0 || (r == -1 && errno == EINPROGRESS))
@@ -219,6 +259,8 @@
 
   disable_nagle (fd);
   disable_sigpipe (fd);
+  set_keepalive (h, fd);
+  set_tcp_options (h, fd);
 
   if (connect (fd, h->rp->ai_addr, h->rp->ai_addrlen) == -1) {
     if (errno != EINPROGRESS) {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libnbd-1.24.2/info/can.c new/libnbd-1.25.5/info/can.c
--- old/libnbd-1.24.2/info/can.c        2026-03-03 18:12:42.000000000 +0100
+++ new/libnbd-1.25.5/info/can.c        2026-04-30 12:51:52.000000000 +0200
@@ -38,10 +38,16 @@
 
   assert (can);
 
-  if (strcasecmp (can, "connect") == 0 ||
-      strcasecmp (can, "read") == 0)
+  if (strcasecmp (can, "connect") == 0)
     feature = 1;
 
+  else if (strcasecmp (can, "open") == 0 ||
+           strcasecmp (can, "read") == 0)
+    /* nbd_get_size won't work unless the server has sent export
+     * flags, which means we are able to open the export.
+     */
+    feature = nbd_get_size (nbd) >= 0;
+
   else if (strcasecmp (can, "tls") == 0)
     feature = nbd_get_tls_negotiated (nbd);
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libnbd-1.24.2/info/info-can-read.sh 
new/libnbd-1.25.5/info/info-can-read.sh
--- old/libnbd-1.24.2/info/info-can-read.sh     2026-03-03 18:12:42.000000000 
+0100
+++ new/libnbd-1.25.5/info/info-can-read.sh     2026-04-30 12:51:52.000000000 
+0200
@@ -21,9 +21,32 @@
 set -e
 set -x
 
-# --can read always returns true.
+requires $NBDKIT eval --version
 
-requires $NBDKIT null --version
+# --can open / --can read are only successful when the export can be
+# opened.
 
-$NBDKIT -v -U - null \
+$NBDKIT -v -U - eval \
+        get_size=' echo 64M ' \
+        --run '$VG nbdinfo --can open "nbd+unix:///?socket=$unixsocket"'
+
+$NBDKIT -v -U - eval \
+        get_size=' echo 64M ' \
         --run '$VG nbdinfo --can read "nbd+unix:///?socket=$unixsocket"'
+
+# But if the export cannot be opened, we expected them to fail.
+
+st=0
+$NBDKIT -v -U - eval \
+        open='echo EIO go away >&2; exit 1' \
+        get_size=' echo 64M ' \
+        --run '$VG nbdinfo --can open "nbd+unix:///?socket=$unixsocket"' \
+    || st=$?
+test $st = 2
+
+$NBDKIT -v -U - eval \
+        open='echo EIO go away >&2; exit 1' \
+        get_size=' echo 64M ' \
+        --run '$VG nbdinfo --can read "nbd+unix:///?socket=$unixsocket"' \
+    || st=$?
+test $st = 2
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libnbd-1.24.2/info/info-can.sh 
new/libnbd-1.25.5/info/info-can.sh
--- old/libnbd-1.24.2/info/info-can.sh  2026-03-03 18:12:42.000000000 +0100
+++ new/libnbd-1.25.5/info/info-can.sh  2026-04-30 12:51:52.000000000 +0200
@@ -30,7 +30,7 @@
 
 # --can connect is tested in info-can-connect.sh
 
-# --can read is tested in info-can-read.sh
+# --can open and --can read are tested in info-can-read.sh
 
 # --can zero is tested in info-can-zero.sh
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libnbd-1.24.2/info/nbdinfo.pod 
new/libnbd-1.25.5/info/nbdinfo.pod
--- old/libnbd-1.24.2/info/nbdinfo.pod  2026-03-03 18:12:42.000000000 +0100
+++ new/libnbd-1.25.5/info/nbdinfo.pod  2026-04-30 12:51:52.000000000 +0200
@@ -164,16 +164,18 @@
 
 For convenience this is the opposite of I<--is read-only>.
 
+=item nbdinfo --can open URI
+
 =item nbdinfo --can read URI
 
-All NBD servers must support read, so this always exits with success
-(unless there is a failure connecting to the URI).
+Test if we can connect to the NBD URI and open the named export.  As
+all NBD servers must support read, I<--can read> is a synonym.
 
 =item nbdinfo --can connect URI
 
 Test if we can connect to the NBD URI.  This tests the NBD handshake
 as far as NBD options processing, but does not guarantee that the
-export can be opened.
+export can be opened (see I<--can open>).
 
 =item nbdinfo --is tls URI
 
@@ -355,6 +357,8 @@
 
 =item B<--can multi-conn>
 
+=item B<--can open>
+
 =item B<--can read>
 
 =item B<--can trim>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libnbd-1.24.2/lib/handle.c 
new/libnbd-1.25.5/lib/handle.c
--- old/libnbd-1.24.2/lib/handle.c      2026-03-03 18:12:42.000000000 +0100
+++ new/libnbd-1.25.5/lib/handle.c      2026-04-30 12:51:52.000000000 +0200
@@ -29,6 +29,7 @@
 #include <netdb.h>
 #include <sys/types.h>
 #include <sys/wait.h>
+#include <netinet/tcp.h>
 
 #include "ascii-ctype.h"
 #include "internal.h"
@@ -68,6 +69,8 @@
     goto error1;
   }
 
+  h->close_cbs = (close_cb_vector) empty_vector;
+
   h->unique = 1;
   h->tls_verify_peer = true;
   h->tls_priority = strdup (TLS_PRIORITY);
@@ -75,6 +78,9 @@
     set_error (errno, "strdup");
     goto error1;
   }
+  h->tcp_keepcnt = -1;
+  h->tcp_keepidle = -1;
+  h->tcp_keepintvl = -1;
   h->request_eh = true;
   h->request_sr = true;
   h->request_meta = true;
@@ -128,6 +134,18 @@
   return NULL;
 }
 
+static void
+call_close_callback (nbd_close_callback cb)
+{
+  CALL_CALLBACK (cb);
+}
+
+static void
+free_close_callback (nbd_close_callback cb)
+{
+  FREE_CALLBACK (cb);
+}
+
 void
 nbd_close (struct nbd_handle *h)
 {
@@ -142,6 +160,14 @@
 
   debug (h, "closing handle");
 
+  /* Call any close callbacks, and free them.  We do this in two
+   * phases so that close callbacks can share structs without them
+   * being freed early.
+   */
+  close_cb_vector_iter (&h->close_cbs, call_close_callback);
+  close_cb_vector_iter (&h->close_cbs, free_close_callback);
+  close_cb_vector_reset (&h->close_cbs);
+
   /* Free user callbacks first. */
   nbd_unlocked_clear_debug_callback (h);
 
@@ -224,6 +250,18 @@
 }
 
 int
+nbd_unlocked_add_close_callback (struct nbd_handle *h,
+                                 nbd_close_callback *close_callback)
+{
+  if (close_cb_vector_append (&h->close_cbs, *close_callback) == -1) {
+    set_error (errno, "realloc");
+    return -1;
+  }
+  SET_CALLBACK_TO_NULL (*close_callback);
+  return 0;
+}
+
+int
 nbd_unlocked_set_socket_activation_name (struct nbd_handle *h,
                                          const char *name)
 {
@@ -331,6 +369,7 @@
   return
     sizeof *h
     + estimate_string (h->hname)
+    + h->close_cbs.cap * sizeof (nbd_close_callback)
     + estimate_string (h->export_name)
     + estimate_string (h->sact_name)
     + estimate_string (h->tls_certificates)
@@ -780,3 +819,60 @@
 {
   return h->chunks_received;
 }
+
+int
+nbd_unlocked_set_keepalive (struct nbd_handle *h, bool enable)
+{
+  h->keepalive = enable;
+  return 0;
+}
+
+int
+nbd_unlocked_get_keepalive (struct nbd_handle *h)
+{
+  if (!h->sock) {
+    /* Not connected so return the flag from the handle. */
+    return h->keepalive;
+  }
+  else {
+    /* Read the SO_KEEPALIVE status from the underlying socket. */
+    int opt;
+    socklen_t optlen = sizeof opt;
+    int fd;
+
+    fd = h->sock->ops->get_fd (h->sock);
+    if (fd == -1) {
+      set_error (EBADF, "socket open but no associated file descriptor");
+      return -1;
+    }
+    if (getsockopt (fd, SOL_SOCKET, SO_KEEPALIVE, &opt, &optlen) == -1) {
+      set_error (errno, "getsockopt: SOL_SOCKET: SO_KEEPALIVE");
+      return -1;
+    }
+    if (optlen != sizeof opt) {
+      set_error (ENOTSUP, "getsockopt: SOL_SOCKET: SO_KEEPALIVE: "
+                 "unexpected parameter size");
+    }
+    return opt;
+  }
+}
+
+int
+nbd_unlocked_set_tcp_option (struct nbd_handle *h,
+                             int option, int value)
+{
+  switch (option) {
+#ifdef TCP_KEEPCNT
+  case TCP_KEEPCNT:   h->tcp_keepcnt = value;   return 0;
+#endif
+#ifdef TCP_KEEPIDLE
+  case TCP_KEEPIDLE:  h->tcp_keepidle = value;  return 0;
+#endif
+#ifdef TCP_KEEPINTVL
+  case TCP_KEEPINTVL: h->tcp_keepintvl = value; return 0;
+#endif
+  default:
+    set_error (ENOTSUP, "unknown or unsupported TCP option (%d)", option);
+    return -1;
+  }
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libnbd-1.24.2/lib/internal.h 
new/libnbd-1.25.5/lib/internal.h
--- old/libnbd-1.24.2/lib/internal.h    2026-03-03 18:12:42.000000000 +0100
+++ new/libnbd-1.25.5/lib/internal.h    2026-04-30 12:51:52.000000000 +0200
@@ -72,10 +72,12 @@
   char *name;                   /* Name of meta context. */
   uint32_t context_id;          /* Context ID negotiated with the server. */
 };
-DEFINE_VECTOR_TYPE (meta_vector, struct meta_context);
+DEFINE_VECTOR_TYPE(meta_vector, struct meta_context);
 
 DEFINE_VECTOR_TYPE(uint32_vector, uint32_t);
 
+DEFINE_VECTOR_TYPE(close_cb_vector, nbd_close_callback);
+
 struct export {
   char *name;
   char *description;
@@ -109,6 +111,9 @@
   /* Private data, for the application to use. */
   _Atomic uintptr_t private_data;
 
+  /* Close callback(s). */
+  close_cb_vector close_cbs;
+
   char *export_name;            /* Export name, never NULL. */
   char *sact_name;              /* Socket activation name, can be NULL. */
 
@@ -321,6 +326,10 @@
   struct addrinfo hints;
   struct addrinfo *result, *rp;
   int connect_errno;
+  bool keepalive;
+  int tcp_keepcnt;
+  int tcp_keepidle;
+  int tcp_keepintvl;
 
   /* When sending metadata contexts, this is used. */
   string_vector querylist;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libnbd-1.24.2/ocaml/tests/Makefile.am 
new/libnbd-1.25.5/ocaml/tests/Makefile.am
--- old/libnbd-1.24.2/ocaml/tests/Makefile.am   2026-03-03 18:12:42.000000000 
+0100
+++ new/libnbd-1.25.5/ocaml/tests/Makefile.am   2026-04-30 12:51:52.000000000 
+0200
@@ -27,6 +27,7 @@
        test_120_set_non_defaults.ml \
        test_130_private_data.ml \
        test_140_explicit_close.ml \
+       test_150_close_callback.ml \
        test_200_connect_command.ml \
        test_210_opt_abort.ml \
        test_220_opt_list.ml \
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libnbd-1.24.2/ocaml/tests/test_150_close_callback.ml 
new/libnbd-1.25.5/ocaml/tests/test_150_close_callback.ml
--- old/libnbd-1.24.2/ocaml/tests/test_150_close_callback.ml    1970-01-01 
01:00:00.000000000 +0100
+++ new/libnbd-1.25.5/ocaml/tests/test_150_close_callback.ml    2026-04-30 
12:51:52.000000000 +0200
@@ -0,0 +1,39 @@
+(* hey emacs, this is OCaml code: -*- tuareg -*- *)
+(* libnbd OCaml test case
+ * Copyright Red Hat
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *)
+
+let () =
+  let a = ref 0 and b = ref 0 in
+  let nbd1 = NBD.create () and nbd2 = NBD.create () in
+
+  NBD.add_close_callback nbd1 (fun () -> a := 1; 0);
+  NBD.add_close_callback nbd1 (fun () -> b := 2; 0);
+  NBD.add_close_callback nbd2 (fun () -> a := 2; 0);
+
+  assert (!a = 0);
+  assert (!b = 0);
+
+  NBD.close nbd1;
+  assert (!a = 1);
+  assert (!b = 2);
+
+  NBD.close nbd2;
+  assert (!a = 2);
+  assert (!b = 2)
+
+let () = Gc.compact ()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libnbd-1.24.2/python/Makefile.am 
new/libnbd-1.25.5/python/Makefile.am
--- old/libnbd-1.24.2/python/Makefile.am        2026-03-03 18:12:42.000000000 
+0100
+++ new/libnbd-1.25.5/python/Makefile.am        2026-04-30 12:51:52.000000000 
+0200
@@ -37,6 +37,8 @@
        libnbd-python.pod \
        nbdsh.py \
        pycodestyle.sh \
+       test-aio-connect-ip.sh \
+       test-aio-connect-unix-oldformat.sh \
        test-aio-connect-unix.sh \
        $(srcdir)/t/*.py \
        $(NULL)
@@ -98,7 +100,9 @@
 LOG_COMPILER = $(top_builddir)/run
 TESTS += \
        run-python-tests \
+       test-aio-connect-unix-oldformat.sh \
        test-aio-connect-unix.sh \
+       test-aio-connect-ip.sh \
        $(NULL)
 
 endif HAVE_NBDKIT
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libnbd-1.24.2/python/t/150-close-callback.py 
new/libnbd-1.25.5/python/t/150-close-callback.py
--- old/libnbd-1.24.2/python/t/150-close-callback.py    1970-01-01 
01:00:00.000000000 +0100
+++ new/libnbd-1.25.5/python/t/150-close-callback.py    2026-04-30 
12:51:52.000000000 +0200
@@ -0,0 +1,50 @@
+# libnbd Python bindings
+# Copyright Red Hat
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+import nbd
+
+a = 0
+b = 0
+
+
+def set_a(i):
+    global a
+    a = i
+
+
+def set_b(i):
+    global b
+    b = i
+
+
+h1 = nbd.NBD()
+h2 = nbd.NBD()
+
+h1.add_close_callback(lambda: set_a(1))
+h1.add_close_callback(lambda: set_b(2))
+h2.add_close_callback(lambda: set_a(2))
+
+assert a == 0
+assert b == 0
+
+h1.close()
+assert a == 1
+assert b == 2
+
+h2.close()
+assert a == 2
+assert b == 2
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libnbd-1.24.2/python/test-aio-connect-ip.sh 
new/libnbd-1.25.5/python/test-aio-connect-ip.sh
--- old/libnbd-1.24.2/python/test-aio-connect-ip.sh     1970-01-01 
01:00:00.000000000 +0100
+++ new/libnbd-1.25.5/python/test-aio-connect-ip.sh     2026-04-30 
12:51:52.000000000 +0200
@@ -0,0 +1,60 @@
+#!/usr/bin/env bash
+# nbd client library in userspace
+# Copyright Red Hat
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+. ../tests/functions.sh
+
+set -e
+set -x
+
+requires $PYTHON --version
+
+# Python tests all fail on macOS because of SIP misfeature.
+requires_not test "$(uname)" = "Darwin"
+
+# Requires libxml2 for h.get_uri
+requires_uri
+
+# Requires nbdkit >= 1.47 supporting --port=0 properly.
+requires $NBDKIT --version
+minor=$( $NBDKIT --dump-config | grep ^version_minor | $CUT -d= -f2 )
+requires test $minor -ge 47
+
+# Check the host supports IPv6.
+requires ip -V
+requires bash -c 'ip -o -6 addr show scope host | grep inet6'
+
+define script <<'EOF'
+import nbd
+import sys
+
+h = nbd.NBD()
+port = sys.argv[1]
+addr = ("AF_INET6", ("::1", port))
+h.aio_connect(addr)
+while h.aio_is_connecting():
+    h.poll(1)
+assert h.aio_is_ready()
+
+# libnbd should be able to form a URI from this address.
+uri = h.get_uri()
+print("uri:", uri)
+assert uri == ("nbd://[::1]:%s/" % port)
+EOF
+export script
+
+$NBDKIT --port=0 null --run '$PYTHON -c "$script" "$port"'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/libnbd-1.24.2/python/test-aio-connect-unix-oldformat.sh 
new/libnbd-1.25.5/python/test-aio-connect-unix-oldformat.sh
--- old/libnbd-1.24.2/python/test-aio-connect-unix-oldformat.sh 1970-01-01 
01:00:00.000000000 +0100
+++ new/libnbd-1.25.5/python/test-aio-connect-unix-oldformat.sh 2026-04-30 
12:51:52.000000000 +0200
@@ -0,0 +1,52 @@
+#!/usr/bin/env bash
+# nbd client library in userspace
+# Copyright Red Hat
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+. ../tests/functions.sh
+
+set -e
+set -x
+
+requires $NBDKIT --version
+requires $PYTHON --version
+
+# Python tests all fail on macOS because of SIP misfeature.
+requires_not test "$(uname)" = "Darwin"
+
+# Requires libxml2 for h.get_uri
+requires_uri
+
+define script <<'EOF'
+import nbd
+import sys
+
+h = nbd.NBD()
+unixsocket = sys.argv[1]
+addr = unixsocket
+h.aio_connect(addr)
+while h.aio_is_connecting():
+    h.poll(1)
+assert h.aio_is_ready()
+
+# libnbd should be able to form a URI from this address.
+uri = h.get_uri()
+print("uri:", uri)
+assert uri == ("nbd+unix:///?socket=%s" % unixsocket)
+EOF
+export script
+
+$NBDKIT -U - null --run '$PYTHON -c "$script" "$unixsocket"'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libnbd-1.24.2/python/test-aio-connect-unix.sh 
new/libnbd-1.25.5/python/test-aio-connect-unix.sh
--- old/libnbd-1.24.2/python/test-aio-connect-unix.sh   2026-03-03 
18:12:42.000000000 +0100
+++ new/libnbd-1.25.5/python/test-aio-connect-unix.sh   2026-04-30 
12:51:52.000000000 +0200
@@ -27,13 +27,26 @@
 # Python tests all fail on macOS because of SIP misfeature.
 requires_not test "$(uname)" = "Darwin"
 
-$NBDKIT -U - null --run '$PYTHON -c "
+# Requires libxml2 for h.get_uri
+requires_uri
+
+define script <<'EOF'
 import nbd
 import sys
+
 h = nbd.NBD()
-addr = sys.argv[1]
+unixsocket = sys.argv[1]
+addr = ("AF_UNIX", unixsocket)
 h.aio_connect(addr)
 while h.aio_is_connecting():
     h.poll(1)
 assert h.aio_is_ready()
-" "$unixsocket"'
+
+# libnbd should be able to form a URI from this address.
+uri = h.get_uri()
+print("uri:", uri)
+assert uri == ("nbd+unix:///?socket=%s" % unixsocket)
+EOF
+export script
+
+$NBDKIT -U - null --run '$PYTHON -c "$script" "$unixsocket"'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libnbd-1.24.2/python/utils.c 
new/libnbd-1.25.5/python/utils.c
--- old/libnbd-1.24.2/python/utils.c    2026-03-03 18:12:42.000000000 +0100
+++ new/libnbd-1.25.5/python/utils.c    2026-04-30 12:51:52.000000000 +0200
@@ -28,6 +28,8 @@
 #include <assert.h>
 #include <sys/socket.h>
 #include <sys/un.h>
+#include <arpa/inet.h>
+#include <netinet/in.h>
 
 #include <libnbd.h>
 
@@ -99,63 +101,250 @@
  * general rules described here:
  * https://docs.python.org/3/library/socket.html
  *
+ * Because that mapping is not unique (it only makes sense in the
+ * context of knowing the address family), the caller has to pass in
+ * the address family as the first parameter.  Therefore the formats
+ * that we parse are:
+ *
+ * ("AF_UNIX", path)
+ * ("AF_UNIX", (path))
+ * ("AF_INET", (addr, port))
+ * ("AF_INET6", (addr, port, [additional elements ignored]))
+ *
+ * For backwards compatibility with libnbd <= 1.24, we also allow a
+ * single string here which is parsed as a Unix domain socket.
+ *
  * There is a function in cpython called getsockaddrarg which roughly
  * does the same thing, but in cpython they know the socket family
- * already (which we do not).  In any case that function cannot be
- * called directly.
+ * already.  In any case that function cannot be called directly.
  */
+static int parse_path_to_sockaddr_un (PyObject *path,
+                                      struct sockaddr_un *sun, socklen_t *len);
+static int parse_addr_to_sockaddr_in (PyObject *tuple,
+                                      struct sockaddr_in *sin, socklen_t *len);
+static int parse_addr_to_sockaddr_in6 (PyObject *tuple,
+                                       struct sockaddr_in6 *sin6,
+                                       socklen_t *len);
+
 int
 nbd_internal_py_get_sockaddr (PyObject *addr,
                               struct sockaddr_storage *ss, socklen_t *len)
 {
   memset (ss, 0, sizeof *ss);
 
-  if (PyUnicode_Check (addr)) { /* AF_UNIX */
+  /* For backwards compatibility with libnbd <= 1.24, parse a bare
+   * string as AF_UNIX.
+   */
+  if (PyUnicode_Check (addr)) {
     struct sockaddr_un *sun = (struct sockaddr_un *)ss;
-    const char *unixsocket;
-    size_t namelen;
+    return parse_path_to_sockaddr_un (addr, sun, len);
+  }
 
-    sun->sun_family = AF_UNIX;
-    unixsocket = PyUnicode_AsUTF8 (addr);
-    if (!unixsocket)
-      goto err;
-    namelen = strlen (unixsocket);
-    if (namelen > sizeof sun->sun_path) {
+  else if (PyTuple_Check (addr)) {
+    const char *af;
+    Py_ssize_t n;
+    PyObject *sockaddr;
+
+    /* Must be a 2-element tuple. */
+    n = PyTuple_Size (addr);
+    if (n != 2) {
       PyErr_SetString (PyExc_RuntimeError,
-                       "get_sockaddr: Unix domain socket name too long");
+                       "get_sockaddr: must be a 2-element tuple, "
+                       "with the first element being the address family, "
+                       "for example: (\"AF_UNIX\", path) or "
+                       "(\"AF_INET\", (ipaddr, port))");
       return -1;
     }
-    memcpy (sun->sun_path, unixsocket, namelen);
-    *len = sizeof *sun;
-    return 0;
-  }
 
-#if 0
-  else if (PyTuple_Check (addr)) {
-    Py_ssize_t n = PyTuple_Size (addr);
+    /* First element is the address family. */
+    af = PyUnicode_AsUTF8 (PyTuple_GetItem (addr, 0));
+    if (!af) return -1;
+
+    /* Second element is the sockaddr. */
+    sockaddr = PyTuple_GetItem (addr, 1);
+
+    /* AF_UNIX */
+    if (strcasecmp (af, "UNIX") == 0 || strcasecmp (af, "AF_UNIX") == 0) {
+      if (PyTuple_Check (sockaddr) && PyTuple_Size (sockaddr) == 1)
+        sockaddr = PyTuple_GetItem (sockaddr, 0);
+
+      if (PyUnicode_Check (sockaddr)) {
+        struct sockaddr_un *sun = (struct sockaddr_un *)ss;
+        return parse_path_to_sockaddr_un (sockaddr, sun, len);
+      }
 
-    switch (n) {
-    case 2:                     /* AF_INET */
-      /* XXX TODO */
-      break;
-
-    case 4:                     /* AF_INET6 */
-      /* XXX TODO */
-      break;
+      PyErr_SetString (PyExc_RuntimeError,
+                       "get_sockaddr: wrong format for Unix domain socket");
+      return -1;
+    }
 
-    default:
-      goto err;
+    /* AF_INET */
+    else if (strcasecmp (af, "INET") == 0 ||
+             strcasecmp (af, "AF_INET") == 0) {
+      if (PyTuple_Check (sockaddr)) {
+        struct sockaddr_in *sin = (struct sockaddr_in *)ss;
+        return parse_addr_to_sockaddr_in (sockaddr, sin, len);
+      }
+
+      PyErr_SetString (PyExc_RuntimeError,
+                       "get_sockaddr: wrong format for AF_INET socket");
+      return -1;
+    }
+
+    /* AF_INET6 */
+    else if (strcasecmp (af, "INET6") == 0 ||
+             strcasecmp (af, "AF_INET6") == 0) {
+      if (PyTuple_Check (sockaddr)) {
+        struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)ss;
+        return parse_addr_to_sockaddr_in6 (sockaddr, sin6, len);
+      }
+
+      PyErr_SetString (PyExc_RuntimeError,
+                       "get_sockaddr: wrong format for AF_INET6 socket");
+      return -1;
+    }
+
+    else {
+      PyErr_SetString (PyExc_TypeError,
+                       "get_sockaddr: unknown address family");
+      return -1;
     }
   }
-#endif
 
   else {
-  err:
-    PyErr_SetString (PyExc_TypeError, "get_sockaddr: unknown address type");
+    PyErr_SetString (PyExc_TypeError, "get_sockaddr: "
+                     "unknown socket address type");
     return -1;
   }
 }
 
+/* Parse path (which must be PyUnicode) to sockaddr_un. */
+static int
+parse_path_to_sockaddr_un (PyObject *path,
+                           struct sockaddr_un *sun, socklen_t *len)
+{
+  const char *unixsocket;
+  size_t namelen;
+
+  sun->sun_family = AF_UNIX;
+  *len = sizeof *sun;
+
+  unixsocket = PyUnicode_AsUTF8 (path);
+  if (!unixsocket) {
+    PyErr_SetString (PyExc_TypeError, "get_sockaddr: cannot parse socket 
path");
+    return -1;
+  }
+  namelen = strlen (unixsocket);
+  if (namelen > sizeof sun->sun_path) {
+    PyErr_SetString (PyExc_RuntimeError,
+                     "get_sockaddr: Unix domain socket name too long");
+    return -1;
+  }
+  memcpy (sun->sun_path, unixsocket, namelen);
+  return 0;
+}
+
+/* Parse (addr, port) tuple (which must be PyTuple) to sockaddr_in. */
+static int
+parse_addr_to_sockaddr_in (PyObject *tuple,
+                           struct sockaddr_in *sin, socklen_t *len)
+{
+  const char *addr_str, *port_str;
+  int port;
+  const Py_ssize_t n = PyTuple_Size (tuple);
+
+  if (n < 2) {
+    PyErr_SetString (PyExc_TypeError, "get_sockaddr: need (addr, port) "
+                     "for socket address");
+    return -1;
+  }
+
+  addr_str = PyUnicode_AsUTF8 (PyTuple_GetItem (tuple, 0));
+  port_str = PyUnicode_AsUTF8 (PyTuple_GetItem (tuple, 1));
+
+  if (!addr_str || !port_str) {
+    PyErr_SetString (PyExc_TypeError, "get_sockaddr: cannot parse "
+                     "address or port number in socket address");
+    return -1;
+  }
+
+  sin->sin_family = AF_INET;
+  *len = sizeof *sin;
+
+  switch (inet_pton (AF_INET, addr_str, &sin->sin_addr)) {
+  case 1: break; /* success */
+  case 0: /* invalid address string */
+    PyErr_SetString (PyExc_RuntimeError,
+                     "get_sockaddr: inet_pton: cannot parse IPv4 address");
+    return -1;
+  case -1: /* invalid address family, probably impossible */
+    PyErr_SetString (PyExc_RuntimeError,
+                     "get_sockaddr: inet_pton: invalid address family");
+    return -1;
+  }
+
+  if (sscanf (port_str, "%d", &port) != 1) {
+    PyErr_SetString (PyExc_RuntimeError,
+                     "get_sockaddr: cannot parse port number");
+    return -1;
+  }
+  sin->sin_port = htons (port);
+
+  return 0;
+}
+
+/* Parse (addr, port) tuple (which must be PyTuple) to sockaddr_in6. */
+static int
+parse_addr_to_sockaddr_in6 (PyObject *tuple,
+                            struct sockaddr_in6 *sin6, socklen_t *len)
+{
+  const char *addr_str, *port_str;
+  int port;
+  const Py_ssize_t n = PyTuple_Size (tuple);
+
+  if (n < 2) {
+    PyErr_SetString (PyExc_TypeError, "get_sockaddr: need (addr, port) "
+                     "for socket address");
+    return -1;
+  }
+
+  /* element 2 = flowinfo, element 3 = scope_id, both are
+   * currently ignored.
+   */
+  addr_str = PyUnicode_AsUTF8 (PyTuple_GetItem (tuple, 0));
+  port_str = PyUnicode_AsUTF8 (PyTuple_GetItem (tuple, 1));
+
+  if (!addr_str || !port_str) {
+    PyErr_SetString (PyExc_TypeError, "get_sockaddr: cannot parse "
+                     "address or port number in socket address");
+    return -1;
+  }
+
+  sin6->sin6_family = AF_INET6;
+  *len = sizeof *sin6;
+
+  switch (inet_pton (AF_INET6, addr_str, &sin6->sin6_addr)) {
+  case 1: break; /* success */
+  case 0: /* invalid address string */
+    PyErr_SetString (PyExc_RuntimeError,
+                     "get_sockaddr: inet_pton: cannot parse IPv6 address");
+    return -1;
+  case -1: /* invalid address family, probably impossible */
+    PyErr_SetString (PyExc_RuntimeError,
+                     "get_sockaddr: inet_pton: invalid address family");
+    return -1;
+  }
+
+  if (sscanf (port_str, "%d", &port) != 1) {
+    PyErr_SetString (PyExc_RuntimeError,
+                     "get_sockaddr: cannot parse port number");
+    return -1;
+  }
+  sin6->sin6_port = htons (port);
+
+  return 0;
+}
+
 /* Obtain the type object for nbd.Buffer */
 PyObject *
 nbd_internal_py_get_nbd_buffer_type (void)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libnbd-1.24.2/tests/Makefile.am 
new/libnbd-1.25.5/tests/Makefile.am
--- old/libnbd-1.24.2/tests/Makefile.am 2026-03-03 18:12:42.000000000 +0100
+++ new/libnbd-1.25.5/tests/Makefile.am 2026-04-30 12:51:52.000000000 +0200
@@ -73,6 +73,7 @@
        compile-header-only \
        compile-c \
        compile-iso-c99 \
+       close-callback \
        close-null \
        debug \
        debug-environment \
@@ -90,6 +91,7 @@
        compile-header-only \
        compile-c \
        compile-iso-c99 \
+       close-callback \
        close-null \
        debug \
        debug-environment \
@@ -114,6 +116,9 @@
        $(NULL)
 compile_iso_c99_LDADD = $(top_builddir)/lib/libnbd.la
 
+close_callback_SOURCES = close-callback.c
+close_callback_LDADD = $(top_builddir)/lib/libnbd.la
+
 close_null_SOURCES = close-null.c
 close_null_LDADD = $(top_builddir)/lib/libnbd.la
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libnbd-1.24.2/tests/close-callback.c 
new/libnbd-1.25.5/tests/close-callback.c
--- old/libnbd-1.24.2/tests/close-callback.c    1970-01-01 01:00:00.000000000 
+0100
+++ new/libnbd-1.25.5/tests/close-callback.c    2026-04-30 12:51:52.000000000 
+0200
@@ -0,0 +1,105 @@
+/* NBD client library in userspace
+ * Copyright Red Hat
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/* Check that nbd_close callbacks work. */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <assert.h>
+
+#include <libnbd.h>
+
+static int
+set_to_1 (void *vp)
+{
+  int *i = vp;
+
+  printf ("set_to_1: %d -> 1\n", *i);
+  fflush (stdout);
+  *i = 1;
+  return 0;
+}
+
+static int
+set_to_2 (void *vp)
+{
+  int *i = vp;
+
+  printf ("set_to_2: %d -> 2\n", *i);
+  fflush (stdout);
+  *i = 2;
+  return 0;
+}
+
+int
+main (int argc, char *argv[])
+{
+  struct nbd_handle *nbd1, *nbd2;
+  int a = 0, b = 0;
+  void *data;
+
+  nbd1 = nbd_create ();
+  if (nbd1 == NULL) {
+    fprintf (stderr, "%s\n", nbd_get_error ());
+    exit (EXIT_FAILURE);
+  }
+
+  nbd2 = nbd_create ();
+  if (nbd2 == NULL) {
+    fprintf (stderr, "%s\n", nbd_get_error ());
+    exit (EXIT_FAILURE);
+  }
+
+  /* Add some close callbacks to both handles. */
+  nbd_add_close_callback (nbd1,
+                          (nbd_close_callback){ .callback = set_to_1,
+                                                .user_data = &a });
+  nbd_add_close_callback (nbd1,
+                          (nbd_close_callback){ .callback = set_to_2,
+                                                .user_data = &b });
+  nbd_add_close_callback (nbd2,
+                          (nbd_close_callback){ .callback = set_to_2,
+                                                .user_data = &a });
+
+  /* This tests that the callback is freed only once.  It relies on
+   * the C library detecting double frees.
+   */
+  data = malloc (64);
+  assert (data);
+  nbd_add_close_callback (nbd1,
+                          (nbd_close_callback){ .user_data = data,
+                                                .free = free });
+
+  /* Initial state. */
+  assert (a == 0);
+  assert (b == 0);
+
+  /* Close the first handle. */
+  nbd_close (nbd1);
+  assert (a == 1);
+  assert (b == 2);
+
+  /* Close the second handle. */
+  nbd_close (nbd2);
+  assert (a == 2);
+  assert (b == 2);
+
+  exit (EXIT_SUCCESS);
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libnbd-1.24.2/tests/connect-tcp.c 
new/libnbd-1.25.5/tests/connect-tcp.c
--- old/libnbd-1.24.2/tests/connect-tcp.c       2026-03-03 18:12:42.000000000 
+0100
+++ new/libnbd-1.25.5/tests/connect-tcp.c       2026-04-30 12:51:52.000000000 
+0200
@@ -25,6 +25,7 @@
 #include <string.h>
 #include <fcntl.h>
 #include <unistd.h>
+#include <netinet/tcp.h>
 
 #include <libnbd.h>
 
@@ -41,6 +42,7 @@
   pid_t pid;
   size_t i;
   char *actual_uri, *expected_uri;
+  int r;
 
   unlink (PIDFILE);
 
@@ -73,11 +75,41 @@
     exit (EXIT_FAILURE);
   }
 
+  /* Setting keepalive won't do anything for this short connection.
+   * However it should be possible to set these parameters even on
+   * non-Linux.
+   */
+  r = nbd_get_keepalive (nbd);
+  if (r != 0) {
+    fprintf (stderr, "expected nbd_get_keepalive == 0, but got %d\n", r);
+    exit (EXIT_FAILURE);
+  }
+  nbd_set_keepalive (nbd, 1); /* in the current impl this cannot fail */
+  r = nbd_get_keepalive (nbd);
+  if (r != 1) {
+    fprintf (stderr, "expected nbd_get_keepalive == 1, but got %d\n", r);
+    exit (EXIT_FAILURE);
+  }
+#ifdef TCP_KEEPCNT
+  nbd_set_tcp_option (nbd, TCP_KEEPCNT, 10);
+#endif
+#ifdef TCP_KEEPIDLE
+  nbd_set_tcp_option (nbd, TCP_KEEPIDLE, 60);
+#endif
+#ifdef TCP_KEEPINTVL
+  nbd_set_tcp_option (nbd, TCP_KEEPINTVL, 10);
+#endif
+
   if (nbd_connect_tcp (nbd, "localhost", port_str) == -1) {
     fprintf (stderr, "%s\n", nbd_get_error ());
     exit (EXIT_FAILURE);
   }
 
+  /* See if keepalive was established, but don't fail if it was not. */
+  r = nbd_get_keepalive (nbd);
+  printf ("nbd_get_keepalive after connection returned %d\n", r);
+  fflush (stdout);
+
   if (nbd_supports_uri (nbd) == 1) {
     /* libnbd should be able to construct a URI for this connection. */
     if (asprintf (&expected_uri, "nbd://localhost:%s/", port_str) == -1) {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libnbd-1.24.2/ublk/nbdublk.c 
new/libnbd-1.25.5/ublk/nbdublk.c
--- old/libnbd-1.24.2/ublk/nbdublk.c    2026-03-03 18:12:42.000000000 +0100
+++ new/libnbd-1.25.5/ublk/nbdublk.c    2026-04-30 12:51:52.000000000 +0200
@@ -46,6 +46,7 @@
 
 handles nbd = empty_vector;
 unsigned connections = 4;
+bool keepalive = false;
 bool readonly = false;
 bool rotational;
 bool can_fua;
@@ -78,7 +79,7 @@
 "Mount NBD server as a virtual device:\n"
 "\n"
 #ifdef HAVE_LIBXML2
-"    nbdublk [-C N|--connections N] [-r] [-v|--verbose]\n"
+"    nbdublk [-C N|--connections N] [--keepalive] [-r] [-v|--verbose]\n"
 "            " DEVICE_PREFIX "<N> URI\n"
 "\n"
 "Other modes:\n"
@@ -143,6 +144,7 @@
   enum mode mode = MODE_URI;
   enum {
     HELP_OPTION = CHAR_MAX + 1,
+    KEEPALIVE_OPTION,
     LONG_OPTIONS,
     SHORT_OPTIONS,
   };
@@ -153,8 +155,10 @@
   const char *short_options = "+C:rvV";
   const struct option long_options[] = {
     { "help",               no_argument,       NULL, HELP_OPTION },
-    { "long-options",       no_argument,       NULL, LONG_OPTIONS },
     { "connections",        required_argument, NULL, 'C' },
+    { "keepalive",          no_argument,       NULL, KEEPALIVE_OPTION },
+    { "keep-alive",         no_argument,       NULL, KEEPALIVE_OPTION },
+    { "long-options",       no_argument,       NULL, LONG_OPTIONS },
     { "readonly",           no_argument,       NULL, 'r' },
     { "read-only",          no_argument,       NULL, 'r' },
     { "short-options",      no_argument,       NULL, SHORT_OPTIONS },
@@ -182,6 +186,10 @@
     case HELP_OPTION:
       usage (stdout, EXIT_SUCCESS);
 
+    case KEEPALIVE_OPTION:
+      keepalive = true;
+      break;
+
     case LONG_OPTIONS:
       for (i = 0; long_options[i].name != NULL; ++i) {
         if (strcmp (long_options[i].name, "long-options") != 0 &&
@@ -489,6 +497,7 @@
     exit (EXIT_FAILURE);
   }
   nbd_set_debug (h, verbose);
+  nbd_set_keepalive (h, keepalive);
 
   /* Connect to the NBD server synchronously. */
   switch (mode) {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libnbd-1.24.2/ublk/nbdublk.pod 
new/libnbd-1.25.5/ublk/nbdublk.pod
--- old/libnbd-1.24.2/ublk/nbdublk.pod  2026-03-03 18:12:42.000000000 +0100
+++ new/libnbd-1.25.5/ublk/nbdublk.pod  2026-04-30 12:51:52.000000000 +0200
@@ -4,7 +4,7 @@
 
 =head1 SYNOPSIS
 
- nbdublk [-C N|--connections N] [-r] [-v|--verbose]
+ nbdublk [-C N|--connections N] [--keepalive] [-r] [-v|--verbose]
          { /dev/ublkb<N> | <N> | - } URI
 
 =for paragraph
@@ -121,6 +121,11 @@
 Disable multi-conn.  Only use a single connection to the NBD server.
 See L</THREAD MODEL> below.
 
+=item B<--keepalive>
+
+Set the keepalive (C<"SO_KEEPALIVE">) flag on the connection.  See
+L<nbd_set_keepalive(3)> and L<socket(7)>.
+
 =item B<-r>
 
 =item B<--readonly>

Reply via email to