The TCP_USER_TIMEOUT socket option (Linux-only) specifies the maximum
amount of time in milliseconds that transmitted data may remain
unacknowledged, or buffered data may remain untransmitted, before TCP
forcibly closes the connection and returns ETIMEDOUT to the
application.  Without it, a peer that becomes silently unreachable can
take well over an hour to be noticed (the kernel default), which is
particularly painful for use cases like QEMU live migration.

Expose user-timeout as a new optional InetSocketAddress member, gated
on a new HAVE_TCP_USER_TIMEOUT meson check, and apply it in
inet_set_sockopts() alongside the existing keep-alive options.  Plumb
the same option through the legacy QemuOpts-based inet_parse() path so
that URI-style users (-incoming tcp:, migrate tcp:, HMP
nbd_server_start, ...) can set it too.

Update the documentation for -netdev stream (addr.user-timeout=N) and
-incoming tcp: (user-timeout=N, flat key as for the other inet_opts
entries), and extend test_inet_parse_all_options_good accordingly.

Signed-off-by: Mitsuru Kariya <[email protected]>
---
 meson.build                    |  2 ++
 qapi/sockets.json              |  7 +++++++
 qemu-options.hx                | 13 +++++++++----
 tests/unit/test-util-sockets.c | 13 +++++++++++++
 util/qemu-sockets.c            | 24 ++++++++++++++++++++++++
 5 files changed, 55 insertions(+), 4 deletions(-)

