From 0f4892b186312ddd5f836121124e21cb99e5682d Mon Sep 17 00:00:00 2001 From: Dirk Engling Date: Sat, 24 Oct 2026 17:22:26 +0200 Subject: Add tests and build infra for the new ip address parser and formatter --- Makefile | 10 +-- tests/t_ip.c | 217 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 222 insertions(+), 5 deletions(-) create mode 100644 tests/t_ip.c diff --git a/Makefile b/Makefile index f35d1af..27d635d 100644 --- a/Makefile +++ b/Makefile @@ -68,9 +68,9 @@ 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_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_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 +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 OBJECTS = $(SOURCES:%.c=%.o) OBJECTS_debug = $(SOURCES:%.c=%.debug.o) @@ -105,8 +105,8 @@ 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 -TEST_UNITS=scan_urlencoded_query.c ot_buf.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 +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_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. diff --git a/tests/t_ip.c b/tests/t_ip.c new file mode 100644 index 0000000..2fe3a72 --- /dev/null +++ b/tests/t_ip.c @@ -0,0 +1,217 @@ +/* This software was written by Dirk Engling + It is considered beerware. Prost. Skol. Cheers or whatever. + + $id$ */ + +/* System */ +#include +#include + +/* Opentracker */ +#include "ot_ip.h" +#include "test.h" + +static char text[64]; + +static const char *formatted(const char *ip) { + text[ip_format(text, ip)] = 0; + return text; +} + +/* ------------------------------------------------------------------ IPv4 */ + +/* ------------------------------------------------------- RFC 5952 output */ + +static void case_format_rfc5952(void) { + test_case("4.1 leading zeros in a group are suppressed"); + CHECK_STR(formatted("\x20\x01\x0d\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01"), "2001:db8::1"); + + test_case("4.2.1 the longest run of zero groups becomes ::"); + CHECK_STR(formatted("\x20\x01\x0d\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x01"), "2001:db8::2:1"); + + test_case("4.2.2 a single zero group is never shortened"); + CHECK_STR(formatted("\x20\x01\x0d\xb8\x00\x00\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01"), "2001:db8:0:1:1:1:1:1"); + + test_case("4.2.1 a longer run wins over an earlier shorter one"); + CHECK_STR(formatted("\x20\x01\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01"), "2001:0:0:1::1"); + + test_case("4.2.3 of two runs of equal length the first is shortened"); + CHECK_STR(formatted("\x20\x01\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x01\x00\x01"), "2001::1:0:0:1:1"); + + test_case("4.3 hexadecimal digits are lower case"); + CHECK_STR(formatted("\xab\xcd\xef\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01"), "abcd:ef01::1"); + + test_case("the unspecified and loopback addresses"); + CHECK_STR(formatted("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"), "::"); + CHECK_STR(formatted("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01"), "::1"); + + test_case("a full address with no zero group"); + CHECK_STR(formatted("\x11\x11\x22\x22\x33\x33\x44\x44\x55\x55\x66\x66\x77\x77\x88\x88"), "1111:2222:3333:4444:5555:6666:7777:8888"); +} + +static void case_format_trailing_zero_group(void) { + /* libowfat's formatter looked two bytes past the address to decide this + one, so a peer address ending in a zero group came out as "::" whenever + the bytes that happened to follow were not themselves zero. In + ot_stats.c those bytes are the peer's port. */ + const char ip[] = "\x20\x01\x0d\xb8\x00\x01\x00\x02\x00\x03\x00\x04\x00\x05\x00\x00"; + + test_case("a single trailing zero group is written out, not shortened"); + CHECK_STR(formatted(ip), "2001:db8:1:2:3:4:5:0"); + + test_case("and nothing beyond the sixteen bytes is consulted"); + { + char padded[20]; + memcpy(padded, ip, 16); + memset(padded + 16, 0xff, 4); /* a port and flags, as a peer record has */ + CHECK_STR(formatted(padded), "2001:db8:1:2:3:4:5:0"); + memset(padded + 16, 0x00, 4); + CHECK_STR(formatted(padded), "2001:db8:1:2:3:4:5:0"); + } +} + +static void case_v4mapped(void) { + const char mapped[] = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x0a\x00\x00\x01"; + const char unmapped[] = "\x20\x01\x0d\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01"; + + test_case("a v4 mapped address is recognised"); + CHECK(ip_is_v4mapped(mapped)); + CHECK_INT(ip_is_v4mapped(unmapped), 0); + + /* The tracker has only ever shown the plain quad, so that is the one + form the formatter produces; "::ffff:10.0.0.1" is not written anywhere */ + test_case("a mapped address is written as the plain dotted quad"); + CHECK_STR(formatted(mapped), "10.0.0.1"); + CHECK_STR(formatted("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x00\x00\x00\x00"), "0.0.0.0"); + CHECK_STR(formatted("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff"), "255.255.255.255"); + + test_case("anything else follows RFC 5952"); + CHECK_STR(formatted(unmapped), "2001:db8::1"); +} + +/* -------------------------------------------------------- RFC 4291 input */ + +static void case_scan_forms(void) { + char ip[16]; + + test_case("2.2 form 1, every group spelled out"); + CHECK_INT(ip_scan("1111:2222:3333:4444:5555:6666:7777:8888", ip), 39); + CHECK_MEM(ip, "\x11\x11\x22\x22\x33\x33\x44\x44\x55\x55\x66\x66\x77\x77\x88\x88", 16); + + test_case("2.2 form 2, zero groups abbreviated"); + CHECK_INT(ip_scan("2001:db8::1", ip), 11); + CHECK_MEM(ip, "\x20\x01\x0d\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01", 16); + CHECK_INT(ip_scan("::1", ip), 3); + CHECK_MEM(ip, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01", 16); + CHECK_INT(ip_scan("::", ip), 2); + CHECK_MEM(ip, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 16); + CHECK_INT(ip_scan("1::", ip), 3); + CHECK_MEM(ip, "\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 16); + + test_case("2.2 form 3, a dotted quad in the last two groups"); + CHECK_INT(ip_scan("::ffff:1.2.3.4", ip), 14); + CHECK_MEM(ip, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x01\x02\x03\x04", 16); + CHECK_INT(ip_scan("1:2:3:4:5:6:1.2.3.4", ip), 19); + CHECK_MEM(ip, "\x00\x01\x00\x02\x00\x03\x00\x04\x00\x05\x00\x06\x01\x02\x03\x04", 16); + + test_case("upper case input is accepted"); + CHECK_INT(ip_scan("2001:DB8::1", ip), 11); + CHECK_MEM(ip, "\x20\x01\x0d\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01", 16); +} + +static void case_scan_accepts_bare_ipv4(void) { + char ip[16]; + + /* Every address opentracker holds is sixteen bytes, so an operator writing + a plain IPv4 address in the config has to land as the v4 mapped form */ + test_case("a bare IPv4 address is stored v4 mapped"); + CHECK_INT(ip_scan("10.0.0.1", ip), 8); + CHECK_MEM(ip, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x0a\x00\x00\x01", 16); + CHECK(ip_is_v4mapped(ip)); +} + +static void case_scan_reports_length(void) { + char ip[16]; + + /* The config parser continues from where the address ended, which is how + "address:port" and "address/bits" are read */ + test_case("scanning stops at the character after the address"); + CHECK_INT(ip_scan("10.0.0.1:6969", ip), 8); + CHECK_INT(ip_scan("2001:db8::1/64", ip), 11); + CHECK_INT(ip_scan("::1]", ip), 3); + CHECK_INT(ip_scan("1:2:3:4:5:6:7:8 trailing", ip), 15); +} + +static void case_scan_rejects(void) { + char ip[16]; + + test_case("an empty or junk string is refused"); + CHECK_INT(ip_scan("", ip), 0); + CHECK_INT(ip_scan("x", ip), 0); + CHECK_INT(ip_scan(":", ip), 0); + CHECK_INT(ip_scan(":1:2", ip), 0); + + test_case("too few groups without :: are refused"); + CHECK_INT(ip_scan("1:2:3:4:5:6:7", ip), 0); + CHECK_INT(ip_scan("2001:db8:1", ip), 0); + + /* libowfat accepted a prefix of each of these and carried on. Taking only + part of a malformed address is worse than refusing it, especially where + the text comes from a config file or an X-Forwarded-For header. */ + test_case("a second :: is refused rather than truncated"); + CHECK_INT(ip_scan("1::2::3", ip), 0); + CHECK_INT(ip_scan("::1::", ip), 0); + CHECK_INT(ip_scan("::::", ip), 0); + + test_case("more than eight groups is refused"); + CHECK_INT(ip_scan("1:2:3:4:5:6:7:8:9", ip), 0); + + test_case("a group of more than four digits is refused, not truncated"); + CHECK_INT(ip_scan("12345::1", ip), 0); + CHECK_INT(ip_scan("fffff::1", ip), 0); +} + +static void case_round_trip(void) { + /* Whatever the formatter writes, the scanner has to read back */ + static const char *const samples[] = {"::", + "::1", + "1::", + "2001:db8::1", + "2001:db8:1:2:3:4:5:0", + "1111:2222:3333:4444:5555:6666:7777:8888", + "2001::1:0:0:1", + "abcd:ef01::1", + NULL}; + char ip[16], again[16]; + size_t i; + + test_case("every formatted address scans back to the same bytes"); + for (i = 0; samples[i]; ++i) { + size_t n = ip_scan(samples[i], ip); + if (n != strlen(samples[i])) { + test_failf(__FILE__, __LINE__, "\"%s\" consumed %zu of %zu", samples[i], n, strlen(samples[i])); + return; + } + text[ip_format(text, ip)] = 0; + if (strcmp(text, samples[i])) { + test_failf(__FILE__, __LINE__, "\"%s\" formatted back as \"%s\"", samples[i], text); + return; + } + if (!ip_scan(text, again) || memcmp(ip, again, 16)) { + test_failf(__FILE__, __LINE__, "\"%s\" did not survive a second pass", samples[i]); + return; + } + } + ++test_checks; +} + +void test_ip(void) { + case_format_rfc5952(); + case_format_trailing_zero_group(); + case_v4mapped(); + case_scan_forms(); + case_scan_accepts_bare_ipv4(); + case_scan_reports_length(); + case_scan_rejects(); + case_round_trip(); +} -- cgit v1.3