srdusr
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2025-11-19 21:18:00 +0200
committersrdusr <[email protected]>2025-11-19 21:18:00 +0200
commit2d010c9f851ea4eb851179db012c8977ce6e4bd5 (patch)
tree2cf84d9643df0baba62aac1b230c21f09e4872e0
parenta8f4866576fd70894ef0080c7797708db664880e (diff)
downloadpacketeer-2d010c9f851ea4eb851179db012c8977ce6e4bd5.tar.gz
packeteer-2d010c9f851ea4eb851179db012c8977ce6e4bd5.zip
Add RTP/RTCP as a labeled heuristic fallback for unmatched UDP traffic
Architecturally different from every other protocol added so far: RTP has no fixed well-known port at all - it's negotiated per call via SDP/SIP/WebRTC signaling this project doesn't parse - so L7Registry's port-keyed dispatch doesn't apply. Handled instead as a fallback tried only when a UDP packet's normal port-based lookup finds nothing, with every match labeled "?" (e.g. "RTCP? SR") to mark it as inferred from packet shape rather than certain - the same honesty Wireshark itself applies to heuristic dissection, which is off by default there for exactly this reason. The two heuristics aren't equally trusted, and the code says so: RTCP checks a narrow packet-type range (200-204) plus an exact self-declared length, both unlikely to occur by chance; RTP leans mostly on the 2-bit version field, since its other structural checks are trivially satisfied whenever those bits happen to be zero, the common case even for unrelated traffic. Shipped anyway - a labeled guess on real RTP/RTCP traffic is more useful than silence - but this is the first place in the project where a match doesn't mean certainty. Live-verified against genuine media traffic: ffmpeg streaming a real RTP video test pattern to loopback, correctly decoded with incrementing sequence numbers and a consistent SSRC across the stream, plus a real RTCP Sender Report ffmpeg sent alongside it.
-rw-r--r--CMakeLists.txt2
-rw-r--r--PLAN.md29
-rw-r--r--include/packeteer/net/rtcp.hpp69
-rw-r--r--include/packeteer/net/rtp.hpp88
-rw-r--r--include/packeteer/summarize.hpp13
-rw-r--r--tests/test_rtcp.cpp69
-rw-r--r--tests/test_rtp.cpp92
-rw-r--r--tests/test_summarize.cpp30
8 files changed, 390 insertions, 2 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b7fab04..20fc70e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -99,6 +99,8 @@ add_executable(packeteer_tests
tests/test_net.cpp
tests/test_arp.cpp
tests/test_igmp.cpp
+ tests/test_rtcp.cpp
+ tests/test_rtp.cpp
tests/test_ipv6.cpp
tests/test_dns.cpp
tests/test_mdns.cpp
diff --git a/PLAN.md b/PLAN.md
index 50d4981..a4db288 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -31,8 +31,9 @@ unowned buffers) via a real-world capture pipeline.
4. [done] Bounded channel + drop-on-backpressure between capture and render
5. [in progress] L7 dissector interface, add protocols incrementally --
interface + DNS + HTTP + TLS SNI + mDNS + SSH banner + NTP + DHCP +
- FTP + SMTP + TFTP + QUIC + SNMP done (packeteer/l7/); ARP/VLAN/IGMP done
- at the L2/L3 level too (packeteer/net/); more protocols can still
+ FTP + SMTP + TFTP + QUIC + SNMP done (packeteer/l7/); ARP/VLAN/IGMP
+ done at the L2/L3 level too, and RTP/RTCP as a heuristic UDP
+ fallback (packeteer/net/); more protocols can still
be added incrementally,
by design
6. [done] Filtering (-f <expr>, libpcap's own BPF compiler - see Decisions)
@@ -562,3 +563,27 @@ None currently open.
correctly with matching request-ids across both directions, and a
real `snmptrap` SNMPv2-Trap on port 162 confirmed the second
registered port actually gets used, not just the first.
+- RTP/RTCP (net/rtp.hpp, net/rtcp.hpp) - architecturally different
+ from every other L7 protocol here: RTP has no fixed well-known port
+ at all, it's negotiated per call via SDP/SIP/WebRTC signaling this
+ project doesn't parse, so L7Registry's port-keyed dispatch simply
+ doesn't apply. Handled instead as a heuristic fallback tried only
+ when a UDP packet's normal port-based lookup finds nothing, and
+ every match is labeled with a "?" (e.g. "RTCP? SR") to mark it as
+ inferred from packet shape, not certain the way a port-matched
+ dissector's result is - the same honesty Wireshark itself applies
+ to heuristic dissection (off by default there for exactly this
+ reason). The two heuristics are deliberately not equally trusted:
+ RTCP's is comparatively strong (packet type in a narrow 200-204
+ range unlikely by chance, plus an exact self-declared length field);
+ RTP's leans mostly on the 2-bit version field being 2, since CSRC
+ count/extension/padding consistency checks are trivially satisfied
+ whenever those bits are zero - the common case even for unrelated
+ traffic. Both still ship, since a labeled guess on real RTP/RTCP
+ traffic is more useful than silence, but this is the first place in
+ the project where "matched" doesn't mean "certain."
+ Live-verified against genuine media traffic: `ffmpeg -f lavfi -i
+ testsrc ... -f rtp rtp://127.0.0.1:5004`, captured on loopback,
+ correctly decoded a real RTP video stream (payload type 96,
+ incrementing sequence numbers, one consistent SSRC across the whole
+ stream) and a real RTCP Sender Report ffmpeg sent alongside it.
diff --git a/include/packeteer/net/rtcp.hpp b/include/packeteer/net/rtcp.hpp
new file mode 100644
index 0000000..30380e8
--- /dev/null
+++ b/include/packeteer/net/rtcp.hpp
@@ -0,0 +1,69 @@
+#pragma once
+
+#include <cstdint>
+#include <optional>
+#include <span>
+#include <string>
+
+#include "packeteer/byteio.hpp"
+
+// RFC 3550 RTCP. Unlike every other L7 protocol in this project, RTP/
+// RTCP have no fixed well-known port - the port is negotiated per
+// call via SDP/SIP/WebRTC signaling this project doesn't parse, so
+// L7Registry's port-keyed dispatch doesn't apply here at all. This is
+// instead tried as a heuristic fallback on UDP traffic that didn't
+// match anything else (see summarize.hpp), and reported with a "?" to
+// mark it as inferred rather than certain, the same honesty Wireshark
+// itself applies to heuristic dissection.
+//
+// RTCP's heuristic is comparatively strong: real RTCP packet types are
+// a narrow, specific range (200-204) unlikely to occur by chance, and
+// the packet carries its own exact length in 32-bit words, both
+// checkable without any session context - meaningfully stronger than
+// rtp.hpp's heuristic, which leans mostly on the 2-bit version field.
+namespace packeteer::net {
+
+inline constexpr std::uint8_t kRtcpSenderReport = 200;
+inline constexpr std::uint8_t kRtcpReceiverReport = 201;
+inline constexpr std::uint8_t kRtcpSourceDescription = 202;
+inline constexpr std::uint8_t kRtcpBye = 203;
+inline constexpr std::uint8_t kRtcpApp = 204;
+
+struct RtcpHeader {
+ std::uint8_t version;
+ std::uint8_t packet_type;
+ std::uint16_t length_words; // packet length in 32-bit words, minus one (RFC 3550 6.4.1)
+};
+
+// Only the first RTCP packet in what may be a compound datagram
+// (several RTCP sub-packets concatenated, e.g. SR followed by SDES)
+// is decoded - matching this project's single-message, best-effort
+// scope elsewhere (DNS's first question, HTTP's first line).
+inline std::optional<RtcpHeader> parse_rtcp_heuristic(std::span<const unsigned char> bytes) {
+ if (bytes.size() < 4) return std::nullopt;
+
+ std::uint8_t version = static_cast<std::uint8_t>((bytes[0] >> 6) & 0x03);
+ if (version != 2) return std::nullopt;
+
+ std::uint8_t packet_type = bytes[1];
+ if (packet_type < kRtcpSenderReport || packet_type > kRtcpApp) return std::nullopt;
+
+ std::uint16_t length_words = read_be16(bytes, 2);
+ std::size_t declared_len = (static_cast<std::size_t>(length_words) + 1) * 4;
+ if (bytes.size() < declared_len) return std::nullopt; // buffer too short for the length claimed
+
+ return RtcpHeader{version, packet_type, length_words};
+}
+
+inline std::string rtcp_packet_type_name(std::uint8_t packet_type) {
+ switch (packet_type) {
+ case kRtcpSenderReport: return "SR";
+ case kRtcpReceiverReport: return "RR";
+ case kRtcpSourceDescription: return "SDES";
+ case kRtcpBye: return "BYE";
+ case kRtcpApp: return "APP";
+ default: return "unknown"; // unreachable: parse_rtcp_heuristic already bounds this
+ }
+}
+
+} // namespace packeteer::net
diff --git a/include/packeteer/net/rtp.hpp b/include/packeteer/net/rtp.hpp
new file mode 100644
index 0000000..adf7a4d
--- /dev/null
+++ b/include/packeteer/net/rtp.hpp
@@ -0,0 +1,88 @@
+#pragma once
+
+#include <cstdint>
+#include <optional>
+#include <span>
+
+#include "packeteer/byteio.hpp"
+
+// RFC 3550 RTP. See rtcp.hpp's header comment for why this is a
+// heuristic UDP fallback rather than a port-registered dissector --
+// RTP has no fixed well-known port at all.
+//
+// This heuristic is deliberately weaker than RTCP's: RTP's payload
+// type is a full 7 bits (minus the 72-76 range reserved to avoid
+// colliding with RTCP's 200-204), so plausible values span most of
+// that range, and the only other checks available (CSRC count and
+// extension/padding fitting inside the buffer) are satisfied trivially
+// whenever those bits happen to be zero - the overwhelmingly common
+// case even for genuinely unrelated UDP traffic. In practice this
+// heuristic is not much stronger than "the first two bits happen to
+// read 2". It's still applied (labeled "?", the same as RTCP) because
+// a labeled guess on real RTP traffic is more useful than silence, but
+// treat matches with real caution on anything that isn't obviously a
+// media stream already.
+namespace packeteer::net {
+
+inline constexpr std::uint8_t kRtcpPayloadTypeReservedLow = 72;
+inline constexpr std::uint8_t kRtcpPayloadTypeReservedHigh = 76;
+
+struct RtpHeader {
+ std::uint8_t version;
+ bool padding;
+ bool extension;
+ std::uint8_t csrc_count;
+ bool marker;
+ std::uint8_t payload_type;
+ std::uint16_t sequence_number;
+ std::uint32_t timestamp;
+ std::uint32_t ssrc;
+};
+
+inline std::optional<RtpHeader> parse_rtp_heuristic(std::span<const unsigned char> bytes) {
+ if (bytes.size() < 12) return std::nullopt;
+
+ std::uint8_t b0 = bytes[0];
+ std::uint8_t version = static_cast<std::uint8_t>((b0 >> 6) & 0x03);
+ if (version != 2) return std::nullopt;
+ bool padding = (b0 & 0x20) != 0;
+ bool extension = (b0 & 0x10) != 0;
+ std::uint8_t csrc_count = b0 & 0x0F;
+
+ std::uint8_t b1 = bytes[1];
+ bool marker = (b1 & 0x80) != 0;
+ std::uint8_t payload_type = b1 & 0x7F;
+ if (payload_type >= kRtcpPayloadTypeReservedLow && payload_type <= kRtcpPayloadTypeReservedHigh) {
+ return std::nullopt; // reserved specifically so RTP/RTCP types never collide
+ }
+
+ std::size_t header_len = 12 + static_cast<std::size_t>(csrc_count) * 4;
+ if (bytes.size() < header_len) return std::nullopt;
+
+ if (extension) {
+ if (bytes.size() < header_len + 4) return std::nullopt;
+ std::uint16_t ext_len_words = read_be16(bytes, header_len + 2);
+ if (bytes.size() < header_len + 4 + static_cast<std::size_t>(ext_len_words) * 4) {
+ return std::nullopt;
+ }
+ }
+
+ if (padding) {
+ std::uint8_t pad_count = bytes.back();
+ if (pad_count == 0 || pad_count > bytes.size()) return std::nullopt;
+ }
+
+ RtpHeader header{};
+ header.version = version;
+ header.padding = padding;
+ header.extension = extension;
+ header.csrc_count = csrc_count;
+ header.marker = marker;
+ header.payload_type = payload_type;
+ header.sequence_number = read_be16(bytes, 2);
+ header.timestamp = read_be32(bytes, 4);
+ header.ssrc = read_be32(bytes, 8);
+ return header;
+}
+
+} // namespace packeteer::net
diff --git a/include/packeteer/summarize.hpp b/include/packeteer/summarize.hpp
index aeff60d..79ec283 100644
--- a/include/packeteer/summarize.hpp
+++ b/include/packeteer/summarize.hpp
@@ -28,6 +28,8 @@
#include "packeteer/net/igmp.hpp"
#include "packeteer/net/ipv4.hpp"
#include "packeteer/net/ipv6.hpp"
+#include "packeteer/net/rtcp.hpp"
+#include "packeteer/net/rtp.hpp"
#include "packeteer/net/tcp.hpp"
#include "packeteer/net/udp.hpp"
@@ -194,6 +196,17 @@ inline std::string summarize_transport_and_above(const IpInfo& info) {
out += udp_buf;
if (auto l7 = l7_summarize(udp->payload, udp->header.src_port, udp->header.dst_port)) {
out += " | " + *l7;
+ } else if (auto rtcp = net::parse_rtcp_heuristic(udp->payload)) {
+ // RTP/RTCP have no fixed port to key a real dissector
+ // off (see rtcp.hpp/rtp.hpp); tried only once nothing
+ // port-based matched, and marked with "?" since this
+ // is inferred from packet shape, not certain the way
+ // a port-matched dissector's result is.
+ out += " | RTCP? " + net::rtcp_packet_type_name(rtcp->packet_type);
+ } else if (auto rtp = net::parse_rtp_heuristic(udp->payload)) {
+ out += " | RTP? pt=" + std::to_string(rtp->payload_type) +
+ " seq=" + std::to_string(rtp->sequence_number) +
+ " ssrc=" + std::to_string(rtp->ssrc);
}
}
} else if (info.proto == net::kProtoIcmp) {
diff --git a/tests/test_rtcp.cpp b/tests/test_rtcp.cpp
new file mode 100644
index 0000000..9dde6cc
--- /dev/null
+++ b/tests/test_rtcp.cpp
@@ -0,0 +1,69 @@
+#include <doctest/doctest.h>
+
+#include <vector>
+
+#include "packeteer/net/rtcp.hpp"
+
+using namespace packeteer::net;
+
+namespace {
+
+std::vector<unsigned char> rtcp_packet(std::uint8_t packet_type, std::uint16_t length_words,
+ std::size_t total_bytes) {
+ std::vector<unsigned char> bytes(total_bytes, 0);
+ bytes[0] = 0x80; // version 2, no padding, RC/SC = 0
+ bytes[1] = packet_type;
+ bytes[2] = static_cast<unsigned char>(length_words >> 8);
+ bytes[3] = static_cast<unsigned char>(length_words & 0xFF);
+ return bytes;
+}
+
+} // namespace
+
+TEST_CASE("parse_rtcp_heuristic decodes a Sender Report") {
+ auto bytes = rtcp_packet(kRtcpSenderReport, 5, 24); // (5+1)*4 = 24 bytes
+ auto rtcp = parse_rtcp_heuristic(bytes);
+ REQUIRE(rtcp.has_value());
+ CHECK(rtcp->version == 2);
+ CHECK(rtcp->packet_type == kRtcpSenderReport);
+ CHECK(rtcp->length_words == 5);
+}
+
+TEST_CASE("parse_rtcp_heuristic accepts the first packet of a longer compound datagram") {
+ auto bytes = rtcp_packet(kRtcpReceiverReport, 1, 8); // declares 8 bytes
+ bytes.resize(40, 0); // but the datagram continues with more RTCP packets after it
+ auto rtcp = parse_rtcp_heuristic(bytes);
+ REQUIRE(rtcp.has_value());
+ CHECK(rtcp->packet_type == kRtcpReceiverReport);
+}
+
+TEST_CASE("parse_rtcp_heuristic rejects a packet type outside 200-204") {
+ auto bytes = rtcp_packet(199, 1, 8);
+ CHECK_FALSE(parse_rtcp_heuristic(bytes).has_value());
+
+ auto bytes2 = rtcp_packet(205, 1, 8);
+ CHECK_FALSE(parse_rtcp_heuristic(bytes2).has_value());
+}
+
+TEST_CASE("parse_rtcp_heuristic rejects version other than 2") {
+ std::vector<unsigned char> bytes = {0x00, kRtcpBye, 0x00, 0x00};
+ CHECK_FALSE(parse_rtcp_heuristic(bytes).has_value());
+}
+
+TEST_CASE("parse_rtcp_heuristic rejects a buffer shorter than the declared length") {
+ auto bytes = rtcp_packet(kRtcpBye, 10, 8); // declares (10+1)*4=44 bytes, buffer is only 8
+ CHECK_FALSE(parse_rtcp_heuristic(bytes).has_value());
+}
+
+TEST_CASE("parse_rtcp_heuristic rejects a buffer shorter than the fixed header") {
+ std::vector<unsigned char> bytes = {0x80, kRtcpBye};
+ CHECK_FALSE(parse_rtcp_heuristic(bytes).has_value());
+}
+
+TEST_CASE("rtcp_packet_type_name names every known type") {
+ CHECK(rtcp_packet_type_name(kRtcpSenderReport) == "SR");
+ CHECK(rtcp_packet_type_name(kRtcpReceiverReport) == "RR");
+ CHECK(rtcp_packet_type_name(kRtcpSourceDescription) == "SDES");
+ CHECK(rtcp_packet_type_name(kRtcpBye) == "BYE");
+ CHECK(rtcp_packet_type_name(kRtcpApp) == "APP");
+}
diff --git a/tests/test_rtp.cpp b/tests/test_rtp.cpp
new file mode 100644
index 0000000..860f267
--- /dev/null
+++ b/tests/test_rtp.cpp
@@ -0,0 +1,92 @@
+#include <doctest/doctest.h>
+
+#include <vector>
+
+#include "packeteer/net/rtp.hpp"
+
+using namespace packeteer::net;
+
+namespace {
+
+std::vector<unsigned char> rtp_packet(std::uint8_t payload_type, std::uint16_t seq,
+ std::uint32_t timestamp, std::uint32_t ssrc,
+ std::size_t extra_payload = 0) {
+ std::vector<unsigned char> bytes(12 + extra_payload, 0);
+ bytes[0] = 0x80; // version 2, no padding, no extension, CC=0
+ bytes[1] = payload_type & 0x7F;
+ bytes[2] = static_cast<unsigned char>(seq >> 8);
+ bytes[3] = static_cast<unsigned char>(seq & 0xFF);
+ bytes[4] = static_cast<unsigned char>(timestamp >> 24);
+ bytes[5] = static_cast<unsigned char>(timestamp >> 16);
+ bytes[6] = static_cast<unsigned char>(timestamp >> 8);
+ bytes[7] = static_cast<unsigned char>(timestamp);
+ bytes[8] = static_cast<unsigned char>(ssrc >> 24);
+ bytes[9] = static_cast<unsigned char>(ssrc >> 16);
+ bytes[10] = static_cast<unsigned char>(ssrc >> 8);
+ bytes[11] = static_cast<unsigned char>(ssrc);
+ return bytes;
+}
+
+} // namespace
+
+TEST_CASE("parse_rtp_heuristic decodes a plain fixed-header packet") {
+ auto bytes = rtp_packet(0, 1000, 160000, 0xDEADBEEF, 160);
+ auto rtp = parse_rtp_heuristic(bytes);
+ REQUIRE(rtp.has_value());
+ CHECK(rtp->version == 2);
+ CHECK(rtp->payload_type == 0);
+ CHECK(rtp->sequence_number == 1000);
+ CHECK(rtp->timestamp == 160000);
+ CHECK(rtp->ssrc == 0xDEADBEEF);
+ CHECK_FALSE(rtp->padding);
+ CHECK_FALSE(rtp->extension);
+ CHECK(rtp->csrc_count == 0);
+}
+
+TEST_CASE("parse_rtp_heuristic rejects version other than 2") {
+ std::vector<unsigned char> bytes(12, 0);
+ bytes[0] = 0x00;
+ CHECK_FALSE(parse_rtp_heuristic(bytes).has_value());
+}
+
+TEST_CASE("parse_rtp_heuristic rejects payload types reserved to avoid an RTCP clash") {
+ auto bytes = rtp_packet(74, 1, 1, 1);
+ CHECK_FALSE(parse_rtp_heuristic(bytes).has_value());
+}
+
+TEST_CASE("parse_rtp_heuristic accounts for CSRC count when checking buffer length") {
+ std::vector<unsigned char> bytes(12, 0);
+ bytes[0] = 0x82; // version 2, CC=2: needs 12 + 2*4 = 20 bytes minimum
+ CHECK_FALSE(parse_rtp_heuristic(bytes).has_value()); // buffer too short for claimed CSRCs
+
+ bytes.resize(20, 0);
+ bytes[0] = 0x82;
+ CHECK(parse_rtp_heuristic(bytes).has_value());
+}
+
+TEST_CASE("parse_rtp_heuristic validates the extension header fits when the X bit is set") {
+ std::vector<unsigned char> bytes(12, 0);
+ bytes[0] = 0x90; // version 2, extension bit set, CC=0
+ CHECK_FALSE(parse_rtp_heuristic(bytes).has_value()); // no room for even the 4-byte ext header
+
+ bytes.resize(16, 0);
+ bytes[0] = 0x90;
+ bytes[14] = 0x00;
+ bytes[15] = 0x00; // extension length = 0 words
+ CHECK(parse_rtp_heuristic(bytes).has_value());
+}
+
+TEST_CASE("parse_rtp_heuristic validates the padding count when the P bit is set") {
+ std::vector<unsigned char> bytes(16, 0);
+ bytes[0] = 0xA0; // version 2, padding bit set, CC=0
+ bytes[15] = 0; // a padding count of 0 is invalid (RFC 3550 5.1)
+ CHECK_FALSE(parse_rtp_heuristic(bytes).has_value());
+
+ bytes[15] = 4; // a plausible padding count
+ CHECK(parse_rtp_heuristic(bytes).has_value());
+}
+
+TEST_CASE("parse_rtp_heuristic rejects a buffer shorter than the fixed 12-byte header") {
+ std::vector<unsigned char> bytes(8, 0x80);
+ CHECK_FALSE(parse_rtp_heuristic(bytes).has_value());
+}
diff --git a/tests/test_summarize.cpp b/tests/test_summarize.cpp
index 9eef454..f180f87 100644
--- a/tests/test_summarize.cpp
+++ b/tests/test_summarize.cpp
@@ -186,6 +186,36 @@ TEST_CASE("summarize_packet decodes an ARP request end to end") {
"ARP who-has 10.0.0.2 tell 10.0.0.1 (aa:bb:cc:dd:ee:ff)");
}
+TEST_CASE("summarize_packet falls back to the RTCP heuristic on an unmatched UDP port") {
+ std::vector<unsigned char> rtcp = {0x80, 0xC9, 0x00, 0x01, 0, 0, 0, 0}; // RR, len=1 -> 8 bytes
+
+ std::vector<unsigned char> udp(8, 0);
+ udp[0] = 0x4E; udp[1] = 0x20; // src port 20000: not any registered L7 port
+ udp[2] = 0x4E; udp[3] = 0x21; // dst port 20001: likewise unregistered
+ std::uint16_t udp_len = static_cast<std::uint16_t>(8 + rtcp.size());
+ udp[4] = static_cast<unsigned char>(udp_len >> 8);
+ udp[5] = static_cast<unsigned char>(udp_len & 0xFF);
+
+ std::vector<unsigned char> ip(20, 0);
+ ip[0] = 0x45;
+ ip[8] = 64;
+ ip[9] = packeteer::net::kProtoUdp;
+ ip[12] = 10; ip[13] = 0; ip[14] = 0; ip[15] = 1;
+ ip[16] = 10; ip[17] = 0; ip[18] = 0; ip[19] = 2;
+
+ std::vector<unsigned char> eth = {
+ 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x08, 0x00,
+ };
+
+ std::vector<unsigned char> frame = eth;
+ frame.insert(frame.end(), ip.begin(), ip.end());
+ frame.insert(frame.end(), udp.begin(), udp.end());
+ frame.insert(frame.end(), rtcp.begin(), rtcp.end());
+
+ auto line = packeteer::summarize_packet(frame, DLT_EN10MB);
+ CHECK(line.find("RTCP? RR") != std::string::npos);
+}
+
TEST_CASE("summarize_packet decodes IGMP directly on IP (not through a TCP/UDP port)") {
std::vector<unsigned char> igmp = {0x16, 0x00, 0x00, 0x00, 239, 255, 255, 250}; // v2 report