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 --- Makefile | 29 +++-- ot_socket.c | 256 ++++++++++++++++++++++++++++++++++++++++++++ ot_socket.h | 45 ++++++++ tests/runner.c | 1 + tests/t_socket.c | 315 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ tests/test.h | 1 + 6 files changed, 637 insertions(+), 10 deletions(-) create mode 100644 ot_socket.c create mode 100644 ot_socket.h create mode 100644 tests/t_socket.c diff --git a/Makefile b/Makefile index 27d635d..2587b62 100644 --- a/Makefile +++ b/Makefile @@ -68,22 +68,25 @@ CFLAGS+=-I$(LIBOWFAT_HEADERS) -DGIT_VERSION=$(GIT_VERSION) -Wall -pipe -pthread LDFLAGS+=-L$(LIBOWFAT_LIBRARY) -lowfat -pthread BINARY =opentracker -HEADERS=trackerlogic.h scan_urlencoded_query.h ot_buf.h ot_hex.h ot_ip.h ot_scan.h ot_mutex.h ot_random.h ot_stats.h ot_vector.h ot_clean.h ot_udp.h ot_iovec.h ot_fullscrape.h ot_accesslist.h ot_http.h ot_livesync.h ot_rijndael.h -SOURCES=opentracker.c trackerlogic.c scan_urlencoded_query.c ot_buf.c ot_hex.c ot_ip.c ot_scan.c ot_mutex.c ot_random.c ot_stats.c ot_vector.c ot_clean.c ot_udp.c ot_iovec.c ot_fullscrape.c ot_accesslist.c ot_http.c ot_livesync.c ot_rijndael.c -SOURCES_proxy=proxy.c ot_vector.c ot_mutex.c ot_random.c ot_scan.c ot_ip.c +HEADERS=trackerlogic.h scan_urlencoded_query.h ot_buf.h ot_hex.h ot_ip.h ot_scan.h ot_socket.h ot_mutex.h ot_random.h ot_stats.h ot_vector.h ot_clean.h ot_udp.h ot_iovec.h ot_fullscrape.h ot_accesslist.h ot_http.h ot_livesync.h ot_rijndael.h +SOURCES=opentracker.c trackerlogic.c scan_urlencoded_query.c ot_buf.c ot_hex.c ot_ip.c ot_scan.c ot_socket.c ot_mutex.c ot_random.c ot_stats.c ot_vector.c ot_clean.c ot_udp.c ot_iovec.c ot_fullscrape.c ot_accesslist.c ot_http.c ot_livesync.c ot_rijndael.c +SOURCES_proxy=proxy.c ot_vector.c ot_mutex.c ot_random.c ot_scan.c ot_ip.c ot_socket.c OBJECTS = $(SOURCES:%.c=%.o) OBJECTS_debug = $(SOURCES:%.c=%.debug.o) -OBJECTS_proxy = $(SOURCES_proxy:%.c=%.o) -OBJECTS_proxy_debug = $(SOURCES_proxy:%.c=%.debug.o) +OBJECTS_proxy = $(SOURCES_proxy:%.c=%.proxy.o) +OBJECTS_proxy_debug = $(SOURCES_proxy:%.c=%.proxy.debug.o) -.SUFFIXES: .debug.o .o .c +.SUFFIXES: .proxy.debug.o .proxy.o .debug.o .o .c all: $(BINARY) $(BINARY).debug CFLAGS_production = $(CFLAGS) $(OPTS_production) $(FEATURES) CFLAGS_debug = $(CFLAGS) $(OPTS_debug) $(FEATURES) +CFLAGS_proxy = $(CFLAGS_production) -DWANT_SYNC_LIVE +CFLAGS_proxy_debug = $(CFLAGS_debug) -DWANT_SYNC_LIVE + $(BINARY): $(OBJECTS) $(HEADERS) $(CC) -o $@ $(OBJECTS) $(LDFLAGS) $(STRIP) $@ @@ -94,6 +97,12 @@ proxy: $(OBJECTS_proxy) $(HEADERS) proxy.debug: $(OBJECTS_proxy_debug) $(HEADERS) $(CC) -o $@ $(OBJECTS_proxy_debug) $(LDFLAGS) +.c.proxy.debug.o : $(HEADERS) + $(CC) -c -o $@ $(CFLAGS_proxy_debug) $(<:.proxy.debug.o=.c) + +.c.proxy.o : $(HEADERS) + $(CC) -c -o $@ $(CFLAGS_proxy) $(<:.proxy.o=.c) + .c.debug.o : $(HEADERS) $(CC) -c -o $@ $(CFLAGS_debug) $(<:.debug.o=.c) @@ -105,12 +114,12 @@ proxy.debug: $(OBJECTS_proxy_debug) $(HEADERS) # has to. tests/stubs.c supplies the few symbols those modules reference but # the tests never reach. TESTBIN=tests/runner -TEST_SOURCES=tests/runner.c tests/test.c tests/stubs.c tests/t_scan_urlencoded_query.c tests/t_iovec.c tests/t_vector.c tests/t_random.c tests/t_hex.c tests/t_accesslist.c tests/t_scan.c tests/t_buf.c tests/t_ip.c -TEST_UNITS=scan_urlencoded_query.c ot_buf.c ot_ip.c ot_iovec.c ot_vector.c ot_random.c ot_hex.c ot_scan.c ot_accesslist.c +TEST_SOURCES=tests/runner.c tests/test.c tests/stubs.c tests/t_scan_urlencoded_query.c tests/t_iovec.c tests/t_vector.c tests/t_random.c tests/t_hex.c tests/t_accesslist.c tests/t_scan.c tests/t_buf.c tests/t_ip.c tests/t_socket.c +TEST_UNITS=scan_urlencoded_query.c ot_buf.c ot_ip.c ot_socket.c ot_iovec.c ot_vector.c ot_random.c ot_hex.c ot_scan.c ot_accesslist.c TEST_HEADERS=tests/test.h $(HEADERS) # Tests reach code that sits behind feature flags, so the units under test are # built with those features enabled. A test binary is not a production binary. -FEATURES_test=$(FEATURES) -DWANT_ACCESSLIST_BLACK +FEATURES_test=$(FEATURES) -DWANT_ACCESSLIST_BLACK -DWANT_SYNC_LIVE CFLAGS_test = $(CFLAGS) -I. -g -O1 $(FEATURES_test) $(TESTBIN): $(TEST_SOURCES) $(TEST_UNITS) $(TEST_HEADERS) @@ -122,7 +131,7 @@ check: $(TESTBIN) .PHONY: all check clean install clean: - rm -rf opentracker opentracker.debug $(TESTBIN) *.o *~ + rm -rf opentracker opentracker.debug proxy proxy.debug $(TESTBIN) *.o *~ install: install -m 755 opentracker $(DESTDIR)$(BINDIR) diff --git a/ot_socket.c b/ot_socket.c new file mode 100644 index 0000000..e26ad52 --- /dev/null +++ b/ot_socket.c @@ -0,0 +1,256 @@ +/* This software was written by Dirk Engling + It is considered beerware. Prost. Skol. Cheers or whatever. + + $id$ */ + +/* System */ +#include +#include +#include +#include +#include +#include + +/* Opentracker */ +#include "ot_ip.h" +#include "ot_socket.h" + +int g_socket_reuseport; + +static void sockaddr_from_ip(struct sockaddr_in6 *sa, const ot_ip6 ip, uint16_t port) { + memset(sa, 0, sizeof(*sa)); + sa->sin6_family = AF_INET6; + sa->sin6_port = htons(port); + memcpy(&sa->sin6_addr, ip, sizeof(ot_ip6)); +} + +/* Linux reports an IPv4 peer on a dual stack socket as a v4 mapped + sockaddr_in6, the BSDs and macOS hand back a plain sockaddr_in instead, so + both forms have to be read. */ +static void ip_from_sockaddr(ot_ip6 ip, uint16_t *port, const struct sockaddr_storage *ss) { + 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, sizeof(ot_ip6)); + *port = ntohs(sa->sin6_port); + } +} + +static int set_nonblocking(int s) { + int flags = fcntl(s, F_GETFL, 0); + + if (flags == -1) + return -1; + return fcntl(s, F_SETFL, flags | O_NONBLOCK); +} + +/* Close without letting the failure we are about to report be overwritten */ +static int close_and_fail(int s) { + int saved = errno; + close(s); + errno = saved; + return -1; +} + +static int socket_v6(int type) { + int s = socket(PF_INET6, type, 0); + int zero = 0; + + if (s == -1) + return -1; + + /* A tracker that answered only one family would be half a tracker */ + if (setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, &zero, sizeof(zero)) == -1) + return close_and_fail(s); + + if (set_nonblocking(s) == -1) + return close_and_fail(s); + + return s; +} + +static int socket_bind_v6(int s, const ot_ip6 ip, uint16_t port) { + struct sockaddr_in6 sa; + int one = 1; + + setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); +#ifdef SO_REUSEPORT + if (g_socket_reuseport) + setsockopt(s, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one)); +#endif + + sockaddr_from_ip(&sa, ip, port); + return bind(s, (struct sockaddr *)&sa, sizeof(sa)); +} + +int socket_tcp_listen(const ot_ip6 ip, uint16_t port) { + int s = socket_v6(SOCK_STREAM); + + if (s == -1) + return -1; + if (socket_bind_v6(s, ip, port) == -1 || listen(s, SOMAXCONN) == -1) + return close_and_fail(s); + + return s; +} + +int socket_udp_bind(const ot_ip6 ip, uint16_t port) { + int s = socket_v6(SOCK_DGRAM); + + if (s == -1) + return -1; + if (socket_bind_v6(s, ip, port) == -1) + return close_and_fail(s); + + return s; +} + +int socket_tcp_connect(const ot_ip6 srcip, uint16_t srcport, const ot_ip6 dstip, uint16_t dstport) { + struct sockaddr_in6 sa; + int s = socket_v6(SOCK_STREAM); + + if (s == -1) + return -1; + if (socket_bind_v6(s, srcip, srcport) == -1) + return close_and_fail(s); + + sockaddr_from_ip(&sa, dstip, dstport); + if (connect(s, (struct sockaddr *)&sa, sizeof(sa)) == -1 && errno != EINPROGRESS && errno != EWOULDBLOCK) + return close_and_fail(s); + + return s; +} + +int socket_is_connected(int s) { + int error = 0; + socklen_t len = sizeof(error); + + if (getsockopt(s, SOL_SOCKET, SO_ERROR, &error, &len) == -1) + return 0; + if (error) { + errno = error; + return 0; + } + return 1; +} + +int socket_accept(int s, ot_ip6 ip, uint16_t *port) { + struct sockaddr_storage ss; + socklen_t len = sizeof(ss); + int fd; + + memset(&ss, 0, sizeof(ss)); + if ((fd = accept(s, (struct sockaddr *)&ss, &len)) == -1) + return -1; + + /* Linux hands over a blocking socket whatever the listening socket was, the + BSDs and macOS inherit the listener's flags, so say which one we want. */ + if (set_nonblocking(fd) == -1) + return close_and_fail(fd); + + ip_from_sockaddr(ip, port, &ss); + return fd; +} + +ssize_t socket_recv(int s, void *buf, size_t len, ot_ip6 ip, uint16_t *port) { + struct sockaddr_storage ss; + socklen_t sslen = sizeof(ss); + ssize_t received; + + memset(&ss, 0, sizeof(ss)); + received = recvfrom(s, buf, len, 0, (struct sockaddr *)&ss, &sslen); + if (received == -1) + return -1; + + ip_from_sockaddr(ip, port, &ss); + return received; +} + +ssize_t socket_send(int s, const void *buf, size_t len, const ot_ip6 ip, uint16_t port) { + struct sockaddr_in6 sa; + + sockaddr_from_ip(&sa, ip, port); + return sendto(s, buf, len, 0, (struct sockaddr *)&sa, sizeof(sa)); +} + +#ifdef WANT_SYNC_LIVE +static void sockaddr4_from_ip(struct sockaddr_in *sa, const ot_ip6 ip, uint16_t port) { + memset(sa, 0, sizeof(*sa)); + sa->sin_family = AF_INET; + sa->sin_port = htons(port); + memcpy(&sa->sin_addr, ip + sizeof(ip_v4mapped_prefix), 4); +} + +static int socket_v4_bind(const ot_ip6 ip, uint16_t port) { + struct sockaddr_in sa; + int s, one = 1; + + if ((s = socket(PF_INET, SOCK_DGRAM, 0)) == -1) + return -1; + + setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); +#ifdef SO_REUSEPORT + if (g_socket_reuseport) + setsockopt(s, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one)); +#endif + + sockaddr4_from_ip(&sa, ip, port); + if (bind(s, (struct sockaddr *)&sa, sizeof(sa)) == -1) + return close_and_fail(s); + + return s; +} + +int socket_mcast_join(const ot_ip6 group, const ot_ip6 interface, uint16_t port) { + struct ip_mreq mreq; + ot_ip6 any; + int s; + + /* The group is what is listened to, so the socket takes the wildcard and + the join names the interface to receive it on */ + memcpy(any, ip_v4mapped_prefix, sizeof(ip_v4mapped_prefix)); + memset(any + sizeof(ip_v4mapped_prefix), 0, 4); + + if ((s = socket_v4_bind(any, port)) == -1) + return -1; + + memset(&mreq, 0, sizeof(mreq)); + memcpy(&mreq.imr_multiaddr, group + sizeof(ip_v4mapped_prefix), 4); + memcpy(&mreq.imr_interface, interface + sizeof(ip_v4mapped_prefix), 4); + if (setsockopt(s, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) == -1) + return close_and_fail(s); + + return s; +} + +int socket_mcast_bind(const ot_ip6 interface, uint16_t port, int loopback) { + /* The BSDs and macOS want a byte for the IPv4 multicast options, Linux + accepts either that or an int. */ + unsigned char ttl = 1, loop = (unsigned char)(loopback != 0); + int s = socket_v4_bind(interface, port); + + if (s == -1) + return -1; + if (set_nonblocking(s) == -1) + return close_and_fail(s); + + if (setsockopt(s, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, sizeof(ttl)) == -1 || + setsockopt(s, IPPROTO_IP, IP_MULTICAST_LOOP, &loop, sizeof(loop)) == -1) + return close_and_fail(s); + + return s; +} + +ssize_t socket_mcast_send(int s, const void *buf, size_t len, const ot_ip6 group, uint16_t port) { + struct sockaddr_in sa; + + sockaddr4_from_ip(&sa, group, port); + return sendto(s, buf, len, 0, (struct sockaddr *)&sa, sizeof(sa)); +} +#endif diff --git a/ot_socket.h b/ot_socket.h new file mode 100644 index 0000000..20fd839 --- /dev/null +++ b/ot_socket.h @@ -0,0 +1,45 @@ +/* This software was written by Dirk Engling + It is considered beerware. Prost. Skol. Cheers or whatever. + + $id$ */ + +#ifndef OT_SOCKET_H__ +#define OT_SOCKET_H__ + +#include +#include + +#include "trackerlogic.h" + +/* Every socket opentracker opens is an AF_INET6 socket with IPV6_V6ONLY + cleared, so one descriptor serves both families and an IPv4 peer arrives + as the v4 mapped address */ + +/* Ask the kernel to let several processes bind the same address */ +extern int g_socket_reuseport; + +/* Create, bind and, for TCP, listen. The returned socket is non blocking + Returns -1 on failure with errno set */ +int socket_tcp_listen(const ot_ip6 ip, uint16_t port); +int socket_udp_bind(const ot_ip6 ip, uint16_t port); + +int socket_tcp_connect(const ot_ip6 srcip, uint16_t srcport, const ot_ip6 dstip, uint16_t dstport); +int socket_is_connected(int s); +int socket_accept(int s, ot_ip6 ip, uint16_t *port); + +ssize_t socket_recv(int s, void *buf, size_t len, ot_ip6 ip, uint16_t *port); +ssize_t socket_send(int s, const void *buf, size_t len, const ot_ip6 ip, uint16_t port); + +#ifdef WANT_SYNC_LIVE +/* + socket_mcast_join takes the wildcard address and names the interface in the + join, and hands back a blocking socket, since the thread that reads it does + nothing else. socket_mcast_bind hands back a non blocking one, because the + thread that sends from it is also serving clients. Incoming datagrams are + read with socket_recv */ +int socket_mcast_join(const ot_ip6 group, const ot_ip6 interface, uint16_t port); +int socket_mcast_bind(const ot_ip6 interface, uint16_t port, int loopback); +ssize_t socket_mcast_send(int s, const void *buf, size_t len, const ot_ip6 group, uint16_t port); +#endif + +#endif diff --git a/tests/runner.c b/tests/runner.c index 10b6625..db62246 100644 --- a/tests/runner.c +++ b/tests/runner.c @@ -22,6 +22,7 @@ static const struct module modules[] = {{"scan_urlencoded_query", test_scan_urle {"ot_scan", test_scan}, {"ot_buf", test_buf}, {"ot_ip", test_ip}, + {"ot_socket", test_socket}, {"ot_accesslist", test_accesslist}, {NULL, NULL}}; 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 +} diff --git a/tests/test.h b/tests/test.h index 7449b73..d79c4d2 100644 --- a/tests/test.h +++ b/tests/test.h @@ -85,5 +85,6 @@ void test_accesslist(void); void test_scan(void); void test_buf(void); void test_ip(void); +void test_socket(void); #endif -- cgit v1.3