srdusr
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--PLAN.md21
-rw-r--r--include/packeteer/net/icmp.hpp75
-rw-r--r--include/packeteer/summarize.hpp6
-rw-r--r--tests/test_icmp.cpp86
4 files changed, 188 insertions, 0 deletions
diff --git a/PLAN.md b/PLAN.md
index 65130fe..7ad2649 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -664,3 +664,24 @@ None currently open.
with 8 real IPv6 addresses, all correctly listed), and DNS RR type
65 (HTTPS records, ancount=0 in these captures) correctly producing
no answers suffix since there was nothing to list.
+- ICMP embedded-flow decoding: Destination Unreachable, Time Exceeded,
+ Redirect, Source Quench, and Parameter Problem (ICMPv4) / their
+ ICMPv6 equivalents all carry, after their own fixed header, as much
+ of the packet that actually triggered the error as the network could
+ fit - always at least its IP header plus the first 8 bytes of
+ payload (RFC 792/4443), 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, the same insight DNS's answer-record work leaned
+ on when it reused parse_ipv4's sibling reasoning for name
+ compression. Two small is_error_type() helpers gate which ICMP
+ types this is even attempted for (echo/timestamp/Neighbor Discovery
+ types don't carry an embedded packet at all), rather than relying on
+ parse_ipv4/parse_ipv6 to just fail gracefully on irrelevant types.
+ Live-verified with a real traceroute to 8.8.8.8 (traceroute -I,
+ ICMP-based) 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 "[this
+ machine -> 8.8.8.8 proto=1]", matching traceroute's own hop output.
diff --git a/include/packeteer/net/icmp.hpp b/include/packeteer/net/icmp.hpp
index d2613a2..60fd1d7 100644
--- a/include/packeteer/net/icmp.hpp
+++ b/include/packeteer/net/icmp.hpp
@@ -1,11 +1,14 @@
#pragma once
#include <cstdint>
+#include <cstdio>
#include <optional>
#include <span>
#include <string>
#include "packeteer/byteio.hpp"
+#include "packeteer/net/ipv4.hpp"
+#include "packeteer/net/ipv6.hpp"
// ICMPv4 (RFC 792) and ICMPv6 (RFC 4443) share the same first-4-byte
// shape (Type, Code, Checksum) but a completely different type
@@ -36,6 +39,22 @@ inline std::optional<IcmpHeader> parse_icmpv4(std::span<const unsigned char> byt
return header;
}
+// Destination Unreachable, Source Quench, Redirect, Time Exceeded, and
+// Parameter Problem are the ICMPv4 types that carry an embedded
+// original packet (RFC 792) - everything else (echo, timestamp)
+// doesn't, so icmpv4_embedded_flow() is only worth trying for these.
+inline bool icmpv4_is_error_type(std::uint8_t type) {
+ return type == 3 || type == 4 || type == 5 || type == 11 || type == 12;
+}
+
+// Destination Unreachable, Packet Too Big, Time Exceeded, and
+// Parameter Problem for ICMPv6 (RFC 4443) - echo and the various
+// Neighbor Discovery types (Router/Neighbor Solicitation/
+// Advertisement, Redirect) don't carry an embedded packet at all.
+inline bool icmpv6_is_error_type(std::uint8_t type) {
+ return type == 1 || type == 2 || type == 3 || type == 4;
+}
+
inline std::string icmpv4_type_name(std::uint8_t type) {
switch (type) {
case 0: return "Echo Reply";
@@ -81,4 +100,60 @@ inline std::string icmpv6_type_name(std::uint8_t type) {
}
}
+namespace detail {
+inline std::string icmp_ipv4_to_string(const Ipv4Address& addr) {
+ char buf[16];
+ std::snprintf(buf, sizeof(buf), "%u.%u.%u.%u", addr.bytes[0], addr.bytes[1], addr.bytes[2],
+ addr.bytes[3]);
+ return buf;
+}
+} // namespace detail
+
+// Destination Unreachable, Time Exceeded, Redirect, and Parameter
+// Problem all carry, after their own fixed 8-byte 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
+// (RFC 792/4443), exactly enough to recover TCP/UDP port numbers. This
+// is what actually answers "which flow got this error", the reason an
+// ICMP error shows up in a capture in the first place. Reuses
+// parse_ipv4() directly on those bytes rather than a separate parser:
+// it's a genuine (if truncated) IPv4 packet, not a different format.
+inline std::optional<std::string> icmpv4_embedded_flow(std::span<const unsigned char> icmp_bytes) {
+ if (icmp_bytes.size() < 8) return std::nullopt;
+ auto ip = parse_ipv4(icmp_bytes.subspan(8));
+ if (!ip) return std::nullopt;
+
+ std::string out = detail::icmp_ipv4_to_string(ip->header.src) + " -> " +
+ detail::icmp_ipv4_to_string(ip->header.dst) +
+ " proto=" + std::to_string(ip->header.protocol);
+ if ((ip->header.protocol == kProtoTcp || ip->header.protocol == kProtoUdp) &&
+ ip->payload.size() >= 4) {
+ out += " (" + std::to_string(read_be16(ip->payload, 0)) + " -> " +
+ std::to_string(read_be16(ip->payload, 2)) + ")";
+ }
+ return out;
+}
+
+// Same idea for ICMPv6, over the embedded IPv6 packet's fixed 40-byte
+// header. Extension headers on the embedded packet aren't walked (see
+// walk_ipv6_extension_headers() in ipv6.hpp for the full version this
+// deliberately doesn't reuse here) - an embedded packet's next_header
+// naming an extension header rather than TCP/UDP directly is rare
+// enough in practice not to be worth the extra complexity for a
+// nested, best-effort field.
+inline std::optional<std::string> icmpv6_embedded_flow(std::span<const unsigned char> icmp_bytes) {
+ if (icmp_bytes.size() < 8) return std::nullopt;
+ auto ip6 = parse_ipv6(icmp_bytes.subspan(8));
+ if (!ip6) return std::nullopt;
+
+ std::string out = ipv6_to_string(ip6->header.src) + " -> " + ipv6_to_string(ip6->header.dst) +
+ " next=" + std::to_string(ip6->header.next_header);
+ if ((ip6->header.next_header == kProtoTcp || ip6->header.next_header == kProtoUdp) &&
+ ip6->payload.size() >= 4) {
+ out += " (" + std::to_string(read_be16(ip6->payload, 0)) + " -> " +
+ std::to_string(read_be16(ip6->payload, 2)) + ")";
+ }
+ return out;
+}
+
} // namespace packeteer::net
diff --git a/include/packeteer/summarize.hpp b/include/packeteer/summarize.hpp
index c538cec..4126eb5 100644
--- a/include/packeteer/summarize.hpp
+++ b/include/packeteer/summarize.hpp
@@ -218,6 +218,10 @@ inline std::string summarize_transport_and_above(const IpInfo& info) {
if (icmp->identifier) {
out += " id=" + std::to_string(*icmp->identifier) +
" seq=" + std::to_string(*icmp->sequence);
+ } else if (net::icmpv4_is_error_type(icmp->type)) {
+ // Answers the actual reason this error shows up in a
+ // capture: which flow triggered it.
+ if (auto flow = net::icmpv4_embedded_flow(info.payload)) out += " [" + *flow + "]";
}
}
} else if (info.proto == net::kProtoIgmp) {
@@ -234,6 +238,8 @@ inline std::string summarize_transport_and_above(const IpInfo& info) {
if (icmp->identifier) {
out += " id=" + std::to_string(*icmp->identifier) +
" seq=" + std::to_string(*icmp->sequence);
+ } else if (net::icmpv6_is_error_type(icmp->type)) {
+ if (auto flow = net::icmpv6_embedded_flow(info.payload)) out += " [" + *flow + "]";
}
} else {
out += " | ICMPv6"; // truncated: at least say what it is
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<unsigned char> dest_unreachable_wrapping_udp() {
+ std::vector<unsigned char> 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<unsigned char> original_udp_start = {0x1F, 0x90, 0x00, 0x35}; // src=8080, dst=53
+
+ std::vector<unsigned char> 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<unsigned char> 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<unsigned char> 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<unsigned char> 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<unsigned char> 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<unsigned char> original_udp_start = {0x1F, 0x90, 0x00, 0x35};
+
+ std::vector<unsigned char> 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