srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/include
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 /include
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.
Diffstat (limited to 'include')
-rw-r--r--include/packeteer/net/rtcp.hpp69
-rw-r--r--include/packeteer/net/rtp.hpp88
-rw-r--r--include/packeteer/summarize.hpp13
3 files changed, 170 insertions, 0 deletions
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) {