From b66eabbb549835b3c90b483b3a0f9a600212de23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ignacy=20Gaw=C4=99dzki?= Date: Fri, 16 Dec 2022 12:54:04 +0100 Subject: [PATCH] Make the UDP socket use the right source address. When accepting a new UDP packet, replace the session socket with a new one bound to the destination address of the incoming packet. This way the right source IP address will be used on packets sent towards the client. * src/net.h, * src/net.c: Extract code to open new socket, set options and bind into new function netannounce_sockaddr, make netannounce only resolve the local address and port into a sockaddr and call the former. * src/iperf_udp.c: In iperf_udp_accept, use recvmsg instead of recvfrom, in order to retrieve the destination address of the packet in ancillary data. Use that address to create the session socket using netannounce_sockaddr. --- configure.ac | 28 ++++++++++++ src/iperf_udp.c | 96 ++++++++++++++++++++++++++++++++--------- src/net.c | 112 ++++++++++++++++++++++++++++++------------------ src/net.h | 1 + 4 files changed, 176 insertions(+), 61 deletions(-) diff --git a/configure.ac b/configure.ac index 9bc125801..290045afe 100644 --- a/configure.ac +++ b/configure.ac @@ -332,6 +332,34 @@ if test "x$iperf3_cv_header_dontfragment" = "xyes"; then AC_DEFINE([HAVE_DONT_FRAGMENT], [1], [Have IP_MTU_DISCOVER/IP_DONTFRAG/IP_DONTFRAGMENT sockopt.]) fi +# Check for IP_PKTINFO (Linux only) +AC_CACHE_CHECK([IP_PKTINFO socket option], +[iperf3_cv_header_ip_pktinfo], +AC_COMPILE_IFELSE( + [AC_LANG_PROGRAM([[#include ]], + [[int foo = IP_PKTINFO;]])], + iperf3_cv_header_ip_pktinfo=yes, + iperf3_cv_header_ip_pktinfo=no)) +if test "x$iperf3_cv_header_ip_pktinfo" = "xyes"; then + AC_DEFINE([HAVE_IP_PKTINFO], [1], [Have IP_PKTINFO sockopt.]) +fi + +# Check for IP_RECVDSTADDR (BSD?) +AC_CACHE_CHECK([IP_RECVDSTADDR socket option], +[iperf3_cv_header_ip_recvdstaddr], +AC_COMPILE_IFELSE( + [AC_LANG_PROGRAM([[#include ]], + [[int foo = IP_RECVDSTADDR;]])], + iperf3_cv_header_ip_recvdstaddr=yes, + iperf3_cv_header_ip_recvdstaddr=no)) +if test "x$iperf3_cv_header_ip_recvdstaddr" = "xyes"; then + AC_DEFINE([HAVE_IP_RECVDSTADDR], [1], [Have IP_RECVDSTADDR sockopt.]) +fi + +if test "x$iperf3_cv_header_ip_pktinfo$iperf3_cv_header_ip_recvdstaddr" = "xnono"; then + AC_MSG_ERROR([neither IP_PKTINFO nor IP_RECVDSTADDR is available]) +fi + # # Check for tcpi_snd_wnd in struct tcp_info # diff --git a/src/iperf_udp.c b/src/iperf_udp.c index c8835e6d7..f8845eb5c 100644 --- a/src/iperf_udp.c +++ b/src/iperf_udp.c @@ -380,25 +380,91 @@ iperf_udp_buffercheck(struct iperf_test *test, int s) int iperf_udp_accept(struct iperf_test *test) { - struct sockaddr_storage sa_peer; + struct sockaddr_storage sa_peer, sa_local; unsigned int buf; - socklen_t len; + socklen_t len, len_local; int sz, s; int rc; - - /* - * Get the current outstanding socket. This socket will be used to handle - * data transfers and a new "listening" socket will be created. - */ - s = test->prot_listener; + char cbuffer[ +#ifdef HAVE_IP_PKTINFO + CMSG_SPACE(sizeof (struct in_pktinfo) + > sizeof (struct in6_pktinfo)? + sizeof (struct in_pktinfo): + sizeof (struct in6_pktinfo)) +#elif defined(HAVE_IP_RECVDSTADDR) + CMSG_SPACE(sizeof (struct in_addr) + > sizeof (struct in6_pktinfo)? + sizeof (struct in_addr): + sizeof (struct in6_pktinfo)) +#endif + ]; + struct cmsghdr *cm; + struct msghdr msg; + struct iovec iov; /* * Grab the UDP packet sent by the client. From that we can extract the * client's address, and then use that information to bind the remote side * of the socket to the client. */ - len = sizeof(sa_peer); - if ((sz = recvfrom(test->prot_listener, &buf, sizeof(buf), 0, (struct sockaddr *) &sa_peer, &len)) < 0) { + len = len_local = sizeof(sa_peer); + + memset(cbuffer, 0, sizeof cbuffer); + + msg.msg_name = &sa_peer; + msg.msg_namelen = len; + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = cbuffer; + msg.msg_controllen = sizeof cbuffer; + msg.msg_flags = 0; + + iov.iov_base = &buf; + iov.iov_len = sizeof buf; + + /* Get the address of the local socket to determine the domain and port. */ + if (getsockname(test->prot_listener, (struct sockaddr *) &sa_local, + &len_local) < 0) { + i_errno = IESTREAMACCEPT; + return -1; + } + + if ((sz = recvmsg(test->prot_listener, &msg, 0)) < 0) { + i_errno = IESTREAMACCEPT; + return -1; + } + + /* Parse the retrieved ancillary data and retrieve the incoming + packet's destination address. */ + for (cm = CMSG_FIRSTHDR(&msg); cm != NULL; cm = CMSG_NXTHDR(&msg, cm)) + switch (sa_local.ss_family) { + case AF_INET: +#ifdef HAVE_IP_PKTINFO + if (cm->cmsg_level == IPPROTO_IP && cm->cmsg_type == IP_PKTINFO + && cm->cmsg_len >= CMSG_LEN(sizeof (struct in_pktinfo))) + memcpy(&((struct sockaddr_in *) &sa_local)->sin_addr, + CMSG_DATA(cm) + offsetof(struct in_pktinfo, ipi_addr), + sizeof (struct in_addr)); +#elif defined(HAVE_IP_RECVDSTADDR) + if (cm->cmsg_level == IPPROTO_IP && cm->cmsg_type == IP_RECVDSTADDR + && cm->cmsg_len >= CMSG_LEN(sizeof (struct in_addr))) + memcpy(&((struct sockaddr_in *) &sa_local)->sin_addr, + CMSG_DATA(cm), sizeof (struct in_addr)); +#endif + break; + case AF_INET6: + if (cm->cmsg_level == IPPROTO_IPV6 && cm->cmsg_type == IPV6_PKTINFO + && cm->cmsg_len >= CMSG_LEN(sizeof (struct in6_pktinfo))) + memcpy(&((struct sockaddr_in6 *) &sa_local)->sin6_addr, + CMSG_DATA(cm) + offsetof(struct in6_pktinfo, ipi6_addr), + sizeof (struct in6_addr)); + } + + /* Open and bind the session socket. */ + s = netannounce_sockaddr(test->settings->domain, Pudp, + (struct sockaddr *) &sa_local, len_local, + test->bind_dev, 0); + if (s < 0) { i_errno = IESTREAMACCEPT; return -1; } @@ -455,16 +521,6 @@ iperf_udp_accept(struct iperf_test *test) } } - /* - * Create a new "listening" socket to replace the one we were using before. - */ - FD_CLR(test->prot_listener, &test->read_set); // No control messages from old listener - test->prot_listener = netannounce(test->settings->domain, Pudp, test->bind_address, test->bind_dev, test->server_port); - if (test->prot_listener < 0) { - i_errno = IESTREAMLISTEN; - return -1; - } - FD_SET(test->prot_listener, &test->read_set); test->max_fd = (test->max_fd < test->prot_listener) ? test->prot_listener : test->max_fd; diff --git a/src/net.c b/src/net.c index fc21d01ce..a6a11a3b3 100644 --- a/src/net.c +++ b/src/net.c @@ -258,52 +258,22 @@ netdial(int domain, int proto, const char *local, const char *bind_dev, int loca /***************************************************************/ int -netannounce(int domain, int proto, const char *local, const char *bind_dev, int port) +netannounce_sockaddr(int domain, int proto, const struct sockaddr *local, + socklen_t local_len, const char *bind_dev, int is_listen) { - struct addrinfo hints, *res; - char portstr[6]; int s, opt, saved_errno; - snprintf(portstr, 6, "%d", port); - memset(&hints, 0, sizeof(hints)); - /* - * If binding to the wildcard address with no explicit address - * family specified, then force us to get an AF_INET6 socket. On - * CentOS 6 and MacOS, getaddrinfo(3) with AF_UNSPEC in ai_family, - * and ai_flags containing AI_PASSIVE returns a result structure - * with ai_family set to AF_INET, with the result that we create - * and bind an IPv4 address wildcard address and by default, we - * can't accept IPv6 connections. - * - * On FreeBSD, under the above circumstances, ai_family in the - * result structure is set to AF_INET6. - */ - if (domain == AF_UNSPEC && !local) { - hints.ai_family = AF_INET6; - } - else { - hints.ai_family = domain; - } - hints.ai_socktype = proto; - hints.ai_flags = AI_PASSIVE; - if ((gerror = getaddrinfo(local, portstr, &hints, &res)) != 0) - return -1; - - s = socket(res->ai_family, proto, 0); - if (s < 0) { - freeaddrinfo(res); + s = socket(local->sa_family, proto, 0); + if (s < 0) return -1; - } if (bind_dev) { #if defined(HAVE_SO_BINDTODEVICE) - if (setsockopt(s, SOL_SOCKET, SO_BINDTODEVICE, - bind_dev, IFNAMSIZ) < 0) + if (setsockopt(s, SOL_SOCKET, SO_BINDTODEVICE, bind_dev, IFNAMSIZ) < 0) #endif // HAVE_SO_BINDTODEVICE { saved_errno = errno; close(s); - freeaddrinfo(res); errno = saved_errno; return -1; } @@ -314,7 +284,6 @@ netannounce(int domain, int proto, const char *local, const char *bind_dev, int (char *) &opt, sizeof(opt)) < 0) { saved_errno = errno; close(s); - freeaddrinfo(res); errno = saved_errno; return -1; } @@ -327,7 +296,7 @@ netannounce(int domain, int proto, const char *local, const char *bind_dev, int * even though it implements IPV6_V6ONLY. */ #if defined(IPV6_V6ONLY) && !defined(__OpenBSD__) - if (res->ai_family == AF_INET6 && (domain == AF_UNSPEC || domain == AF_INET6)) { + if (local->sa_family == AF_INET6 && (domain == AF_UNSPEC || domain == AF_INET6)) { if (domain == AF_UNSPEC) opt = 0; else @@ -336,22 +305,38 @@ netannounce(int domain, int proto, const char *local, const char *bind_dev, int (char *) &opt, sizeof(opt)) < 0) { saved_errno = errno; close(s); - freeaddrinfo(res); errno = saved_errno; return -1; } } #endif /* IPV6_V6ONLY */ - if (bind(s, (struct sockaddr *) res->ai_addr, res->ai_addrlen) < 0) { + /* Request the passing of in_pktinfo/in6_pktinfo as ancillary data. */ + if (is_listen && proto == SOCK_DGRAM) { + opt = 1; + if (setsockopt(s, local->sa_family == AF_INET6? + IPPROTO_IPV6: IPPROTO_IP, + local->sa_family == AF_INET6? + IPV6_RECVPKTINFO: +#ifdef HAVE_IP_PKTINFO + IP_PKTINFO, +#elif defined(HAVE_IP_RECVDSTADDR) + IP_RECVDSTADDR, +#endif + &opt, sizeof opt) < 0) { saved_errno = errno; close(s); - freeaddrinfo(res); errno = saved_errno; return -1; } + } - freeaddrinfo(res); + if (bind(s, local, local_len) < 0) { + saved_errno = errno; + close(s); + errno = saved_errno; + return -1; + } if (proto == SOCK_STREAM) { if (listen(s, INT_MAX) < 0) { @@ -365,6 +350,51 @@ netannounce(int domain, int proto, const char *local, const char *bind_dev, int return s; } +int +netannounce(int domain, int proto, const char *local, const char *bind_dev, int port) +{ + struct addrinfo hints, *res; + char portstr[6]; + int s; + + snprintf(portstr, 6, "%d", port); + memset(&hints, 0, sizeof(hints)); + /* + * If binding to the wildcard address with no explicit address + * family specified, then force us to get an AF_INET6 socket. On + * CentOS 6 and MacOS, getaddrinfo(3) with AF_UNSPEC in ai_family, + * and ai_flags containing AI_PASSIVE returns a result structure + * with ai_family set to AF_INET, with the result that we create + * and bind an IPv4 address wildcard address and by default, we + * can't accept IPv6 connections. + * + * On FreeBSD, under the above circumstances, ai_family in the + * result structure is set to AF_INET6. + */ + if (domain == AF_UNSPEC && !local) { + hints.ai_family = AF_INET6; + } + else { + hints.ai_family = domain; + } + hints.ai_socktype = proto; + hints.ai_flags = AI_PASSIVE; + if ((gerror = getaddrinfo(local, portstr, &hints, &res)) != 0) + return -1; + + s = netannounce_sockaddr(domain, proto, + (struct sockaddr *) res->ai_addr, + res->ai_addrlen, bind_dev, 1); + if (s < 0) { + freeaddrinfo(res); + return -1; + } + + freeaddrinfo(res); + + return s; +} + /*******************************************************************/ /* Nread - reads 'count' bytes from a socket */ /********************************************************************/ diff --git a/src/net.h b/src/net.h index 026dfd030..3d8fe9f96 100644 --- a/src/net.h +++ b/src/net.h @@ -30,6 +30,7 @@ int timeout_connect(int s, const struct sockaddr *name, socklen_t namelen, int timeout); int create_socket(int domain, int type, int proto, const char *local, const char *bind_dev, int local_port, const char *server, int port, struct addrinfo **server_res_out); int netdial(int domain, int proto, const char *local, const char *bind_dev, int local_port, const char *server, int port, int timeout); +int netannounce_sockaddr(int domain, int proto, const struct sockaddr *local, socklen_t local_len, const char *bind_dev, int is_listen); int netannounce(int domain, int proto, const char *local, const char *bind_dev, int port); int Nread(int fd, char *buf, size_t count, int prot); int Nrecv(int fd, char *buf, size_t count, int prot, int sock_opt);