diff --git a/meson.build b/meson.build
index eb07491819..c5afe8754d 100644
--- a/meson.build
+++ b/meson.build
@@ -2726,6 +2726,8 @@ config_host_data.set('HAVE_TCP_KEEPINTVL',
                      #endif
                      int main(void) { return 0; }''',
                      name: 'Win32 TCP_KEEPINTVL'))
+config_host_data.set('HAVE_TCP_USER_TIMEOUT',
+                     cc.has_header_symbol('netinet/tcp.h', 'TCP_USER_TIMEOUT'))
 
 # has_member
 config_host_data.set('HAVE_SIGEV_NOTIFY_THREAD_ID',
diff --git a/qapi/sockets.json b/qapi/sockets.json
index 473be2ac58..543689a2c7 100644
--- a/qapi/sockets.json
+++ b/qapi/sockets.json
@@ -79,6 +79,12 @@
 #     defined (this includes Linux, Windows, macOS, FreeBSD, but not
 #     OpenBSD).  When set to 0, system setting is used.  (Since 10.1)
 #
+# @user-timeout: time in milliseconds that transmitted data may remain
+#     unacknowledged before the connection is closed.  Only supported
+#     for TCP sockets on systems where TCP_USER_TIMEOUT socket option
+#     is defined (Linux only).  When set to 0, system setting is used.
+#     (Since 11.1)
+#
 # @mptcp: enable multi-path TCP.  (Since 6.1)
 #
 # Since: 1.3
@@ -94,6 +100,7 @@
     '*keep-alive-count': { 'type': 'uint32', 'if': 'HAVE_TCP_KEEPCNT' },
     '*keep-alive-idle': { 'type': 'uint32', 'if': 'HAVE_TCP_KEEPIDLE' },
     '*keep-alive-interval': { 'type': 'uint32', 'if': 'HAVE_TCP_KEEPINTVL' },
+    '*user-timeout': { 'type': 'uint32', 'if': 'HAVE_TCP_USER_TIMEOUT' },
     '*mptcp': { 'type': 'bool', 'if': 'HAVE_IPPROTO_MPTCP' } } }
 
 ##
diff --git a/qemu-options.hx b/qemu-options.hx
index d2b816e16f..63587cccd3 100644
--- a/qemu-options.hx
+++ b/qemu-options.hx
@@ -3034,7 +3034,7 @@ DEF("netdev", HAS_ARG, QEMU_OPTION_netdev,
     "-netdev socket,id=str[,fd=h][,udp=host:port][,localaddr=host:port]\n"
     "                configure a network backend to connect to another 
network\n"
     "                using an UDP tunnel\n"
-    "-netdev 
stream,id=str[,server=on|off],addr.type=inet,addr.host=host,addr.port=port[,addr.to=maxport][,addr.numeric=on|off][,addr.keep-alive=on|off][,addr.keep-alive-count=count][,addr.keep-alive-idle=idle][,addr.keep-alive-interval=interval][,addr.mptcp=on|off][,addr.ipv4=on|off][,addr.ipv6=on|off][,reconnect-ms=milliseconds]\n"
+    "-netdev 
stream,id=str[,server=on|off],addr.type=inet,addr.host=host,addr.port=port[,addr.to=maxport][,addr.numeric=on|off][,addr.keep-alive=on|off][,addr.keep-alive-count=count][,addr.keep-alive-idle=idle][,addr.keep-alive-interval=interval][,addr.user-timeout=timeout][,addr.mptcp=on|off][,addr.ipv4=on|off][,addr.ipv6=on|off][,reconnect-ms=milliseconds]\n"
     "-netdev 
stream,id=str[,server=on|off],addr.type=unix,addr.path=path[,addr.abstract=on|off][,addr.tight=on|off][,reconnect-ms=milliseconds]\n"
     "-netdev 
stream,id=str[,server=on|off],addr.type=fd,addr.str=file-descriptor[,reconnect-ms=milliseconds]\n"
     "                configure a network backend to connect to another 
network\n"
@@ -3640,7 +3640,7 @@ SRST
                          -device e1000,netdev=n1,mac=52:54:00:12:34:56 \\
                          -netdev 
socket,id=n1,mcast=239.192.168.1:1102,localaddr=1.2.3.4
 
-``-netdev 
stream,id=str[,server=on|off],addr.type=inet,addr.host=host,addr.port=port[,addr.to=maxport][,addr.numeric=on|off][,addr.keep-alive=on|off][,addr.keep-alive-count=count][,addr.keep-alive-idle=idle][,addr.keep-alive-interval=interval][,addr.mptcp=on|off][,addr.ipv4=on|off][,addr.ipv6=on|off][,reconnect-ms=milliseconds]``
+``-netdev 
stream,id=str[,server=on|off],addr.type=inet,addr.host=host,addr.port=port[,addr.to=maxport][,addr.numeric=on|off][,addr.keep-alive=on|off][,addr.keep-alive-count=count][,addr.keep-alive-idle=idle][,addr.keep-alive-interval=interval][,addr.user-timeout=timeout][,addr.mptcp=on|off][,addr.ipv4=on|off][,addr.ipv6=on|off][,reconnect-ms=milliseconds]``
     Configure a network backend to connect to another QEMU virtual machine or 
a proxy using a TCP/IP socket.
 
     ``server=on|off``
@@ -3670,6 +3670,11 @@ SRST
         time in seconds between individual keep-alive packets.
         Set to 0 to use the system default.  (default: 0)
 
+    ``addr.user-timeout=timeout``
+        time in milliseconds that transmitted data may remain unacknowledged
+        before the connection is forcibly closed.
+        Set to 0 to use the system default.  (default: 0)
+
     ``addr.mptcp=on|off``
         enable multipath TCP
 
@@ -5365,7 +5370,7 @@ SRST
 ERST
 
 DEF("incoming", HAS_ARG, QEMU_OPTION_incoming, \
-    "-incoming 
tcp:[host]:port[,to=maxport][,numeric=on|off][,keep-alive=on|off][,keep-alive-count=count][,keep-alive-idle=idle][,keep-alive-interval=interval][,mptcp=on|off][,ipv4=on|off][,ipv6=on|off]\n"
 \
+    "-incoming 
tcp:[host]:port[,to=maxport][,numeric=on|off][,keep-alive=on|off][,keep-alive-count=count][,keep-alive-idle=idle][,keep-alive-interval=interval][,user-timeout=timeout][,mptcp=on|off][,ipv4=on|off][,ipv6=on|off]\n"
 \
     "-incoming rdma:host:port[,ipv4=on|off][,ipv6=on|off]\n" \
     "-incoming unix:socketpath\n" \
     "                prepare for incoming migration, listen on\n" \
@@ -5387,7 +5392,7 @@ migration channel types.  The channel type is specified 
in <channel>,
 or is 'main' for all other forms of -incoming.  If multiple -incoming
 options are specified for a channel type, the last one takes precedence.
 
-``-incoming 
tcp:[host]:port[,to=maxport][,numeric=on|off][,keep-alive=on|off][,keep-alive-count=count][,keep-alive-idle=idle][,keep-alive-interval=interval][,mptcp=on|off][,ipv4=on|off][,ipv6=on|off]``
+``-incoming 
tcp:[host]:port[,to=maxport][,numeric=on|off][,keep-alive=on|off][,keep-alive-count=count][,keep-alive-idle=idle][,keep-alive-interval=interval][,user-timeout=timeout][,mptcp=on|off][,ipv4=on|off][,ipv6=on|off]``
   \ 
 ``-incoming rdma:host:port[,ipv4=on|off][,ipv6=on|off]``
     Prepare for incoming migration, listen on a given tcp port.
diff --git a/tests/unit/test-util-sockets.c b/tests/unit/test-util-sockets.c
index b9f2453e29..a56efc1b83 100644
--- a/tests/unit/test-util-sockets.c
+++ b/tests/unit/test-util-sockets.c
@@ -382,6 +382,12 @@ static void inet_parse_test_helper(const char *str,
         g_assert_cmpint(addr.keep_alive_interval, ==,
                         exp_addr->keep_alive_interval);
 #endif
+#ifdef HAVE_TCP_USER_TIMEOUT
+        g_assert_cmpint(addr.has_user_timeout, ==,
+                        exp_addr->has_user_timeout);
+        g_assert_cmpint(addr.user_timeout, ==,
+                        exp_addr->user_timeout);
+#endif
 #ifdef HAVE_IPPROTO_MPTCP
         g_assert_cmpint(addr.has_mptcp, ==, exp_addr->has_mptcp);
         g_assert_cmpint(addr.mptcp, ==, exp_addr->mptcp);
@@ -495,6 +501,10 @@ static void test_inet_parse_all_options_good(void)
         .has_keep_alive_interval = true,
         .keep_alive_interval = 30,
 #endif
+#ifdef HAVE_TCP_USER_TIMEOUT
+        .has_user_timeout = true,
+        .user_timeout = 10000,
+#endif
 #ifdef HAVE_IPPROTO_MPTCP
         .has_mptcp = true,
         .mptcp = false,
@@ -511,6 +521,9 @@ static void test_inet_parse_all_options_good(void)
 #ifdef HAVE_TCP_KEEPINTVL
         ",keep-alive-interval=30"
 #endif
+#ifdef HAVE_TCP_USER_TIMEOUT
+        ",user-timeout=10000"
+#endif
 #ifdef HAVE_IPPROTO_MPTCP
         ",mptcp=off"
 #endif
diff --git a/util/qemu-sockets.c b/util/qemu-sockets.c
index 4773755fd5..f16f9b007c 100644
--- a/util/qemu-sockets.c
+++ b/util/qemu-sockets.c
@@ -263,6 +263,18 @@ static int inet_set_sockopts(int sock, InetSocketAddress 
*saddr, Error **errp)
         }
 #endif
     }
+#ifdef HAVE_TCP_USER_TIMEOUT
+    if (saddr->has_user_timeout && saddr->user_timeout) {
+        int user_timeout = saddr->user_timeout;
+        int ret = setsockopt(sock, IPPROTO_TCP, TCP_USER_TIMEOUT, 
&user_timeout,
+                             sizeof(user_timeout));
+        if (ret < 0) {
+            error_setg_errno(errp, errno,
+                             "Unable to set TCP user timeout option on 
socket");
+            return -1;
+        }
+    }
+#endif
     return 0;
 }
 
@@ -692,6 +704,12 @@ static QemuOptsList inet_opts = {
             .type = QEMU_OPT_NUMBER,
         },
 #endif
+#ifdef HAVE_TCP_USER_TIMEOUT
+        {
+            .name = "user-timeout",
+            .type = QEMU_OPT_NUMBER,
+        },
+#endif
 #ifdef HAVE_IPPROTO_MPTCP
         {
             .name = "mptcp",
@@ -775,6 +793,12 @@ int inet_parse(InetSocketAddress *addr, const char *str, 
Error **errp)
         addr->keep_alive_interval = qemu_opt_get_number(opts, 
"keep-alive-interval", 0);
     }
 #endif
+#ifdef HAVE_TCP_USER_TIMEOUT
+    if (qemu_opt_find(opts, "user-timeout")) {
+        addr->has_user_timeout = true;
+        addr->user_timeout = qemu_opt_get_number(opts, "user-timeout", 0);
+    }
+#endif
 #ifdef HAVE_IPPROTO_MPTCP
     if (qemu_opt_find(opts, "mptcp")) {
         addr->has_mptcp = true;
-- 
2.43.0


Reply via email to