From e41dade9ac3bbd7ffbc1eec826801af1a38d8b9e Mon Sep 17 00:00:00 2001 From: srdusr <99972264+srdusr@users.noreply.github.com> Date: Thu, 28 May 2026 01:23:00 +0200 Subject: Decode ICMP embedded flows - show which packet actually triggered an error Destination Unreachable, Time Exceeded, Redirect, Source Quench, and Parameter Problem (plus their ICMPv6 equivalents) all carry, after their own fixed header, as much of the packet that triggered the error as the network could fit - always at least its IP header plus the first 8 bytes of payload, exactly enough to recover TCP/UDP port numbers. That embedded flow is the actual reason an ICMP error shows up in a capture at all, and wasn't shown before this. Reuses parse_ipv4()/parse_ipv6() directly on the embedded bytes rather than a separate parser - it's a genuine (if truncated) IP packet, not a different format. Two small is_error_type() helpers gate which ICMP types this is attempted for, since echo/timestamp/Neighbor Discovery types don't carry an embedded packet at all. Live-verified with a real traceroute to 8.8.8.8 captured on wlp1s0: genuine Time Exceeded messages from real intermediate routers - this machine's own gateway, then real ISP infrastructure several hops out - all correctly showing the flow that triggered them, matching traceroute's own hop output. --- tests/test_icmp.cpp | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) (limited to 'tests') diff --git a/tests/test_icmp.cpp b/tests/test_icmp.cpp index 520e12d..7016a72 100644 --- a/tests/test_icmp.cpp +++ b/tests/test_icmp.cpp @@ -72,6 +72,92 @@ TEST_CASE("icmpv6_type_name covers known types and falls back for unknown ones") CHECK(icmpv6_type_name(250) == "type=250"); } +namespace { + +// Builds a real ICMPv4 Destination Unreachable message wrapping a +// truncated original UDP packet - exactly the shape RFC 792 promises: +// original IP header + first 8 bytes of the original datagram's data +// (enough to reach a UDP/TCP header's two port fields). +std::vector dest_unreachable_wrapping_udp() { + std::vector original_ip(20, 0); + original_ip[0] = 0x45; + original_ip[9] = kProtoUdp; + original_ip[12] = 10; original_ip[13] = 0; original_ip[14] = 0; original_ip[15] = 5; + original_ip[16] = 10; original_ip[17] = 0; original_ip[18] = 0; original_ip[19] = 6; + + std::vector original_udp_start = {0x1F, 0x90, 0x00, 0x35}; // src=8080, dst=53 + + std::vector icmp = {3, 3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; // port unreachable + icmp.insert(icmp.end(), original_ip.begin(), original_ip.end()); + icmp.insert(icmp.end(), original_udp_start.begin(), original_udp_start.end()); + return icmp; +} + +} // namespace + +TEST_CASE("icmpv4_embedded_flow recovers the original flow's addresses and ports") { + auto flow = icmpv4_embedded_flow(dest_unreachable_wrapping_udp()); + REQUIRE(flow.has_value()); + CHECK(*flow == "10.0.0.5 -> 10.0.0.6 proto=17 (8080 -> 53)"); +} + +TEST_CASE("icmpv4_embedded_flow omits ports for a non-TCP/UDP embedded protocol") { + std::vector original_ip(20, 0); + original_ip[0] = 0x45; + original_ip[9] = kProtoIcmp; // an ICMP error about an ICMP packet (e.g. a ping that failed) + original_ip[12] = 10; original_ip[15] = 1; + original_ip[16] = 10; original_ip[19] = 2; + + std::vector icmp = {11, 0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + icmp.insert(icmp.end(), original_ip.begin(), original_ip.end()); + + auto flow = icmpv4_embedded_flow(icmp); + REQUIRE(flow.has_value()); + CHECK(*flow == "10.0.0.1 -> 10.0.0.2 proto=1"); +} + +TEST_CASE("icmpv4_embedded_flow returns nullopt when there's no room for an embedded packet") { + std::vector icmp = {3, 3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + CHECK_FALSE(icmpv4_embedded_flow(icmp).has_value()); +} + +TEST_CASE("icmpv4_is_error_type covers the RFC 792 error types and excludes echo/timestamp") { + CHECK(icmpv4_is_error_type(3)); // Destination Unreachable + CHECK(icmpv4_is_error_type(11)); // Time Exceeded + CHECK_FALSE(icmpv4_is_error_type(8)); // Echo Request + CHECK_FALSE(icmpv4_is_error_type(13)); // Timestamp Request +} + +TEST_CASE("icmpv6_embedded_flow recovers the original flow's addresses and ports") { + std::vector original_ip6(40, 0); + original_ip6[0] = 0x60; + original_ip6[6] = kProtoUdp; // next_header + original_ip6[7] = 64; // hop_limit + original_ip6[8] = 0x20; original_ip6[9] = 0x01; // src: 2001:db8::1 + original_ip6[10] = 0x0d; original_ip6[11] = 0xb8; + original_ip6[23] = 0x01; + original_ip6[24] = 0x20; original_ip6[25] = 0x01; // dst: 2001:db8::2 + original_ip6[26] = 0x0d; original_ip6[27] = 0xb8; + original_ip6[39] = 0x02; + + std::vector original_udp_start = {0x1F, 0x90, 0x00, 0x35}; + + std::vector icmp = {1, 4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; // port unreachable + icmp.insert(icmp.end(), original_ip6.begin(), original_ip6.end()); + icmp.insert(icmp.end(), original_udp_start.begin(), original_udp_start.end()); + + auto flow = icmpv6_embedded_flow(icmp); + REQUIRE(flow.has_value()); + CHECK(*flow == "2001:db8::1 -> 2001:db8::2 next=17 (8080 -> 53)"); +} + +TEST_CASE("icmpv6_is_error_type covers the RFC 4443 error types and excludes echo/Redirect") { + CHECK(icmpv6_is_error_type(1)); // Destination Unreachable + CHECK(icmpv6_is_error_type(3)); // Time Exceeded + CHECK_FALSE(icmpv6_is_error_type(128)); // Echo Request + CHECK_FALSE(icmpv6_is_error_type(137)); // Redirect: different, not generic embedded-packet format +} + TEST_CASE("the same type number means something different in each protocol's table") { // The whole reason these are two separate tables, not one shared by // number: ICMPv4's echo request is type 8, but ICMPv6's type 8 -- cgit v1.2.3