summaryrefslogtreecommitdiff
path: root/tests/t_ip.c
diff options
context:
space:
mode:
authorDirk Engling <erdgeist@erdgeist.org>2026-10-24 17:22:26 +0200
committerDirk Engling <erdgeist@erdgeist.org>2026-10-24 17:22:26 +0200
commit0f4892b186312ddd5f836121124e21cb99e5682d (patch)
tree68c5853f5970c676f02a4d878eda6c87392b5c70 /tests/t_ip.c
parent60f55f34eeec70842b21accdd7fc0dc5d89493a2 (diff)
Add tests and build infra for the new ip address parser and formatter
Diffstat (limited to 'tests/t_ip.c')
-rw-r--r--tests/t_ip.c217
1 files changed, 217 insertions, 0 deletions
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 @@
1/* This software was written by Dirk Engling <erdgeist@erdgeist.org>
2 It is considered beerware. Prost. Skol. Cheers or whatever.
3
4 $id$ */
5
6/* System */
7#include <stdint.h>
8#include <string.h>
9
10/* Opentracker */
11#include "ot_ip.h"
12#include "test.h"
13
14static char text[64];
15
16static const char *formatted(const char *ip) {
17 text[ip_format(text, ip)] = 0;
18 return text;
19}
20
21/* ------------------------------------------------------------------ IPv4 */
22
23/* ------------------------------------------------------- RFC 5952 output */
24
25static void case_format_rfc5952(void) {
26 test_case("4.1 leading zeros in a group are suppressed");
27 CHECK_STR(formatted("\x20\x01\x0d\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01"), "2001:db8::1");
28
29 test_case("4.2.1 the longest run of zero groups becomes ::");
30 CHECK_STR(formatted("\x20\x01\x0d\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x01"), "2001:db8::2:1");
31
32 test_case("4.2.2 a single zero group is never shortened");
33 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");
34
35 test_case("4.2.1 a longer run wins over an earlier shorter one");
36 CHECK_STR(formatted("\x20\x01\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01"), "2001:0:0:1::1");
37
38 test_case("4.2.3 of two runs of equal length the first is shortened");
39 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");
40
41 test_case("4.3 hexadecimal digits are lower case");
42 CHECK_STR(formatted("\xab\xcd\xef\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01"), "abcd:ef01::1");
43
44 test_case("the unspecified and loopback addresses");
45 CHECK_STR(formatted("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"), "::");
46 CHECK_STR(formatted("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01"), "::1");
47
48 test_case("a full address with no zero group");
49 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");
50}
51
52static void case_format_trailing_zero_group(void) {
53 /* libowfat's formatter looked two bytes past the address to decide this
54 one, so a peer address ending in a zero group came out as "::" whenever
55 the bytes that happened to follow were not themselves zero. In
56 ot_stats.c those bytes are the peer's port. */
57 const char ip[] = "\x20\x01\x0d\xb8\x00\x01\x00\x02\x00\x03\x00\x04\x00\x05\x00\x00";
58
59 test_case("a single trailing zero group is written out, not shortened");
60 CHECK_STR(formatted(ip), "2001:db8:1:2:3:4:5:0");
61
62 test_case("and nothing beyond the sixteen bytes is consulted");
63 {
64 char padded[20];
65 memcpy(padded, ip, 16);
66 memset(padded + 16, 0xff, 4); /* a port and flags, as a peer record has */
67 CHECK_STR(formatted(padded), "2001:db8:1:2:3:4:5:0");
68 memset(padded + 16, 0x00, 4);
69 CHECK_STR(formatted(padded), "2001:db8:1:2:3:4:5:0");
70 }
71}
72
73static void case_v4mapped(void) {
74 const char mapped[] = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x0a\x00\x00\x01";
75 const char unmapped[] = "\x20\x01\x0d\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01";
76
77 test_case("a v4 mapped address is recognised");
78 CHECK(ip_is_v4mapped(mapped));
79 CHECK_INT(ip_is_v4mapped(unmapped), 0);
80
81 /* The tracker has only ever shown the plain quad, so that is the one
82 form the formatter produces; "::ffff:10.0.0.1" is not written anywhere */
83 test_case("a mapped address is written as the plain dotted quad");
84 CHECK_STR(formatted(mapped), "10.0.0.1");
85 CHECK_STR(formatted("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x00\x00\x00\x00"), "0.0.0.0");
86 CHECK_STR(formatted("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff"), "255.255.255.255");
87
88 test_case("anything else follows RFC 5952");
89 CHECK_STR(formatted(unmapped), "2001:db8::1");
90}
91
92/* -------------------------------------------------------- RFC 4291 input */
93
94static void case_scan_forms(void) {
95 char ip[16];
96
97 test_case("2.2 form 1, every group spelled out");
98 CHECK_INT(ip_scan("1111:2222:3333:4444:5555:6666:7777:8888", ip), 39);
99 CHECK_MEM(ip, "\x11\x11\x22\x22\x33\x33\x44\x44\x55\x55\x66\x66\x77\x77\x88\x88", 16);
100
101 test_case("2.2 form 2, zero groups abbreviated");
102 CHECK_INT(ip_scan("2001:db8::1", ip), 11);
103 CHECK_MEM(ip, "\x20\x01\x0d\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01", 16);
104 CHECK_INT(ip_scan("::1", ip), 3);
105 CHECK_MEM(ip, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01", 16);
106 CHECK_INT(ip_scan("::", ip), 2);
107 CHECK_MEM(ip, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 16);
108 CHECK_INT(ip_scan("1::", ip), 3);
109 CHECK_MEM(ip, "\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 16);
110
111 test_case("2.2 form 3, a dotted quad in the last two groups");
112 CHECK_INT(ip_scan("::ffff:1.2.3.4", ip), 14);
113 CHECK_MEM(ip, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x01\x02\x03\x04", 16);
114 CHECK_INT(ip_scan("1:2:3:4:5:6:1.2.3.4", ip), 19);
115 CHECK_MEM(ip, "\x00\x01\x00\x02\x00\x03\x00\x04\x00\x05\x00\x06\x01\x02\x03\x04", 16);
116
117 test_case("upper case input is accepted");
118 CHECK_INT(ip_scan("2001:DB8::1", ip), 11);
119 CHECK_MEM(ip, "\x20\x01\x0d\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01", 16);
120}
121
122static void case_scan_accepts_bare_ipv4(void) {
123 char ip[16];
124
125 /* Every address opentracker holds is sixteen bytes, so an operator writing
126 a plain IPv4 address in the config has to land as the v4 mapped form */
127 test_case("a bare IPv4 address is stored v4 mapped");
128 CHECK_INT(ip_scan("10.0.0.1", ip), 8);
129 CHECK_MEM(ip, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x0a\x00\x00\x01", 16);
130 CHECK(ip_is_v4mapped(ip));
131}
132
133static void case_scan_reports_length(void) {
134 char ip[16];
135
136 /* The config parser continues from where the address ended, which is how
137 "address:port" and "address/bits" are read */
138 test_case("scanning stops at the character after the address");
139 CHECK_INT(ip_scan("10.0.0.1:6969", ip), 8);
140 CHECK_INT(ip_scan("2001:db8::1/64", ip), 11);
141 CHECK_INT(ip_scan("::1]", ip), 3);
142 CHECK_INT(ip_scan("1:2:3:4:5:6:7:8 trailing", ip), 15);
143}
144
145static void case_scan_rejects(void) {
146 char ip[16];
147
148 test_case("an empty or junk string is refused");
149 CHECK_INT(ip_scan("", ip), 0);
150 CHECK_INT(ip_scan("x", ip), 0);
151 CHECK_INT(ip_scan(":", ip), 0);
152 CHECK_INT(ip_scan(":1:2", ip), 0);
153
154 test_case("too few groups without :: are refused");
155 CHECK_INT(ip_scan("1:2:3:4:5:6:7", ip), 0);
156 CHECK_INT(ip_scan("2001:db8:1", ip), 0);
157
158 /* libowfat accepted a prefix of each of these and carried on. Taking only
159 part of a malformed address is worse than refusing it, especially where
160 the text comes from a config file or an X-Forwarded-For header. */
161 test_case("a second :: is refused rather than truncated");
162 CHECK_INT(ip_scan("1::2::3", ip), 0);
163 CHECK_INT(ip_scan("::1::", ip), 0);
164 CHECK_INT(ip_scan("::::", ip), 0);
165
166 test_case("more than eight groups is refused");
167 CHECK_INT(ip_scan("1:2:3:4:5:6:7:8:9", ip), 0);
168
169 test_case("a group of more than four digits is refused, not truncated");
170 CHECK_INT(ip_scan("12345::1", ip), 0);
171 CHECK_INT(ip_scan("fffff::1", ip), 0);
172}
173
174static void case_round_trip(void) {
175 /* Whatever the formatter writes, the scanner has to read back */
176 static const char *const samples[] = {"::",
177 "::1",
178 "1::",
179 "2001:db8::1",
180 "2001:db8:1:2:3:4:5:0",
181 "1111:2222:3333:4444:5555:6666:7777:8888",
182 "2001::1:0:0:1",
183 "abcd:ef01::1",
184 NULL};
185 char ip[16], again[16];
186 size_t i;
187
188 test_case("every formatted address scans back to the same bytes");
189 for (i = 0; samples[i]; ++i) {
190 size_t n = ip_scan(samples[i], ip);
191 if (n != strlen(samples[i])) {
192 test_failf(__FILE__, __LINE__, "\"%s\" consumed %zu of %zu", samples[i], n, strlen(samples[i]));
193 return;
194 }
195 text[ip_format(text, ip)] = 0;
196 if (strcmp(text, samples[i])) {
197 test_failf(__FILE__, __LINE__, "\"%s\" formatted back as \"%s\"", samples[i], text);
198 return;
199 }
200 if (!ip_scan(text, again) || memcmp(ip, again, 16)) {
201 test_failf(__FILE__, __LINE__, "\"%s\" did not survive a second pass", samples[i]);
202 return;
203 }
204 }
205 ++test_checks;
206}
207
208void test_ip(void) {
209 case_format_rfc5952();
210 case_format_trailing_zero_group();
211 case_v4mapped();
212 case_scan_forms();
213 case_scan_accepts_bare_ipv4();
214 case_scan_reports_length();
215 case_scan_rejects();
216 case_round_trip();
217}