diff options
Diffstat (limited to 'tests/t_socket.c')
| -rw-r--r-- | tests/t_socket.c | 315 |
1 files changed, 315 insertions, 0 deletions
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 @@ | |||
| 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 <errno.h> | ||
| 8 | #include <fcntl.h> | ||
| 9 | #include <netinet/in.h> | ||
| 10 | #include <poll.h> | ||
| 11 | #include <string.h> | ||
| 12 | #include <sys/socket.h> | ||
| 13 | #include <unistd.h> | ||
| 14 | |||
| 15 | /* Opentracker */ | ||
| 16 | #include "ot_ip.h" | ||
| 17 | #include "ot_socket.h" | ||
| 18 | #include "test.h" | ||
| 19 | |||
| 20 | /* These cases talk to the loopback interface rather than to a mock, because | ||
| 21 | what is worth pinning down here is the behaviour of the socket options the | ||
| 22 | rest of the tracker relies on, and only the kernel can report that. */ | ||
| 23 | |||
| 24 | static const char ip_any[16] = {0}; | ||
| 25 | static const char ip_loopback[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}; | ||
| 26 | static const char ip_v4_loopback[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 127, 0, 0, 1}; | ||
| 27 | /* RFC 3849 reserves 2001:db8::/32 for documentation, so no host carries it */ | ||
| 28 | static const char ip_unassigned[16] = {0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}; | ||
| 29 | |||
| 30 | /* What a socket ended up bound to, which is not always what was asked for: a | ||
| 31 | FreeBSD jail, for one, rewrites a loopback bind to the address it was given. | ||
| 32 | A datagram a socket sends to itself must come back carrying this, so the | ||
| 33 | cases below compare against it rather than against the address they passed | ||
| 34 | to the bind. */ | ||
| 35 | static void bound_address(int s, char ip[16], uint16_t *port) { | ||
| 36 | struct sockaddr_storage ss; | ||
| 37 | socklen_t len = sizeof(ss); | ||
| 38 | |||
| 39 | memset(&ss, 0, sizeof(ss)); | ||
| 40 | memset(ip, 0, 16); | ||
| 41 | *port = 0; | ||
| 42 | if (getsockname(s, (struct sockaddr *)&ss, &len) == -1) | ||
| 43 | return; | ||
| 44 | |||
| 45 | /* Spelled out here rather than borrowed from ot_socket.c, so that a wrong | ||
| 46 | idea about how a kernel reports an address cannot hide in both at once */ | ||
| 47 | if (ss.ss_family == AF_INET) { | ||
| 48 | const struct sockaddr_in *sa = (const struct sockaddr_in *)&ss; | ||
| 49 | |||
| 50 | memcpy(ip, ip_v4mapped_prefix, sizeof(ip_v4mapped_prefix)); | ||
| 51 | memcpy(ip + sizeof(ip_v4mapped_prefix), &sa->sin_addr, 4); | ||
| 52 | *port = ntohs(sa->sin_port); | ||
| 53 | } else { | ||
| 54 | const struct sockaddr_in6 *sa = (const struct sockaddr_in6 *)&ss; | ||
| 55 | |||
| 56 | memcpy(ip, &sa->sin6_addr, 16); | ||
| 57 | *port = ntohs(sa->sin6_port); | ||
| 58 | } | ||
| 59 | } | ||
| 60 | |||
| 61 | static uint16_t bound_port(int s) { | ||
| 62 | char ip[16]; | ||
| 63 | uint16_t port; | ||
| 64 | |||
| 65 | bound_address(s, ip, &port); | ||
| 66 | return port; | ||
| 67 | } | ||
| 68 | |||
| 69 | static int is_nonblocking(int s) { | ||
| 70 | int flags = fcntl(s, F_GETFL, 0); | ||
| 71 | return flags != -1 && (flags & O_NONBLOCK) != 0; | ||
| 72 | } | ||
| 73 | |||
| 74 | static int sock_flag(int s, int level, int option) { | ||
| 75 | int value = -1; | ||
| 76 | socklen_t len = sizeof(value); | ||
| 77 | |||
| 78 | if (getsockopt(s, level, option, &value, &len) == -1) | ||
| 79 | return -1; | ||
| 80 | return value != 0; | ||
| 81 | } | ||
| 82 | |||
| 83 | /* Wait for one event, so a test never blocks forever on a lost packet */ | ||
| 84 | static int wait_for(int s, short events) { | ||
| 85 | struct pollfd pfd; | ||
| 86 | |||
| 87 | pfd.fd = s; | ||
| 88 | pfd.events = events; | ||
| 89 | pfd.revents = 0; | ||
| 90 | return poll(&pfd, 1, 2000) == 1; | ||
| 91 | } | ||
| 92 | |||
| 93 | /* ------------------------------------------------------------ invariants */ | ||
| 94 | |||
| 95 | /* Asked for the wildcard address, because a socket bound to one specific | ||
| 96 | IPv6 address cannot carry IPv4 traffic anyway and Linux turns IPV6_V6ONLY | ||
| 97 | back on in that case. The tracker's own default is the wildcard. */ | ||
| 98 | static void case_socket_options(void) { | ||
| 99 | int udp, tcp; | ||
| 100 | |||
| 101 | test_case("a bound udp socket is non blocking and serves both families"); | ||
| 102 | udp = socket_udp_bind(ip_any, 0); | ||
| 103 | CHECK(udp != -1); | ||
| 104 | CHECK_INT(is_nonblocking(udp), 1); | ||
| 105 | CHECK_INT(sock_flag(udp, IPPROTO_IPV6, IPV6_V6ONLY), 0); | ||
| 106 | close(udp); | ||
| 107 | |||
| 108 | test_case("a listening tcp socket is non blocking and serves both families"); | ||
| 109 | tcp = socket_tcp_listen(ip_any, 0); | ||
| 110 | CHECK(tcp != -1); | ||
| 111 | CHECK_INT(is_nonblocking(tcp), 1); | ||
| 112 | CHECK_INT(sock_flag(tcp, IPPROTO_IPV6, IPV6_V6ONLY), 0); | ||
| 113 | close(tcp); | ||
| 114 | } | ||
| 115 | |||
| 116 | #ifdef SO_REUSEPORT | ||
| 117 | static void case_reuseport_is_opt_in(void) { | ||
| 118 | int s; | ||
| 119 | |||
| 120 | test_case("reuseport stays off unless the operator asks for it"); | ||
| 121 | g_socket_reuseport = 0; | ||
| 122 | s = socket_udp_bind(ip_loopback, 0); | ||
| 123 | CHECK(s != -1); | ||
| 124 | CHECK_INT(sock_flag(s, SOL_SOCKET, SO_REUSEPORT), 0); | ||
| 125 | close(s); | ||
| 126 | |||
| 127 | test_case("reuseport reaches the socket when it is asked for"); | ||
| 128 | g_socket_reuseport = 1; | ||
| 129 | s = socket_udp_bind(ip_loopback, 0); | ||
| 130 | CHECK(s != -1); | ||
| 131 | CHECK_INT(sock_flag(s, SOL_SOCKET, SO_REUSEPORT), 1); | ||
| 132 | close(s); | ||
| 133 | |||
| 134 | g_socket_reuseport = 0; | ||
| 135 | } | ||
| 136 | #endif | ||
| 137 | |||
| 138 | /* ------------------------------------------------------------------- udp */ | ||
| 139 | |||
| 140 | static void case_udp_roundtrip(void) { | ||
| 141 | char buf[32], ip[16], own_ip[16]; | ||
| 142 | uint16_t port = 0, own_port = 0; | ||
| 143 | int s; | ||
| 144 | |||
| 145 | test_case("a datagram comes back with the sender's address and port"); | ||
| 146 | s = socket_udp_bind(ip_loopback, 0); | ||
| 147 | CHECK(s != -1); | ||
| 148 | bound_address(s, own_ip, &own_port); | ||
| 149 | CHECK(own_port != 0); | ||
| 150 | CHECK_INT(socket_send(s, "opentracker", 11, own_ip, own_port), 11); | ||
| 151 | CHECK_INT(wait_for(s, POLLIN), 1); | ||
| 152 | CHECK_INT(socket_recv(s, buf, sizeof(buf), ip, &port), 11); | ||
| 153 | CHECK_MEM(buf, "opentracker", 11); | ||
| 154 | CHECK_MEM(ip, own_ip, 16); | ||
| 155 | CHECK_INT(port, own_port); | ||
| 156 | close(s); | ||
| 157 | } | ||
| 158 | |||
| 159 | /* An IPv4 peer must arrive in the v4 mapped form opentracker stores, however | ||
| 160 | the kernel chose to report it. Linux fills a v4 mapped sockaddr_in6 here | ||
| 161 | and the BSDs fill a plain sockaddr_in, so this is the case that tells the | ||
| 162 | two apart. */ | ||
| 163 | static void case_udp_v4mapped(void) { | ||
| 164 | char buf[32], ip[16], receiver_ip[16], sender_ip[16]; | ||
| 165 | uint16_t port = 0, receiver_port = 0, sender_port = 0; | ||
| 166 | int receiver, sender; | ||
| 167 | |||
| 168 | test_case("an IPv4 sender is reported in its v4 mapped form"); | ||
| 169 | receiver = socket_udp_bind(ip_v4_loopback, 0); | ||
| 170 | CHECK(receiver != -1); | ||
| 171 | sender = socket_udp_bind(ip_v4_loopback, 0); | ||
| 172 | CHECK(sender != -1); | ||
| 173 | bound_address(receiver, receiver_ip, &receiver_port); | ||
| 174 | bound_address(sender, sender_ip, &sender_port); | ||
| 175 | CHECK_INT(ip_is_v4mapped(sender_ip), 1); | ||
| 176 | |||
| 177 | CHECK_INT(socket_send(sender, "v4", 2, receiver_ip, receiver_port), 2); | ||
| 178 | CHECK_INT(wait_for(receiver, POLLIN), 1); | ||
| 179 | CHECK_INT(socket_recv(receiver, buf, sizeof(buf), ip, &port), 2); | ||
| 180 | CHECK_INT(ip_is_v4mapped(ip), 1); | ||
| 181 | CHECK_MEM(ip, sender_ip, 16); | ||
| 182 | CHECK_INT(port, sender_port); | ||
| 183 | close(sender); | ||
| 184 | close(receiver); | ||
| 185 | } | ||
| 186 | |||
| 187 | /* ------------------------------------------------------------------- tcp */ | ||
| 188 | |||
| 189 | static void case_tcp_roundtrip(void) { | ||
| 190 | char ip[16], listener_ip[16], client_ip[16]; | ||
| 191 | uint16_t port = 0, listener_port = 0, client_port = 0; | ||
| 192 | int listener, client, accepted; | ||
| 193 | |||
| 194 | test_case("a connection is accepted and carries the caller's address"); | ||
| 195 | listener = socket_tcp_listen(ip_loopback, 0); | ||
| 196 | CHECK(listener != -1); | ||
| 197 | bound_address(listener, listener_ip, &listener_port); | ||
| 198 | CHECK(listener_port != 0); | ||
| 199 | |||
| 200 | client = socket_tcp_connect(ip_loopback, 0, listener_ip, listener_port); | ||
| 201 | CHECK(client != -1); | ||
| 202 | CHECK_INT(wait_for(client, POLLOUT), 1); | ||
| 203 | CHECK_INT(socket_is_connected(client), 1); | ||
| 204 | bound_address(client, client_ip, &client_port); | ||
| 205 | |||
| 206 | CHECK_INT(wait_for(listener, POLLIN), 1); | ||
| 207 | accepted = socket_accept(listener, ip, &port); | ||
| 208 | CHECK(accepted != -1); | ||
| 209 | CHECK_MEM(ip, client_ip, 16); | ||
| 210 | CHECK_INT(port, client_port); | ||
| 211 | |||
| 212 | test_case("the accepted socket is handed over non blocking"); | ||
| 213 | CHECK_INT(is_nonblocking(accepted), 1); | ||
| 214 | |||
| 215 | close(accepted); | ||
| 216 | close(client); | ||
| 217 | close(listener); | ||
| 218 | } | ||
| 219 | |||
| 220 | static void case_connect_refused(void) { | ||
| 221 | char listener_ip[16]; | ||
| 222 | int listener, client; | ||
| 223 | uint16_t port; | ||
| 224 | |||
| 225 | test_case("a refused connection is reported by socket_is_connected"); | ||
| 226 | /* Take a port, then drop it, so nothing is listening on it any more */ | ||
| 227 | listener = socket_tcp_listen(ip_loopback, 0); | ||
| 228 | CHECK(listener != -1); | ||
| 229 | bound_address(listener, listener_ip, &port); | ||
| 230 | close(listener); | ||
| 231 | |||
| 232 | client = socket_tcp_connect(ip_loopback, 0, listener_ip, port); | ||
| 233 | CHECK(client != -1); | ||
| 234 | CHECK_INT(wait_for(client, POLLOUT), 1); | ||
| 235 | CHECK_INT(socket_is_connected(client), 0); | ||
| 236 | CHECK_INT(errno, ECONNREFUSED); | ||
| 237 | close(client); | ||
| 238 | } | ||
| 239 | |||
| 240 | /* --------------------------------------------------------- failed binds */ | ||
| 241 | |||
| 242 | /* The lowest free descriptor is handed out first, so a number that stays put | ||
| 243 | across a failing call shows the call did not keep a socket behind. */ | ||
| 244 | static int next_descriptor(void) { | ||
| 245 | int fd = dup(0); | ||
| 246 | |||
| 247 | if (fd != -1) | ||
| 248 | close(fd); | ||
| 249 | return fd; | ||
| 250 | } | ||
| 251 | |||
| 252 | static void case_bind_failure(void) { | ||
| 253 | int before; | ||
| 254 | |||
| 255 | test_case("binding an address no interface carries fails"); | ||
| 256 | before = next_descriptor(); | ||
| 257 | CHECK(before != -1); | ||
| 258 | CHECK_INT(socket_tcp_listen(ip_unassigned, 0), -1); | ||
| 259 | CHECK_INT(socket_udp_bind(ip_unassigned, 0), -1); | ||
| 260 | |||
| 261 | test_case("a failed bind leaves no socket behind"); | ||
| 262 | CHECK_INT(next_descriptor(), before); | ||
| 263 | } | ||
| 264 | |||
| 265 | #ifdef WANT_SYNC_LIVE | ||
| 266 | /* Reading an IPv4 multicast option back: Linux answers with an int, the BSDs | ||
| 267 | with a single byte, so ask how much was written. */ | ||
| 268 | static int mcast_option(int s, int option) { | ||
| 269 | int value = 0; | ||
| 270 | socklen_t len = sizeof(value); | ||
| 271 | |||
| 272 | if (getsockopt(s, IPPROTO_IP, option, &value, &len) == -1) | ||
| 273 | return -1; | ||
| 274 | if (len == 1) | ||
| 275 | return *(unsigned char *)&value; | ||
| 276 | return value; | ||
| 277 | } | ||
| 278 | |||
| 279 | static void case_mcast_sender(void) { | ||
| 280 | int s; | ||
| 281 | |||
| 282 | test_case("the sending socket keeps its traffic on the local network"); | ||
| 283 | s = socket_mcast_bind(ip_v4_loopback, 0, 0); | ||
| 284 | CHECK(s != -1); | ||
| 285 | CHECK_INT(mcast_option(s, IP_MULTICAST_TTL), 1); | ||
| 286 | |||
| 287 | test_case("and does not hear itself unless it is asked to"); | ||
| 288 | CHECK_INT(mcast_option(s, IP_MULTICAST_LOOP), 0); | ||
| 289 | |||
| 290 | test_case("whoever sends from it is serving clients too, so it cannot block"); | ||
| 291 | CHECK_INT(is_nonblocking(s), 1); | ||
| 292 | close(s); | ||
| 293 | |||
| 294 | test_case("the proxy asks to hear itself and gets it"); | ||
| 295 | s = socket_mcast_bind(ip_v4_loopback, 0, 1); | ||
| 296 | CHECK(s != -1); | ||
| 297 | CHECK_INT(mcast_option(s, IP_MULTICAST_LOOP), 1); | ||
| 298 | close(s); | ||
| 299 | } | ||
| 300 | #endif | ||
| 301 | |||
| 302 | void test_socket(void) { | ||
| 303 | case_socket_options(); | ||
| 304 | #ifdef SO_REUSEPORT | ||
| 305 | case_reuseport_is_opt_in(); | ||
| 306 | #endif | ||
| 307 | case_udp_roundtrip(); | ||
| 308 | case_udp_v4mapped(); | ||
| 309 | case_tcp_roundtrip(); | ||
| 310 | case_connect_refused(); | ||
| 311 | case_bind_failure(); | ||
| 312 | #ifdef WANT_SYNC_LIVE | ||
| 313 | case_mcast_sender(); | ||
| 314 | #endif | ||
| 315 | } | ||
