From 8eeb661bfd5450db693465c890d7cc208206e3c2 Mon Sep 17 00:00:00 2001 From: Dennis Braun Date: Sat, 25 Apr 2026 23:01:09 +0200 Subject: [PATCH] fix: support IPv6 connection monitor traceroute --- .../test_traceroute_probe.py | 229 ++++++++++++ tools/traceroute_helper.c | 341 ++++++++++++++---- 2 files changed, 490 insertions(+), 80 deletions(-) diff --git a/tests/modules/connection_monitor/test_traceroute_probe.py b/tests/modules/connection_monitor/test_traceroute_probe.py index 74203171..49f9e5f1 100644 --- a/tests/modules/connection_monitor/test_traceroute_probe.py +++ b/tests/modules/connection_monitor/test_traceroute_probe.py @@ -2,6 +2,7 @@ import hashlib import subprocess +from pathlib import Path from unittest.mock import patch, MagicMock import pytest @@ -238,3 +239,231 @@ def test_reverse_dns_timeout(self): assert result.hops[0].hop_host is None assert result.hops[1].hop_host is None + + +class TestIPv6Support: + """Issue #363 — traceroute must accept IPv6 hop output and IPv6 targets.""" + + def test_parse_ipv6_hops(self): + """IPv6 textual addresses in hop lines must round-trip through the parser.""" + stdout = ( + "1\t2001:db8::1\t1.23\t3\n" + "2\t*\t-1\t0\n" + "3\t2606:4700:4700::1111\t12.34\t3\n" + ) + completed = subprocess.CompletedProcess( + args=[HELPER_PATH, "2606:4700:4700::1111", "30", "2000"], + returncode=0, + stdout=stdout, + stderr="", + ) + probe = TracerouteProbe() + with patch(f"{MODULE}.subprocess.run", return_value=completed), \ + patch(f"{MODULE}.socket.gethostbyaddr", side_effect=OSError): + result = probe.run("2606:4700:4700::1111") + + assert result.reached_target is True + assert len(result.hops) == 3 + assert result.hops[0].hop_ip == "2001:db8::1" + assert result.hops[0].latency_ms == 1.23 + assert result.hops[1].hop_ip is None + assert result.hops[2].hop_ip == "2606:4700:4700::1111" + assert result.hops[2].latency_ms == 12.34 + + def test_route_fingerprint_with_ipv6(self): + """SHA256 fingerprint includes IPv6 textual addresses unchanged.""" + stdout = ( + "1\t2001:db8::1\t1.23\t3\n" + "2\t*\t-1\t0\n" + "3\t2606:4700:4700::1111\t12.34\t3\n" + ) + completed = subprocess.CompletedProcess( + args=[HELPER_PATH, "2606:4700:4700::1111", "30", "2000"], + returncode=0, + stdout=stdout, + stderr="", + ) + expected = hashlib.sha256( + "2001:db8::1|*|2606:4700:4700::1111".encode() + ).hexdigest() + probe = TracerouteProbe() + with patch(f"{MODULE}.subprocess.run", return_value=completed), \ + patch(f"{MODULE}.socket.gethostbyaddr", side_effect=OSError): + result = probe.run("2606:4700:4700::1111") + + assert result.route_fingerprint == expected + + def test_passes_ipv6_literal_to_helper_unchanged(self): + """The helper must receive the IPv6 literal verbatim as argv[1].""" + completed = subprocess.CompletedProcess( + args=[HELPER_PATH, "2606:4700:4700::1111", "30", "2000"], + returncode=0, + stdout="1\t2606:4700:4700::1111\t5.00\t3\n", + stderr="", + ) + probe = TracerouteProbe() + with patch(f"{MODULE}.subprocess.run", return_value=completed) as mock_run, \ + patch(f"{MODULE}.socket.gethostbyaddr", side_effect=OSError): + probe.run("2606:4700:4700::1111") + invoked = mock_run.call_args.args[0] + assert invoked[0] == HELPER_PATH + assert invoked[1] == "2606:4700:4700::1111" + + +def _strip_c_comments(src: str) -> str: + """Remove C block and line comments from `src`. + + Structural order assertions on C source must look at code only — words + inside comments would otherwise satisfy the regex and mask reordering + bugs (or, as here, falsely report them). + """ + import re + + # Block comments are stripped first so that "//" inside a /* ... */ + # cannot be mistaken for a line comment, and "/*" inside a // line + # cannot swallow code on the next line. + src = re.sub(r"/\*.*?\*/", " ", src, flags=re.DOTALL) + src = re.sub(r"//[^\n]*", " ", src) + return src + + +class TestHelperSource: + """Issue #363 regression coverage: structural assertions on the C source. + + The C helper requires raw-socket privileges, so we cannot exercise it + end-to-end in CI. These tests catch the IPv4-only regression structurally + (mirroring the icmp_probe_helper.c gates in test_probe.py) and prove the + helper still compiles cleanly with strict warnings.""" + + HELPER_SRC = ( + Path(__file__).resolve().parents[3] / "tools" / "traceroute_helper.c" + ) + + def test_helper_source_handles_ipv6(self): + """Source must reference the AF_INET6 / ICMPv6 surface, not just IPv4.""" + src = self.HELPER_SRC.read_text() + assert "AF_UNSPEC" in src, "helper must resolve with AF_UNSPEC, not AF_INET" + assert "AF_INET6" in src, "helper must open AF_INET6 sockets" + assert "IPPROTO_ICMPV6" in src, "helper must speak IPPROTO_ICMPV6" + assert "IPV6_UNICAST_HOPS" in src, ( + "helper must set the IPv6 hop limit (IPV6_UNICAST_HOPS), " + "not only the IPv4 IP_TTL" + ) + assert "INET6_ADDRSTRLEN" in src, ( + "helper must size address buffers for IPv6 (INET6_ADDRSTRLEN), " + "not the IPv4-only INET_ADDRSTRLEN" + ) + assert ( + "ICMP6_ECHO_REQUEST" in src or "128" in src + ), "helper must send ICMPv6 echo request (type 128)" + assert ( + "ICMP6_ECHO_REPLY" in src or "129" in src + ), "helper must parse ICMPv6 echo reply (type 129)" + assert "ICMP6_TIME_EXCEEDED" in src, ( + "helper must classify ICMPv6 Time Exceeded for hop discovery" + ) + assert "inet_ntop(AF_INET6" in src, ( + "helper must format IPv6 hop addresses with inet_ntop(AF_INET6, ...)" + ) + + def test_helper_source_keeps_ipv4_path(self): + """IPv4 behavior must be preserved alongside the new IPv6 path.""" + src = self.HELPER_SRC.read_text() + assert "IPPROTO_ICMP" in src + assert "IP_TTL" in src + assert "ICMP_ECHO" in src + assert "ICMP_TIME_EXCEEDED" in src + assert "inet_ntop(AF_INET" in src + + def test_helper_compiles_with_strict_warnings(self, tmp_path): + """Helper builds cleanly with -O2 -Wall -Werror.""" + binary = tmp_path / "docsight-traceroute-helper" + compile_result = subprocess.run( + ["gcc", "-O2", "-Wall", "-Werror", + "-o", str(binary), str(self.HELPER_SRC)], + capture_output=True, + text=True, + ) + assert compile_result.returncode == 0, compile_result.stderr + assert binary.exists() + + def test_helper_drops_privileges_before_resolution(self): + """Regression: NSS/DNS must run only after seteuid(getuid()). + + The setuid helper model requires raw-socket creation while euid + is elevated, then an immediate privilege drop, and only then DNS + resolution. A previous IPv6 patch reordered resolve_host() to + run before the socket+seteuid sequence, which would invoke NSS + modules as root. Inside main(), this test asserts the textual + order open_icmp_socket(...) -> seteuid(...) -> resolve_host(...). + """ + import re + + src = _strip_c_comments(self.HELPER_SRC.read_text()) + + main_match = re.search(r"\bint\s+main\s*\(", src) + assert main_match, "helper must define main()" + main_body = src[main_match.start():] + + open_socket_calls = [ + m.start() for m in re.finditer(r"\bopen_icmp_socket\s*\(", main_body) + ] + seteuid_calls = [ + m.start() + for m in re.finditer(r"\bseteuid\s*\(\s*getuid\s*\(", main_body) + ] + resolve_calls = [ + m.start() for m in re.finditer(r"\bresolve_host\s*\(", main_body) + ] + + assert open_socket_calls, "main() must open raw sockets" + assert seteuid_calls, ( + "main() must drop privileges with seteuid(getuid())" + ) + assert resolve_calls, "main() must resolve the host" + + earliest_open = min(open_socket_calls) + earliest_seteuid = min(seteuid_calls) + earliest_resolve = min(resolve_calls) + + assert earliest_open < earliest_seteuid, ( + "Raw sockets must be opened BEFORE seteuid(getuid()); they " + "require elevated privileges to create." + ) + assert earliest_seteuid < earliest_resolve, ( + "seteuid(getuid()) must run BEFORE resolve_host(); NSS/DNS " + "code in a setuid helper must execute only after the " + "privilege drop." + ) + + def test_helper_opens_both_families_before_dropping_privileges(self): + """Both AF_INET and AF_INET6 raw sockets must be opened up front. + + Because the resolver runs after the privilege drop, the family + the resolver will choose is not known while euid is still + elevated. The helper therefore must open both family sockets + before seteuid(getuid()), and select between them after + resolve_host() returns. + """ + import re + + src = _strip_c_comments(self.HELPER_SRC.read_text()) + + main_match = re.search(r"\bint\s+main\s*\(", src) + assert main_match, "helper must define main()" + main_body = src[main_match.start():] + + seteuid_match = re.search( + r"\bseteuid\s*\(\s*getuid\s*\(", main_body + ) + assert seteuid_match, "main() must drop privileges" + before_drop = main_body[: seteuid_match.start()] + + assert re.search(r"open_icmp_socket\s*\(\s*AF_INET\s*\)", before_drop), ( + "main() must open the AF_INET raw socket before " + "seteuid(getuid())" + ) + assert re.search(r"open_icmp_socket\s*\(\s*AF_INET6\s*\)", before_drop), ( + "main() must open the AF_INET6 raw socket before " + "seteuid(getuid())" + ) diff --git a/tools/traceroute_helper.c b/tools/traceroute_helper.c index 51349749..6d4f60c2 100644 --- a/tools/traceroute_helper.c +++ b/tools/traceroute_helper.c @@ -9,11 +9,20 @@ * hop_index\t*\t-1\t0 * * Exit: 0 = target reached, 1 = max hops exceeded, 2 = error + * + * Dual-stack: resolves with AF_UNSPEC and traces over ICMPv4 or ICMPv6 + * depending on the resolved address family. The output format does not + * encode the family — callers (the Python wrapper) treat hop_ip as an + * opaque string and the v6 textual form fits because tab is the separator. */ #include #include #include +#include +#include +#include +#include #include #include #include @@ -45,19 +54,30 @@ static unsigned short icmp_checksum(const void *buf, int len) { return (unsigned short)(~sum); } -static int open_icmp_socket(void) { +static int open_icmp_socket(int family) { + if (family == AF_INET6) { + return socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6); + } return socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); } -static int resolve_ipv4(const char *host, struct sockaddr_in *addr) { +/* Resolve host with AF_UNSPEC and return the first usable AF_INET/AF_INET6 + * entry. Caller must freeaddrinfo(*out_list); the returned chosen pointer + * aliases into that list. Single-target traceroute does not need TCP-style + * cross-family fallback — the user asked to trace to a specific host, so we + * commit to one resolved family in resolver order (matches what TCP/UDP + * would do for the same hostname). */ +static int resolve_host(const char *host, + struct addrinfo **out_list, + struct addrinfo **out_chosen) { struct addrinfo hints; struct addrinfo *result = NULL; int rc; memset(&hints, 0, sizeof(hints)); - hints.ai_family = AF_INET; + hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_RAW; - hints.ai_protocol = IPPROTO_ICMP; + /* No service: SOCK_RAW + numeric service trips EAI_SERVICE on glibc. */ rc = getaddrinfo(host, NULL, &hints, &result); if (rc != 0) { @@ -65,44 +85,176 @@ static int resolve_ipv4(const char *host, struct sockaddr_in *addr) { return -1; } - memcpy(addr, result->ai_addr, sizeof(*addr)); - freeaddrinfo(result); + struct addrinfo *chosen = NULL; + for (struct addrinfo *ai = result; ai != NULL; ai = ai->ai_next) { + if (ai->ai_family == AF_INET || ai->ai_family == AF_INET6) { + chosen = ai; + break; + } + } + if (chosen == NULL) { + freeaddrinfo(result); + fprintf(stderr, "no usable address\n"); + return -1; + } + + *out_list = result; + *out_chosen = chosen; return 0; } -/* Send one ICMP Echo Request with given TTL, wait for reply. - * Returns latency in ms (>= 0) on response, -1 on timeout. - * Sets *resp_ip to the responding hop address. Sets *reached if target replied. */ -static double send_probe(int sock, const struct sockaddr_in *dest, +static int set_hop_limit(int sock, int family, int ttl) { + if (family == AF_INET6) { + return setsockopt(sock, IPPROTO_IPV6, IPV6_UNICAST_HOPS, + &ttl, sizeof(ttl)); + } + return setsockopt(sock, IPPROTO_IP, IP_TTL, &ttl, sizeof(ttl)); +} + +static int build_echo_packet(int family, unsigned short ident, + unsigned short seq, unsigned char *packet) { + if (family == AF_INET6) { + size_t len = sizeof(struct icmp6_hdr) + PAYLOAD_SIZE; + memset(packet, 0, len); + struct icmp6_hdr *hdr = (struct icmp6_hdr *)packet; + hdr->icmp6_type = ICMP6_ECHO_REQUEST; + hdr->icmp6_code = 0; + hdr->icmp6_id = htons(ident); + hdr->icmp6_seq = htons(seq); + /* Kernel fills the ICMPv6 checksum on IPPROTO_ICMPV6 raw sockets. */ + hdr->icmp6_cksum = 0; + return (int)len; + } + size_t len = sizeof(struct icmphdr) + PAYLOAD_SIZE; + memset(packet, 0, len); + struct icmphdr *hdr = (struct icmphdr *)packet; + hdr->type = ICMP_ECHO; + hdr->code = 0; + hdr->un.echo.id = htons(ident); + hdr->un.echo.sequence = htons(seq); + hdr->checksum = icmp_checksum(packet, (int)len); + return (int)len; +} + +/* Returns 1 when buf carries our hop response (ident+seq match): + * - on Echo Reply: *reached = 1 + * - on Time Exceeded (TTL): *reached = 0 + * Returns 0 when buf is unrelated (keep waiting). + * + * IPv4 (IPPROTO_ICMP raw) delivers the full IP datagram, so the outer IP + * header must be skipped using ip_hl. The Time-Exceeded payload embeds the + * original packet: outer-IP + outer-ICMP(8) + inner-IP + inner-ICMP(8). */ +static int classify_reply4(const unsigned char *buf, ssize_t n, + unsigned short ident, unsigned short seq, + int *reached) { + *reached = 0; + if (n < (ssize_t)(sizeof(struct ip) + sizeof(struct icmphdr))) { + return 0; + } + const struct ip *ip4 = (const struct ip *)buf; + size_t ip_hlen = (size_t)ip4->ip_hl * 4; + if (ip_hlen < sizeof(struct ip)) ip_hlen = sizeof(struct ip); + if ((size_t)n < ip_hlen + sizeof(struct icmphdr)) { + return 0; + } + const struct icmphdr *outer = (const struct icmphdr *)(buf + ip_hlen); + + if (outer->type == ICMP_ECHOREPLY + && outer->un.echo.id == htons(ident) + && outer->un.echo.sequence == htons(seq)) { + *reached = 1; + return 1; + } + + if (outer->type == ICMP_TIME_EXCEEDED && outer->code == ICMP_EXC_TTL) { + size_t need = ip_hlen + sizeof(struct icmphdr) + + sizeof(struct ip) + sizeof(struct icmphdr); + if ((size_t)n < need) return 0; + const struct icmphdr *inner = (const struct icmphdr *) + (buf + ip_hlen + sizeof(struct icmphdr) + sizeof(struct ip)); + if (inner->un.echo.id == htons(ident) + && inner->un.echo.sequence == htons(seq)) { + return 1; + } + } + return 0; +} + +/* IPv6 (IPPROTO_ICMPV6 raw) delivers only the ICMPv6 message — there is + * no outer IPv6 header in the buffer. The Time-Exceeded payload embeds + * the original packet: outer-ICMPv6(8) + inner-IPv6(40) + inner-ICMPv6(8). */ +static int classify_reply6(const unsigned char *buf, ssize_t n, + unsigned short ident, unsigned short seq, + int *reached) { + *reached = 0; + if (n < (ssize_t)sizeof(struct icmp6_hdr)) { + return 0; + } + const struct icmp6_hdr *outer = (const struct icmp6_hdr *)buf; + + if (outer->icmp6_type == ICMP6_ECHO_REPLY + && outer->icmp6_id == htons(ident) + && outer->icmp6_seq == htons(seq)) { + *reached = 1; + return 1; + } + + if (outer->icmp6_type == ICMP6_TIME_EXCEEDED) { + size_t need = sizeof(struct icmp6_hdr) + sizeof(struct ip6_hdr) + + sizeof(struct icmp6_hdr); + if ((size_t)n < need) return 0; + const struct icmp6_hdr *inner = (const struct icmp6_hdr *) + (buf + sizeof(struct icmp6_hdr) + sizeof(struct ip6_hdr)); + if (inner->icmp6_id == htons(ident) + && inner->icmp6_seq == htons(seq)) { + return 1; + } + } + return 0; +} + +static void format_addr(int family, const struct sockaddr_storage *from, + char *out, size_t outlen) { + if (family == AF_INET6) { + const struct sockaddr_in6 *s6 = (const struct sockaddr_in6 *)from; + inet_ntop(AF_INET6, &s6->sin6_addr, out, (socklen_t)outlen); + } else { + const struct sockaddr_in *s4 = (const struct sockaddr_in *)from; + inet_ntop(AF_INET, &s4->sin_addr, out, (socklen_t)outlen); + } +} + +/* Send one ICMP/ICMPv6 Echo Request with given TTL, wait for a reply. + * Returns latency in ms (>= 0) on response, -1 on timeout/error. + * Sets *resp_ip to the responding hop address. Sets *reached if the target + * itself replied. */ +static double send_probe(int sock, int family, + const struct sockaddr *dest, socklen_t dest_len, unsigned short ident, unsigned short seq, int ttl, int timeout_ms, char *resp_ip, size_t resp_ip_len, int *reached) { - unsigned char packet[sizeof(struct icmphdr) + PAYLOAD_SIZE]; - struct icmphdr *hdr = (struct icmphdr *)packet; - struct timeval start, now; + /* struct icmp6_hdr and struct icmphdr are both 8 bytes; one buffer fits + * either family with the same payload size. */ + unsigned char packet[sizeof(struct icmp6_hdr) + PAYLOAD_SIZE]; + struct timeval start; *reached = 0; resp_ip[0] = '\0'; - if (setsockopt(sock, IPPROTO_IP, IP_TTL, &ttl, sizeof(ttl)) < 0) { + if (set_hop_limit(sock, family, ttl) < 0) { return -1; } - memset(packet, 0, sizeof(packet)); - hdr->type = ICMP_ECHO; - hdr->code = 0; - hdr->un.echo.id = htons(ident); - hdr->un.echo.sequence = htons(seq); - hdr->checksum = icmp_checksum(packet, sizeof(packet)); + int packet_len = build_echo_packet(family, ident, seq, packet); if (gettimeofday(&start, NULL) != 0) return -1; - if (sendto(sock, packet, sizeof(packet), 0, - (const struct sockaddr *)dest, sizeof(*dest)) < 0) { + if (sendto(sock, packet, (size_t)packet_len, 0, dest, dest_len) < 0) { return -1; } for (;;) { + struct timeval now; if (gettimeofday(&now, NULL) != 0) return -1; long elapsed_ms = (now.tv_sec - start.tv_sec) * 1000L @@ -125,8 +277,8 @@ static double send_probe(int sock, const struct sockaddr_in *dest, } if (ready == 0) return -1; - unsigned char buf[1024]; - struct sockaddr_in from; + unsigned char buf[1500]; + struct sockaddr_storage from; socklen_t fromlen = sizeof(from); ssize_t n = recvfrom(sock, buf, sizeof(buf), 0, (struct sockaddr *)&from, &fromlen); @@ -134,55 +286,45 @@ static double send_probe(int sock, const struct sockaddr_in *dest, if (errno == EINTR) continue; return -1; } - if (n < (ssize_t)(20 + sizeof(struct icmphdr))) continue; - - struct icmphdr *reply = (struct icmphdr *)(buf + 20); - - if (reply->type == ICMP_ECHOREPLY - && reply->un.echo.id == htons(ident) - && reply->un.echo.sequence == htons(seq)) { - struct timeval end; - if (gettimeofday(&end, NULL) != 0) return -1; - double ms = (double)(end.tv_sec - start.tv_sec) * 1000.0 - + (double)(end.tv_usec - start.tv_usec) / 1000.0; - inet_ntop(AF_INET, &from.sin_addr, resp_ip, resp_ip_len); - *reached = 1; - return ms; - } - if (reply->type == ICMP_TIME_EXCEEDED && reply->code == ICMP_EXC_TTL) { - /* Embedded original packet: outer IP(20) + ICMP TTL_EXCEEDED(8) + inner IP(20) + inner ICMP(8) */ - if (n < (ssize_t)(20 + 8 + 20 + 8)) continue; - struct icmphdr *inner = (struct icmphdr *)(buf + 20 + 8 + 20); - if (inner->un.echo.id == htons(ident) - && inner->un.echo.sequence == htons(seq)) { - struct timeval end; - if (gettimeofday(&end, NULL) != 0) return -1; - double ms = (double)(end.tv_sec - start.tv_sec) * 1000.0 - + (double)(end.tv_usec - start.tv_usec) / 1000.0; - inet_ntop(AF_INET, &from.sin_addr, resp_ip, resp_ip_len); - return ms; - } + int matched = (family == AF_INET6) + ? classify_reply6(buf, n, ident, seq, reached) + : classify_reply4(buf, n, ident, seq, reached); + if (!matched) { + continue; } - /* Not our packet, keep waiting */ + + struct timeval end; + if (gettimeofday(&end, NULL) != 0) return -1; + double ms = (double)(end.tv_sec - start.tv_sec) * 1000.0 + + (double)(end.tv_usec - start.tv_usec) / 1000.0; + format_addr(family, &from, resp_ip, resp_ip_len); + return ms; + } +} + +static int run_check(void) { + int sock4 = open_icmp_socket(AF_INET); + int sock6 = open_icmp_socket(AF_INET6); + if (sock4 < 0 && sock6 < 0) { + perror("socket"); + return 2; + } + if (seteuid(getuid()) != 0) { + perror("seteuid"); + if (sock4 >= 0) close(sock4); + if (sock6 >= 0) close(sock6); + return 2; } + if (sock4 >= 0) close(sock4); + if (sock6 >= 0) close(sock6); + puts("ok"); + return 0; } int main(int argc, char **argv) { if (argc >= 2 && strcmp(argv[1], "--check") == 0) { - int sock = open_icmp_socket(); - if (sock < 0) { - perror("socket"); - return 2; - } - if (seteuid(getuid()) != 0) { - perror("seteuid"); - close(sock); - return 2; - } - close(sock); - puts("ok"); - return 0; + return run_check(); } if (argc < 2 || argc > 4) { @@ -204,27 +346,65 @@ int main(int argc, char **argv) { return 2; } - /* Open raw socket (requires CAP_NET_RAW or setuid root) */ - int sock = open_icmp_socket(); - if (sock < 0) { + /* Open raw sockets BEFORE name resolution. The setuid helper model + * forbids running NSS/DNS code while euid is root, so raw-socket + * creation must consume the elevated privilege first. We open both + * families up front because the family the resolver will choose is + * not known until after seteuid(getuid()) has run. Failure is only + * reported when neither family is available. */ + int sock4 = open_icmp_socket(AF_INET); + int sock6 = open_icmp_socket(AF_INET6); + if (sock4 < 0 && sock6 < 0) { perror("socket"); return 2; } - /* Immediately drop privileges */ + /* Drop privileges immediately, before any name resolution runs. */ if (seteuid(getuid()) != 0) { perror("seteuid"); - close(sock); + if (sock4 >= 0) close(sock4); + if (sock6 >= 0) close(sock6); return 2; } - struct sockaddr_in dest; - memset(&dest, 0, sizeof(dest)); - if (resolve_ipv4(host, &dest) != 0) { - close(sock); + /* Now safe to invoke NSS/DNS resolution (AF_UNSPEC). */ + struct addrinfo *ai_list = NULL; + struct addrinfo *chosen = NULL; + if (resolve_host(host, &ai_list, &chosen) != 0) { + if (sock4 >= 0) close(sock4); + if (sock6 >= 0) close(sock6); return 2; } + int family = chosen->ai_family; + int sock; + if (family == AF_INET6) { + if (sock6 < 0) { + fprintf(stderr, "no IPv6 raw socket available\n"); + if (sock4 >= 0) close(sock4); + freeaddrinfo(ai_list); + return 2; + } + sock = sock6; + if (sock4 >= 0) close(sock4); + } else { + if (sock4 < 0) { + fprintf(stderr, "no IPv4 raw socket available\n"); + if (sock6 >= 0) close(sock6); + freeaddrinfo(ai_list); + return 2; + } + sock = sock4; + if (sock6 >= 0) close(sock6); + } + + /* Copy destination off the addrinfo so we can free the list before + * the per-hop loop. */ + struct sockaddr_storage dest; + socklen_t dest_len = (socklen_t)chosen->ai_addrlen; + memcpy(&dest, chosen->ai_addr, chosen->ai_addrlen); + freeaddrinfo(ai_list); + unsigned short ident = (unsigned short)(getpid() & 0xFFFF); unsigned short seq = 0; int target_reached = 0; @@ -232,21 +412,22 @@ int main(int argc, char **argv) { for (int hop = 1; hop <= max_hops; hop++) { double best_ms = -1; int responded = 0; - char hop_ip[INET_ADDRSTRLEN]; + char hop_ip[INET6_ADDRSTRLEN]; hop_ip[0] = '\0'; for (int p = 0; p < PROBES_PER_HOP; p++) { seq++; - char probe_ip[INET_ADDRSTRLEN]; + char probe_ip[INET6_ADDRSTRLEN]; int reached = 0; - double ms = send_probe(sock, &dest, ident, seq, - hop, timeout_ms, + double ms = send_probe(sock, family, + (const struct sockaddr *)&dest, dest_len, + ident, seq, hop, timeout_ms, probe_ip, sizeof(probe_ip), &reached); if (ms >= 0) { responded++; if (best_ms < 0 || ms < best_ms) { best_ms = ms; - /* Use the IP from the first successful probe */ + /* Use the IP from the first successful probe. */ if (hop_ip[0] == '\0') { memcpy(hop_ip, probe_ip, sizeof(hop_ip)); }