From 7af7598086e5a3f9a75507cd6f9907a98faf3d9d Mon Sep 17 00:00:00 2001 From: Dirk Engling Date: Sun, 25 Oct 2026 02:41:48 +0200 Subject: Add sub system to abstract low level multi platform socket handling ourselves --- tests/t_socket.c | 315 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 315 insertions(+) create mode 100644 tests/t_socket.c (limited to 'tests/t_socket.c') diff --git a/tests/t_socket.c b/tests/t_socket.c new file mode 100644 index 0000000..cf7d291 --- /dev/null +++ b/tests/t_socket.c @@ -0,0 +1,315 @@ +/* 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 uint16_t bound_port(int s) { + char ip[16]; + uint16_t port; + + bound_address(s, ip, &port); + return 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); + 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); + 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_reuseport_is_opt_in(void) { + int s; + + test_case("reuseport stays off unless the operator asks for it"); + g_socket_reuseport = 0; + s = socket_udp_bind(ip_loopback, 0); + CHECK(s != -1); + CHECK_INT(sock_flag(s, SOL_SOCKET, SO_REUSEPORT), 0); + close(s); + + test_case("reuseport reaches the socket when it is asked for"); + g_socket_reuseport = 1; + s = socket_udp_bind(ip_loopback, 0); + CHECK(s != -1); + CHECK_INT(sock_flag(s, SOL_SOCKET, SO_REUSEPORT), 1); + close(s); + + g_socket_reuseport = 0; +} +#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); + 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); + CHECK(receiver != -1); + sender = socket_udp_bind(ip_v4_loopback, 0); + 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); + 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); + 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), -1); + CHECK_INT(socket_udp_bind(ip_unassigned, 0), -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_reuseport_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 +} -- cgit v1.3