srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/include
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2026-05-29 22:56:00 +0200
committersrdusr <[email protected]>2026-05-29 22:56:00 +0200
commit719b7f439c1c8c76d0573b34daa329061c9ad8a6 (patch)
treed6a1ad5546332859a717bdadbfd72ff4041e18f3 /include
parente41dade9ac3bbd7ffbc1eec826801af1a38d8b9e (diff)
downloadpacketeer-719b7f439c1c8c76d0573b34daa329061c9ad8a6.tar.gz
packeteer-719b7f439c1c8c76d0573b34daa329061c9ad8a6.zip
Fix IPv4/IPv6 fragment continuation data being decoded as fake transport headers
A real correctness bug, not just missing visibility: a non-first fragment's payload is pure continuation data with no TCP/UDP/ICMP header in it at all, but it was being handed to the transport parsers unconditionally, which could misread arbitrary payload bytes as port numbers, sequence numbers, etc. and print a plausible-looking but entirely fake decode. IPv4 gained identification/more_fragments/fragment_offset fields; a nonzero offset now stops summarize_packet before transport dispatch, reporting the fragment instead. IPv6 expresses fragmentation as an extension header instead, so the fix lives in walk_ipv6_extension_headers(): it already walked past Fragment headers, but never checked the offset before continuing on as if a transport header followed - the same bug, reached through a different path. Fixed with an ESP-style hard stop on a nonzero offset. Caught and fixed a second bug while writing the first fix, before it ever ran: the fragment's Identification field was a loop-local variable, discarded the moment the walk continued past a *first* fragment (offset zero) to keep decoding the real payload underneath -- every later return reported no fragment id even though one applied. Fixed by hoisting it to a variable that persists across iterations, the same category of mistake as an earlier IGMPv3 bug. Fuzzed afterward regardless (fuzz_ipv4, fuzz_ipv6, fuzz_summarize, ~16.8M combined runs) - clean. Live-verified with a real 4000-byte ping to the local gateway over the actual 1500-MTU interface: both directions fragmented into 3 pieces each, the first decoded normally with a fragmentation note, and the continuation fragments correctly showed only fragment metadata, no fake ICMP decode attempted.
Diffstat (limited to 'include')
-rw-r--r--include/packeteer/net/ipv4.hpp11
-rw-r--r--include/packeteer/net/ipv6.hpp53
-rw-r--r--include/packeteer/summarize.hpp28
3 files changed, 86 insertions, 6 deletions
diff --git a/include/packeteer/net/ipv4.hpp b/include/packeteer/net/ipv4.hpp
index c6cb656..bcc8fa6 100644
--- a/include/packeteer/net/ipv4.hpp
+++ b/include/packeteer/net/ipv4.hpp
@@ -27,6 +27,13 @@ struct Ipv4Header {
std::uint8_t protocol;
Ipv4Address src;
Ipv4Address dst;
+ std::uint16_t identification; // ties fragments of the same original datagram together
+ bool more_fragments;
+ // In 8-byte units (RFC 791) - 0 means this is either an unfragmented
+ // packet or the *first* fragment, either way the one that actually
+ // carries the transport header. Nonzero means a later fragment:
+ // pure payload continuation, no TCP/UDP/ICMP header present at all.
+ std::uint16_t fragment_offset;
};
struct Ipv4Packet {
@@ -46,6 +53,10 @@ inline std::optional<Ipv4Packet> parse_ipv4(std::span<const unsigned char> bytes
header.version = version;
header.ihl = ihl;
header.total_length = read_be16(bytes, 2);
+ header.identification = read_be16(bytes, 4);
+ std::uint16_t flags_and_offset = read_be16(bytes, 6);
+ header.more_fragments = (flags_and_offset & 0x2000) != 0;
+ header.fragment_offset = flags_and_offset & 0x1FFF;
header.ttl = bytes[8];
header.protocol = bytes[9];
std::copy_n(bytes.begin() + 12, 4, header.src.bytes.begin());
diff --git a/include/packeteer/net/ipv6.hpp b/include/packeteer/net/ipv6.hpp
index 8c048e8..d7ea0d5 100644
--- a/include/packeteer/net/ipv6.hpp
+++ b/include/packeteer/net/ipv6.hpp
@@ -70,6 +70,18 @@ struct Ipv6ExtensionWalkResult {
std::uint8_t final_next_header; // a transport protocol, or an extension type we stopped at
std::span<const unsigned char> payload; // bytes after every extension header walked
bool stopped_at_esp; // true if ESP was hit - see walk_ipv6_extension_headers()
+ // True when a Fragment header (RFC 8200 4.5) was walked with a
+ // nonzero fragment offset - a later fragment, not the first.
+ // final_next_header still correctly names the eventual transport
+ // protocol, but `payload` here is pure continuation data with no
+ // TCP/UDP/ICMPv6 header in it at all; callers must not hand it to
+ // a transport parser. fragment_id is that header's 4-byte
+ // Identification field, set whenever a Fragment header was seen
+ // at all (first fragment or not) - useful for correlating
+ // fragments of the same original datagram even when this flag is
+ // false for the first one.
+ bool is_non_first_fragment;
+ std::optional<std::uint32_t> fragment_id;
};
// Walks Hop-by-Hop, Routing, Destination Options, Fragment, and AH
@@ -96,35 +108,64 @@ struct Ipv6ExtensionWalkResult {
inline Ipv6ExtensionWalkResult walk_ipv6_extension_headers(std::uint8_t next_header,
std::span<const unsigned char> payload) {
constexpr int kMaxExtensionHeaders = 8;
+ // Persists across loop iterations, unlike a Fragment header's other
+ // fields: once a Fragment header is seen (even the first one, whose
+ // walk continues normally afterward), every return from here on
+ // should still report its Identification field, not lose it the
+ // moment the loop moves past that one header.
+ std::optional<std::uint32_t> seen_fragment_id;
for (int i = 0; i < kMaxExtensionHeaders; ++i) {
if (next_header == kNextHeaderEsp) {
- return {next_header, payload, /*stopped_at_esp=*/true};
+ return {next_header, payload, /*stopped_at_esp=*/true, false, seen_fragment_id};
}
std::size_t ext_len;
if (next_header == kNextHeaderFragment) {
- if (payload.size() < 8) return {next_header, payload, false};
+ if (payload.size() < 8) return {next_header, payload, false, false, seen_fragment_id};
ext_len = 8;
} else if (next_header == kNextHeaderAh) {
- if (payload.size() < 2) return {next_header, payload, false};
+ if (payload.size() < 2) return {next_header, payload, false, false, seen_fragment_id};
ext_len = (static_cast<std::size_t>(payload[1]) + 2) * 4;
} else if (next_header == kNextHeaderHopByHop || next_header == kNextHeaderRouting ||
next_header == kNextHeaderDestOptions) {
- if (payload.size() < 2) return {next_header, payload, false};
+ if (payload.size() < 2) return {next_header, payload, false, false, seen_fragment_id};
ext_len = (static_cast<std::size_t>(payload[1]) + 1) * 8;
} else {
break; // TCP/UDP/ICMPv6/anything else we don't chain through: stop here
}
- if (payload.size() < ext_len) return {next_header, payload, false}; // truncated: stop
+ if (payload.size() < ext_len) {
+ return {next_header, payload, false, false, seen_fragment_id}; // truncated: stop
+ }
+
+ if (next_header == kNextHeaderFragment) {
+ // Fragment Offset (13 bits) + Res (2 bits) + M flag (1 bit)
+ // at bytes[2:4]; Identification (4 bytes) at bytes[4:8].
+ std::uint16_t offset_res_m = read_be16(payload, 2);
+ std::uint16_t fragment_offset = (offset_res_m >> 3) & 0x1FFF;
+ seen_fragment_id = read_be32(payload, 4);
+ std::uint8_t this_next_header = payload[0];
+ std::span<const unsigned char> remaining = payload.subspan(ext_len);
+ if (fragment_offset != 0) {
+ // A later fragment: `remaining` is pure continuation
+ // data, not a transport header, regardless of what
+ // this_next_header names. Stop here rather than
+ // walking (and definitely rather than letting a
+ // caller hand this to a transport parser).
+ return {this_next_header, remaining, false, true, seen_fragment_id};
+ }
+ payload = remaining;
+ next_header = this_next_header;
+ continue;
+ }
std::uint8_t this_next_header = payload[0];
payload = payload.subspan(ext_len);
next_header = this_next_header;
}
- return {next_header, payload, false};
+ return {next_header, payload, false, false, seen_fragment_id};
}
// RFC 5952 canonical text form: lowercase hex, and the longest run of
diff --git a/include/packeteer/summarize.hpp b/include/packeteer/summarize.hpp
index 4126eb5..4bb2d24 100644
--- a/include/packeteer/summarize.hpp
+++ b/include/packeteer/summarize.hpp
@@ -319,9 +319,27 @@ inline std::string summarize_packet(std::span<const unsigned char> bytes, int da
out += " | IPv4 (truncated)";
return out;
}
+ bool is_fragment = ip->header.more_fragments || ip->header.fragment_offset != 0;
+ if (ip->header.fragment_offset != 0) {
+ // A later fragment: no TCP/UDP/ICMP header is present in
+ // this packet at all, only continuation payload - handing
+ // it to a transport parser would misread arbitrary payload
+ // bytes as port numbers/sequence numbers/etc. Report the
+ // fragment itself and stop, rather than decode garbage.
+ char buf[96];
+ std::snprintf(buf, sizeof(buf), " | IPv4 %s -> %s ttl=%u proto=%u fragment id=%u offset=%u%s",
+ ipv4_to_string(ip->header.src).c_str(), ipv4_to_string(ip->header.dst).c_str(),
+ ip->header.ttl, ip->header.protocol, ip->header.identification,
+ ip->header.fragment_offset * 8, ip->header.more_fragments ? " (more)" : "");
+ out += buf;
+ return out;
+ }
out += summarize_transport_and_above({"IPv4", ipv4_to_string(ip->header.src),
ipv4_to_string(ip->header.dst), ip->header.ttl,
ip->header.protocol, ip->payload});
+ if (is_fragment) {
+ out += " (fragmented, id=" + std::to_string(ip->header.identification) + ", more follow)";
+ }
} else if (version == 6) {
auto ip6 = net::parse_ipv6(ip_bytes);
if (!ip6) {
@@ -341,9 +359,19 @@ inline std::string summarize_packet(std::span<const unsigned char> bytes, int da
src_str.c_str(), dst_str.c_str(), ip6->header.hop_limit,
net::kNextHeaderEsp);
out += buf;
+ } else if (walked.is_non_first_fragment) {
+ char buf[128];
+ std::snprintf(buf, sizeof(buf),
+ " | IPv6 %s -> %s ttl=%u proto=%u fragment id=%u (not first)",
+ src_str.c_str(), dst_str.c_str(), ip6->header.hop_limit,
+ walked.final_next_header, *walked.fragment_id);
+ out += buf;
} else {
out += summarize_transport_and_above({"IPv6", src_str, dst_str, ip6->header.hop_limit,
walked.final_next_header, walked.payload});
+ if (walked.fragment_id) {
+ out += " (fragmented, id=" + std::to_string(*walked.fragment_id) + ")";
+ }
}
} else {
out += " | IP version " + std::to_string(version) + " (unsupported)";