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
