/* This software was written by Dirk Engling It is considered beerware. Prost. Skol. Cheers or whatever. $id$ */ /* System */ #include #include #include #include #include #include #include /* Opentracker */ #include "ot_ip.h" #include "ot_socket.h" #include "test.h" /* These cases talk to the loopback interface rather than to a mock, because what is worth pinning down here is the behaviour of the socket options the rest of the tracker relies on, and only the kernel can report that. */ static const char ip_any[16] = {0}; static const char ip_loopback[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}; static const char ip_v4_loopback[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 127, 0, 0, 1}; /* RFC 3849 reserves 2001:db8::/32 for documentation, so no host carries it */ static const char ip_unassigned[16] = {0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}; /* What a socket ended up bound to, which is not always what was asked for: a FreeBSD jail, for one, rewrites a loopback bind to the address it was given. A datagram a socket sends to itself must come back carrying this, so the cases below compare against it rather than against the address they passed to the bind. */ static void bound_address(int s, char ip[16], uint16_t *port) { struct sockaddr_storage ss; socklen_t len = sizeof(ss); memset(&ss, 0, sizeof(ss)); memset(ip, 0, 16); *port = 0; if (getsockname(s, (struct sockaddr *)&ss, &len) == -1) return; /* Spelled out here rather than borrowed from ot_socket.c, so that a wrong idea about how a kernel reports an address cannot hide in both at once */ if (ss.ss_family == AF_INET) { const struct sockaddr_in *sa = (const struct sockaddr_in *)&ss; memcpy(ip, ip_v4mapped_prefix, sizeof(ip_v4mapped_prefix)); memcpy(ip + sizeof(ip_v4mapped_prefix), &sa->sin_addr, 4); *port = ntohs(sa->sin_port); } else { const struct sockaddr_in6 *sa = (const struct sockaddr_in6 *)&ss; memcpy(ip, &sa->sin6_addr, 16); *port = ntohs(sa->sin6_port); } } static int is_nonblocking(int s) { int flags = fcntl(s, F_GETFL, 0); return flags != -1 && (flags & O_NONBLOCK) != 0; } static int sock_flag(int s, int level, int option) { int value = -1; socklen_t len = sizeof(value); if (getsockopt(s, level, option, &value, &len) == -1) return -1; return value != 0; } /* Wait for one event, so a test never blocks forever on a lost packet */ static int wait_for(int s, short events) { struct pollfd pfd; pfd.fd = s; pfd.events = events; pfd.revents = 0; return poll(&pfd, 1, 2000) == 1; } /* ------------------------------------------------------------ invariants */ /* Asked for the wildcard address, because a socket bound to one specific IPv6 address cannot carry IPv4 traffic anyway and Linux turns IPV6_V6ONLY back on in that case. The tracker's own default is the wildcard. */ static void case_socket_options(void) { int udp, tcp; test_case("a bound udp socket is non blocking and serves both families"); udp = socket_udp_bind(ip_any, 0, SOCKET_EXCLUSIVE); CHECK(udp != -1); CHECK_INT(is_nonblocking(udp), 1); CHECK_INT(sock_flag(udp, IPPROTO_IPV6, IPV6_V6ONLY), 0); close(udp); test_case("a listening tcp socket is non blocking and serves both families"); tcp = socket_tcp_listen(ip_any, 0, SOCKET_EXCLUSIVE); CHECK(tcp != -1); CHECK_INT(is_nonblocking(tcp), 1); CHECK_INT(sock_flag(tcp, IPPROTO_IPV6, IPV6_V6ONLY), 0); close(tcp); } #ifdef SO_REUSEPORT static void case_sharing_is_opt_in(void) { int s; test_case("an address is exclusive unless sharing is asked for"); s = socket_udp_bind(ip_loopback, 0, SOCKET_EXCLUSIVE); CHECK(s != -1); CHECK_INT(sock_flag(s, SOL_SOCKET, SO_REUSEPORT), 0); close(s); test_case("and shared when it is"); s = socket_udp_bind(ip_loopback, 0, SOCKET_SHARED); CHECK(s != -1); CHECK_INT(sock_flag(s, SOL_SOCKET, SO_REUSEPORT), 1); close(s); } #endif /* ------------------------------------------------------------------- udp */ static void case_udp_roundtrip(void) { char buf[32], ip[16], own_ip[16]; uint16_t port = 0, own_port = 0; int s; test_case("a datagram comes back with the sender's address and port"); s = socket_udp_bind(ip_loopback, 0, SOCKET_EXCLUSIVE); CHECK(s != -1); bound_address(s, own_ip, &own_port); CHECK(own_port != 0); CHECK_INT(socket_send(s, "opentracker", 11, own_ip, own_port), 11); CHECK_INT(wait_for(s, POLLIN), 1); CHECK_INT(socket_recv(s, buf, sizeof(buf), ip, &port), 11); CHECK_MEM(buf, "opentracker", 11); CHECK_MEM(ip, own_ip, 16); CHECK_INT(port, own_port); close(s); } /* An IPv4 peer must arrive in the v4 mapped form opentracker stores, however the kernel chose to report it. Linux fills a v4 mapped sockaddr_in6 here and the BSDs fill a plain sockaddr_in, so this is the case that tells the two apart. */ static void case_udp_v4mapped(void) { char buf[32], ip[16], receiver_ip[16], sender_ip[16]; uint16_t port = 0, receiver_port = 0, sender_port = 0; int receiver, sender; test_case("an IPv4 sender is reported in its v4 mapped form"); receiver = socket_udp_bind(ip_v4_loopback, 0, SOCKET_EXCLUSIVE); CHECK(receiver != -1); sender = socket_udp_bind(ip_v4_loopback, 0, SOCKET_EXCLUSIVE); CHECK(sender != -1); bound_address(receiver, receiver_ip, &receiver_port); bound_address(sender, sender_ip, &sender_port); CHECK_INT(ip_is_v4mapped(sender_ip), 1); CHECK_INT(socket_send(sender, "v4", 2, receiver_ip, receiver_port), 2); CHECK_INT(wait_for(receiver, POLLIN), 1); CHECK_INT(socket_recv(receiver, buf, sizeof(buf), ip, &port), 2); CHECK_INT(ip_is_v4mapped(ip), 1); CHECK_MEM(ip, sender_ip, 16); CHECK_INT(port, sender_port); close(sender); close(receiver); } /* ------------------------------------------------------------------- tcp */ static void case_tcp_roundtrip(void) { char ip[16], listener_ip[16], client_ip[16]; uint16_t port = 0, listener_port = 0, client_port = 0; int listener, client, accepted; test_case("a connection is accepted and carries the caller's address"); listener = socket_tcp_listen(ip_loopback, 0, SOCKET_EXCLUSIVE); CHECK(listener != -1); bound_address(listener, listener_ip, &listener_port); CHECK(listener_port != 0); client = socket_tcp_connect(ip_loopback, 0, listener_ip, listener_port); CHECK(client != -1); CHECK_INT(wait_for(client, POLLOUT), 1); CHECK_INT(socket_is_connected(client), 1); bound_address(client, client_ip, &client_port); CHECK_INT(wait_for(listener, POLLIN), 1); accepted = socket_accept(listener, ip, &port); CHECK(accepted != -1); CHECK_MEM(ip, client_ip, 16); CHECK_INT(port, client_port); test_case("the accepted socket is handed over non blocking"); CHECK_INT(is_nonblocking(accepted), 1); close(accepted); close(client); close(listener); } static void case_connect_refused(void) { char listener_ip[16]; int listener, client; uint16_t port; test_case("a refused connection is reported by socket_is_connected"); /* Take a port, then drop it, so nothing is listening on it any more */ listener = socket_tcp_listen(ip_loopback, 0, SOCKET_EXCLUSIVE); CHECK(listener != -1); bound_address(listener, listener_ip, &port); close(listener); client = socket_tcp_connect(ip_loopback, 0, listener_ip, port); CHECK(client != -1); CHECK_INT(wait_for(client, POLLOUT), 1); CHECK_INT(socket_is_connected(client), 0); CHECK_INT(errno, ECONNREFUSED); close(client); } /* --------------------------------------------------------- failed binds */ /* The lowest free descriptor is handed out first, so a number that stays put across a failing call shows the call did not keep a socket behind. */ static int next_descriptor(void) { int fd = dup(0); if (fd != -1) close(fd); return fd; } static void case_bind_failure(void) { int before; test_case("binding an address no interface carries fails"); before = next_descriptor(); CHECK(before != -1); CHECK_INT(socket_tcp_listen(ip_unassigned, 0, SOCKET_EXCLUSIVE), -1); CHECK_INT(socket_udp_bind(ip_unassigned, 0, SOCKET_EXCLUSIVE), -1); test_case("a failed bind leaves no socket behind"); CHECK_INT(next_descriptor(), before); } #ifdef WANT_SYNC_LIVE /* Reading an IPv4 multicast option back: Linux answers with an int, the BSDs with a single byte, so ask how much was written. */ static int mcast_option(int s, int option) { int value = 0; socklen_t len = sizeof(value); if (getsockopt(s, IPPROTO_IP, option, &value, &len) == -1) return -1; if (len == 1) return *(unsigned char *)&value; return value; } static void case_mcast_sender(void) { int s; test_case("the sending socket keeps its traffic on the local network"); s = socket_mcast_bind(ip_v4_loopback, 0, 0); CHECK(s != -1); CHECK_INT(mcast_option(s, IP_MULTICAST_TTL), 1); test_case("and does not hear itself unless it is asked to"); CHECK_INT(mcast_option(s, IP_MULTICAST_LOOP), 0); test_case("whoever sends from it is serving clients too, so it cannot block"); CHECK_INT(is_nonblocking(s), 1); close(s); test_case("the proxy asks to hear itself and gets it"); s = socket_mcast_bind(ip_v4_loopback, 0, 1); CHECK(s != -1); CHECK_INT(mcast_option(s, IP_MULTICAST_LOOP), 1); close(s); } #endif void test_socket(void) { case_socket_options(); #ifdef SO_REUSEPORT case_sharing_is_opt_in(); #endif case_udp_roundtrip(); case_udp_v4mapped(); case_tcp_roundtrip(); case_connect_refused(); case_bind_failure(); #ifdef WANT_SYNC_LIVE case_mcast_sender(); #endif }