From 9d73ceef627143806688ffbb508a2b5a9a2493eb Mon Sep 17 00:00:00 2001 From: DavidBar-On Date: Thu, 29 Aug 2024 20:09:40 +0300 Subject: [PATCH] Fix #812 and #835 - add control connection keepalive (after rebase with master 3.17.1+) --- configure.ac | 12 ++++ src/iperf.h | 4 ++ src/iperf_api.c | 122 +++++++++++++++++++++++++++++++++++++++++ src/iperf_api.h | 14 +++++ src/iperf_client_api.c | 10 +++- src/iperf_error.c | 28 ++++++++-- src/iperf_locale.c | 4 ++ src/iperf_server_api.c | 6 ++ 8 files changed, 193 insertions(+), 7 deletions(-) diff --git a/configure.ac b/configure.ac index a23c3bcf4..e43ff6dec 100644 --- a/configure.ac +++ b/configure.ac @@ -204,6 +204,18 @@ if test "x$iperf3_cv_header_tcp_user_timeout" = "xyes"; then AC_DEFINE([HAVE_TCP_USER_TIMEOUT], [1], [Have TCP_USER_TIMEOUT sockopt.]) fi +# Check for TCP_KEEPIDLE sockopt (not clear where supported) +AC_CACHE_CHECK([TCP_KEEPIDLE socket option], +[iperf3_cv_header_tcp_keepalive], +AC_COMPILE_IFELSE( + [AC_LANG_PROGRAM([[#include ]], + [[int foo = TCP_KEEPIDLE;]])], + iperf3_cv_header_tcp_keepalive=yes, + iperf3_cv_header_tcp_keepalive=no)) +if test "x$iperf3_cv_header_tcp_keepalive" = "xyes"; then + AC_DEFINE([HAVE_TCP_KEEPALIVE], [1], [Have TCP_KEEPIDLE sockopt.]) +fi + # Check for IPv6 flowlabel support (believed to be Linux only) # We check for IPV6_FLOWLABEL_MGR in even though we # don't use that file directly (we have our own stripped-down diff --git a/src/iperf.h b/src/iperf.h index f297587d1..e30aa5221 100644 --- a/src/iperf.h +++ b/src/iperf.h @@ -184,6 +184,10 @@ struct iperf_settings int idle_timeout; /* server idle time timeout */ unsigned int snd_timeout; /* Timeout for sending tcp messages in active mode, in us */ struct iperf_time rcv_timeout; /* Timeout for receiving messages in active mode, in us */ + int cntl_ka; /* Use Control TCP connection Keepalive */ + int cntl_ka_keepidle; /* Control TCP connection Keepalive idle time (TCP_KEEPIDLE) */ + int cntl_ka_interval; /* Control TCP connection Keepalive interval between retries (TCP_KEEPINTV) */ + int cntl_ka_count; /* Control TCP connection Keepalive number of retries (TCP_KEEPCNT) */ }; struct iperf_test; diff --git a/src/iperf_api.c b/src/iperf_api.c index dafbadf65..5505ff227 100644 --- a/src/iperf_api.c +++ b/src/iperf_api.c @@ -1149,6 +1149,9 @@ iperf_parse_arguments(struct iperf_test *test, int argc, char **argv) {"idle-timeout", required_argument, NULL, OPT_IDLE_TIMEOUT}, {"rcv-timeout", required_argument, NULL, OPT_RCV_TIMEOUT}, {"snd-timeout", required_argument, NULL, OPT_SND_TIMEOUT}, +#if defined(HAVE_TCP_KEEPALIVE) + {"cntl-ka", optional_argument, NULL, OPT_CNTL_KA}, +#endif /* HAVE_TCP_KEEPALIVE */ {"debug", optional_argument, NULL, 'd'}, {"help", no_argument, NULL, 'h'}, {NULL, 0, NULL, 0} @@ -1162,6 +1165,9 @@ iperf_parse_arguments(struct iperf_test *test, int argc, char **argv) char* comma; #endif /* HAVE_CPU_AFFINITY */ char* slash; +#if defined(HAVE_TCP_KEEPALIVE) + char* slash2; +#endif /* HAVE_TCP_KEEPALIVE */ char *p, *p1; struct xbind_entry *xbe; double farg; @@ -1530,6 +1536,39 @@ iperf_parse_arguments(struct iperf_test *test, int argc, char **argv) snd_timeout_flag = 1; break; #endif /* HAVE_TCP_USER_TIMEOUT */ +#if defined (HAVE_TCP_KEEPALIVE) + case OPT_CNTL_KA: + test->settings->cntl_ka = 1; + if (optarg) { + slash = strchr(optarg, '/'); + if (slash) { + *slash = '\0'; + ++slash; + slash2 = strchr(slash, '/'); + if (slash2) { + *slash2 = '\0'; + ++slash2; + if (strlen(slash2) > 0) { + test->settings->cntl_ka_count = atoi(slash2); + } + } + if (strlen(slash) > 0) { + test->settings->cntl_ka_interval = atoi(slash); + } + } + if (strlen(optarg) > 0) { + test->settings->cntl_ka_keepidle = atoi(optarg); + } + } + // Seems that at least in Windows WSL2, TCP keepalive retries full inteval must be + // smaller than the idle interval. Otherwise, the keepalive message is sent only once. + if (test->settings->cntl_ka_keepidle && + test->settings->cntl_ka_keepidle <= (test->settings->cntl_ka_count * test->settings->cntl_ka_interval)) { + i_errno = IECNTLKA; + return -1; + } + break; +#endif /* HAVE_TCP_KEEPALIVE */ case 'A': #if defined(HAVE_CPU_AFFINITY) test->affinity = strtol(optarg, &endptr, 0); @@ -2975,6 +3014,10 @@ iperf_defaults(struct iperf_test *testp) testp->settings->rcv_timeout.secs = DEFAULT_NO_MSG_RCVD_TIMEOUT / SEC_TO_mS; testp->settings->rcv_timeout.usecs = (DEFAULT_NO_MSG_RCVD_TIMEOUT % SEC_TO_mS) * mS_TO_US; testp->zerocopy = 0; + testp->settings->cntl_ka = 0; + testp->settings->cntl_ka_keepidle = 0; + testp->settings->cntl_ka_interval = 0; + testp->settings->cntl_ka_count = 0; memset(testp->cookie, 0, COOKIE_SIZE); @@ -5172,3 +5215,82 @@ iflush(struct iperf_test *test) return rc2; } + +#if defined (HAVE_TCP_KEEPALIVE) +// Set Control Connection TCP Keepalive (especially useful for long UDP test sessions) +int +iperf_set_control_keepalive(struct iperf_test *test) +{ + int opt, kaidle, kainterval, kacount; + socklen_t len; + + if (test->settings->cntl_ka) { + // Set keepalive using system defaults + opt = 1; + if (setsockopt(test->ctrl_sck, SOL_SOCKET, SO_KEEPALIVE, (char *) &opt, sizeof(opt))) { + i_errno = IESETCNTLKA; + return -1; + } + + // Get default values when not specified + if ((kaidle = test->settings->cntl_ka_keepidle) == 0) { + len = sizeof(kaidle); + if (getsockopt(test->ctrl_sck, IPPROTO_TCP, TCP_KEEPIDLE, (char *) &kaidle, &len)) { + i_errno = IESETCNTLKAINTERVAL; + return -1; + } + } + if ((kainterval = test->settings->cntl_ka_interval) == 0) { + len = sizeof(kainterval); + if (getsockopt(test->ctrl_sck, IPPROTO_TCP, TCP_KEEPINTVL, (char *) &kainterval, &len)) { + i_errno = IESETCNTLKAINTERVAL; + return -1; + } + } + if ((kacount = test->settings->cntl_ka_count) == 0) { + len = sizeof(kacount); + if (getsockopt(test->ctrl_sck, IPPROTO_TCP, TCP_KEEPCNT, (char *) &kacount, &len)) { + i_errno = IESETCNTLKACOUNT; + return -1; + } + } + + // Seems that at least in Windows WSL2, TCP keepalive retries full inteval must be + // smaller than the idle interval. Otherwise, the keepalive message is sent only once. + if (test->settings->cntl_ka_keepidle) { + if (test->settings->cntl_ka_keepidle <= (kainterval * kacount)) { + iperf_err(test, "Keepalive Idle time (%d) should be greater than Retries-interval (%d) times Retries-count (%d)", kaidle, kainterval, kacount); + i_errno = IECNTLKA; + return -1; + } + } + + // Set keep alive values when specified + if ((opt = test->settings->cntl_ka_keepidle)) { + if (setsockopt(test->ctrl_sck, IPPROTO_TCP, TCP_KEEPIDLE, (char *) &opt, sizeof(opt))) { + i_errno = IESETCNTLKAKEEPIDLE; + return -1; + } + } + if ((opt = test->settings->cntl_ka_interval)) { + if (setsockopt(test->ctrl_sck, IPPROTO_TCP, TCP_KEEPINTVL, (char *) &opt, sizeof(opt))) { + i_errno = IESETCNTLKAINTERVAL; + return -1; + } + } + if ((opt = test->settings->cntl_ka_count)) { + if (setsockopt(test->ctrl_sck, IPPROTO_TCP, TCP_KEEPCNT, (char *) &opt, sizeof(opt))) { + i_errno = IESETCNTLKACOUNT; + return -1; + } + } + + if (test->verbose) { + printf("Control connection TCP Keepalive TCP_KEEPIDLE/TCP_KEEPINTVL/TCP_KEEPCNT are set to %d/%d/%d\n", + kaidle, kainterval, kacount); + } + } + + return 0; +} +#endif //HAVE_TCP_KEEPALIVE diff --git a/src/iperf_api.h b/src/iperf_api.h index 131314243..9aa345449 100644 --- a/src/iperf_api.h +++ b/src/iperf_api.h @@ -101,6 +101,7 @@ typedef atomic_uint_fast64_t atomic_iperf_size_t; #define OPT_JSON_STREAM 28 #define OPT_SND_TIMEOUT 29 #define OPT_USE_PKCS1_PADDING 30 +#define OPT_CNTL_KA 31 /* states */ #define TEST_START 1 @@ -309,6 +310,14 @@ void iperf_free_stream(struct iperf_stream * sp); */ int iperf_common_sockopts(struct iperf_test *, int s); +#if defined (HAVE_TCP_KEEPALIVE) +/** + * iperf_set_control_keepalive -- set control connection TCP keepalive + * + */ +int iperf_set_control_keepalive(struct iperf_test *test); +#endif //HAVE_TCP_KEEPALIVE + int has_tcpinfo(void); int has_tcpinfo_retransmits(void); void save_tcpinfo(struct iperf_stream *sp, struct iperf_interval_results *irp); @@ -420,6 +429,7 @@ enum { IESNDTIMEOUT = 33, // Illegal message send timeout IEUDPFILETRANSFER = 34, // Cannot transfer file using UDP IESERVERAUTHUSERS = 35, // Cannot access authorized users file + IECNTLKA = 36, // Control connection Keepalive period should be larger than the full retry period (interval * count) /* Test errors */ IENEWTEST = 100, // Unable to create a new test (check perror) IEINITTEST = 101, // Test initialization failed (check perror) @@ -475,6 +485,10 @@ enum { IEPTHREADJOIN=152, // Unable to join thread (check perror) IEPTHREADATTRINIT=153, // Unable to initialize thread attribute (check perror) IEPTHREADATTRDESTROY=154, // Unable to destroy thread attribute (check perror) + IESETCNTLKA = 155, // Unable to set socket keepalive (SO_KEEPALIVE) option + IESETCNTLKAKEEPIDLE = 156, // Unable to set socket keepalive TCP period (TCP_KEEPIDLE) option + IESETCNTLKAINTERVAL = 157, // Unable to set/get socket keepalive TCP retry interval (TCP_KEEPINTVL) option + IESETCNTLKACOUNT = 158, // Unable to set/get socket keepalive TCP number of retries (TCP_KEEPCNT) option /* Stream errors */ IECREATESTREAM = 200, // Unable to create a new stream (check herror/perror) IEINITSTREAM = 201, // Unable to initialize stream (check herror/perror) diff --git a/src/iperf_client_api.c b/src/iperf_client_api.c index 7c22caded..b606c465b 100644 --- a/src/iperf_client_api.c +++ b/src/iperf_client_api.c @@ -421,11 +421,17 @@ iperf_connect(struct iperf_test *test) return -1; } +#if defined (HAVE_TCP_KEEPALIVE) + // Set Control Connection TCP Keepalive (especially useful for long UDP test sessions) + if (iperf_set_control_keepalive(test) < 0) + return -1; +#endif //HAVE_TCP_KEEPALIVE + #if defined(HAVE_TCP_USER_TIMEOUT) if ((opt = test->settings->snd_timeout)) { if (setsockopt(test->ctrl_sck, IPPROTO_TCP, TCP_USER_TIMEOUT, &opt, sizeof(opt)) < 0) { - i_errno = IESETUSERTIMEOUT; - return -1; + i_errno = IESETUSERTIMEOUT; + return -1; } } #endif /* HAVE_TCP_USER_TIMEOUT */ diff --git a/src/iperf_error.c b/src/iperf_error.c index ce925a81f..c4cca621d 100644 --- a/src/iperf_error.c +++ b/src/iperf_error.c @@ -182,7 +182,7 @@ iperf_strerror(int int_errno) case IEINTERVAL: snprintf(errstr, len, "invalid report interval (min = %g, max = %g seconds)", MIN_INTERVAL, MAX_INTERVAL); break; - case IEBIND: /* UNUSED */ + case IEBIND: /* UNUSED */ snprintf(errstr, len, "--bind must be specified to use --cport"); break; case IEUDPBLOCKSIZE: @@ -464,7 +464,7 @@ iperf_strerror(int int_errno) case IETOTALRATE: snprintf(errstr, len, "total required bandwidth is larger than server limit"); break; - case IESKEWTHRESHOLD: + case IESKEWTHRESHOLD: snprintf(errstr, len, "skew threshold must be a positive number"); break; case IEIDLETIMEOUT: @@ -473,16 +473,16 @@ iperf_strerror(int int_errno) case IEBINDDEV: snprintf(errstr, len, "Unable to bind-to-device (check perror, maybe permissions?)"); break; - case IEBINDDEVNOSUPPORT: + case IEBINDDEVNOSUPPORT: snprintf(errstr, len, "`%%` is not supported as system does not support bind to device"); break; - case IEHOSTDEV: + case IEHOSTDEV: snprintf(errstr, len, "host device name (ip%%) is supported (and required) only for IPv6 link-local address"); break; case IENOMSG: snprintf(errstr, len, "idle timeout for receiving data"); break; - case IESETDONTFRAGMENT: + case IESETDONTFRAGMENT: snprintf(errstr, len, "unable to set IP Do-Not-Fragment flag"); break; case IESETUSERTIMEOUT: @@ -507,6 +507,24 @@ iperf_strerror(int int_errno) break; case IEPTHREADATTRDESTROY: snprintf(errstr, len, "unable to destroy thread attributes"); + case IECNTLKA: + snprintf(errstr, len, "control connection Keepalive period should be larger than the full retry period (interval * count)"); + perr = 1; + break; + case IESETCNTLKA: + snprintf(errstr, len, "unable to set socket keepalive (SO_KEEPALIVE) option"); + perr = 1; + break; + case IESETCNTLKAKEEPIDLE: + snprintf(errstr, len, "unable to set socket keepalive TCP period (TCP_KEEPIDLE) option"); + perr = 1; + break; + case IESETCNTLKAINTERVAL: + snprintf(errstr, len, "unable to set/get socket keepalive TCP retry interval (TCP_KEEPINTVL) option"); + perr = 1; + break; + case IESETCNTLKACOUNT: + snprintf(errstr, len, "unable to set/get socket keepalive TCP number of retries (TCP_KEEPCNT) option"); perr = 1; break; default: diff --git a/src/iperf_locale.c b/src/iperf_locale.c index cfdba5826..98a61c283 100644 --- a/src/iperf_locale.c +++ b/src/iperf_locale.c @@ -128,6 +128,10 @@ const char usage_longstr[] = "Usage: iperf3 [-s|-c host] [options]\n" " --snd-timeout # timeout for unacknowledged TCP data\n" " (in ms, default is system settings)\n" #endif /* HAVE_TCP_USER_TIMEOUT */ +#if defined(HAVE_TCP_KEEPALIVE) + " --cntl-ka[=#/#/#] use control connection TCP keepalive - KEEPIDLE/KEEPINTV/KEEPCNT\n" + " each value is optional with system settings default\n" +#endif //HAVE_TCP_KEEPALIVE " -d, --debug[=#] emit debugging output\n" " (optional optional \"=\" and debug level: 1-4. Default is 4 - all messages)\n" " -v, --version show version information and quit\n" diff --git a/src/iperf_server_api.c b/src/iperf_server_api.c index b87734fec..5bf66c4c4 100644 --- a/src/iperf_server_api.c +++ b/src/iperf_server_api.c @@ -166,6 +166,12 @@ iperf_accept(struct iperf_test *test) } #endif /* HAVE_TCP_USER_TIMEOUT */ +#if defined (HAVE_TCP_KEEPALIVE) + // Set Control Connection TCP Keepalive (especially useful for long UDP test sessions) + if (iperf_set_control_keepalive(test) < 0) + return -1; +#endif //HAVE_TCP_KEEPALIVE + if (Nread(test->ctrl_sck, test->cookie, COOKIE_SIZE, Ptcp) != COOKIE_SIZE) { /* * Note this error covers both the case of a system error