srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/include
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2024-05-21 22:24:00 +0200
committersrdusr <[email protected]>2024-05-21 22:24:00 +0200
commitfbedc55d5aa861c381701c9f913b34ee7ab57ec4 (patch)
treed34c3648b61d417f2a5d8690e0eb4a76bd64c943 /include
parente0f4c701028aa81026a17cf9ebfb36112184f4bc (diff)
downloadpacketeer-fbedc55d5aa861c381701c9f913b34ee7ab57ec4.tar.gz
packeteer-fbedc55d5aa861c381701c9f913b34ee7ab57ec4.zip
Add GUI parity for -c/-a, mDNS/SSH dissectors, and two new fuzz harnesses
GUI parity: checksum_status()/reassembled_http_status() moved out of main.cpp into a shared wireframe/packet_diagnostics.hpp so the GUI can show the same -c/-a diagnostics for the selected packet without duplicating the Ethernet/IPv4/TCP walk. Visually verified under Xvfb with the same split-segment scenario used to verify -a on the CLI. Two new L7 dissectors: mDNS (reuses parse_dns outright - RFC 6762 keeps DNS's wire format, just a different port) and SSH's cleartext identification banner. Live-verified against this machine's real sshd and a real DNS-wire-format packet sent to port 5353. Two new fuzz harnesses (fuzz_checksum, fuzz_tcp_reassembly) covering code added here that the original nine harnesses never touched. All 12 run clean across ~90M executions with no crashes. NAMES.md and PLAN.md updated with this round's decisions and naming candidates.
Diffstat (limited to 'include')
-rw-r--r--include/wireframe/l7/mdns.hpp44
-rw-r--r--include/wireframe/l7/ssh.hpp65
-rw-r--r--include/wireframe/packet_diagnostics.hpp92
-rw-r--r--include/wireframe/summarize.hpp12
4 files changed, 212 insertions, 1 deletions
diff --git a/include/wireframe/l7/mdns.hpp b/include/wireframe/l7/mdns.hpp
new file mode 100644
index 0000000..887d811
--- /dev/null
+++ b/include/wireframe/l7/mdns.hpp
@@ -0,0 +1,44 @@
+#pragma once
+
+#include <cstdint>
+#include <optional>
+#include <span>
+#include <string>
+
+#include "wireframe/l7/dissector.hpp"
+#include "wireframe/l7/dns.hpp"
+
+// mDNS (RFC 6762) reuses DNS's exact wire format - same header layout,
+// same question/name encoding - just over a different port (5353,
+// usually to/from the multicast address 224.0.0.251) and typically
+// with many questions/answers per packet instead of DNS's usual one.
+// parse_dns() already only looks at the first question, which is true
+// here too; the only real difference worth a label is which protocol
+// this traffic actually is, so real-world capture output doesn't read
+// "DNS" for traffic that never touched a resolver.
+namespace wireframe::net {
+
+inline constexpr std::uint16_t kMdnsPort = 5353;
+
+class MdnsDissector : public L7Dissector {
+public:
+ std::uint16_t port() const override { return kMdnsPort; }
+
+ std::optional<std::string> summarize(std::span<const unsigned char> payload) const override {
+ auto msg = parse_dns(payload);
+ if (!msg) return std::nullopt;
+
+ // No id= field here unlike DnsDissector's summary: RFC 6762
+ // 18.1 has multicast queries send it as zero, so printing it
+ // would just be "id=0" noise on real traffic.
+ std::string out = "mDNS ";
+ out += msg->header.is_response ? "response" : "query";
+ if (msg->header.is_response) out += " ancount=" + std::to_string(msg->header.ancount);
+ if (msg->question) {
+ out += " " + msg->question->name + " type=" + std::to_string(msg->question->qtype);
+ }
+ return out;
+ }
+};
+
+} // namespace wireframe::net
diff --git a/include/wireframe/l7/ssh.hpp b/include/wireframe/l7/ssh.hpp
new file mode 100644
index 0000000..efa471f
--- /dev/null
+++ b/include/wireframe/l7/ssh.hpp
@@ -0,0 +1,65 @@
+#pragma once
+
+#include <cstdint>
+#include <optional>
+#include <span>
+#include <string>
+#include <string_view>
+
+#include "wireframe/l7/dissector.hpp"
+
+// SSH's identification exchange (RFC 4253 section 4.2) is the one part
+// of an SSH connection sent in the clear, before key exchange starts
+// encrypting everything: both sides open with a single line of the
+// form "SSH-protoversion-softwareversion[ comments]" terminated by
+// CR LF (a bare LF is tolerated too, same leniency this project's HTTP
+// dissector already uses). Only that first line is ever readable --
+// everything after key exchange is opaque, so this dissector only ever
+// has one line to look at, on either side of the connection.
+namespace wireframe::net {
+
+inline constexpr std::uint16_t kSshPort = 22;
+
+struct SshBanner {
+ std::string proto_version;
+ std::string software_version;
+};
+
+inline std::optional<SshBanner> parse_ssh_banner(std::span<const unsigned char> payload) {
+ std::string_view text(reinterpret_cast<const char*>(payload.data()), payload.size());
+ if (text.substr(0, 4) != "SSH-") return std::nullopt;
+
+ std::size_t line_end = text.find("\r\n");
+ if (line_end == std::string_view::npos) {
+ line_end = text.find('\n');
+ if (line_end == std::string_view::npos) return std::nullopt;
+ }
+ std::string_view line = text.substr(4, line_end - 4); // past "SSH-"
+
+ std::size_t dash = line.find('-');
+ if (dash == std::string_view::npos) return std::nullopt;
+
+ SshBanner banner;
+ banner.proto_version = std::string(line.substr(0, dash));
+
+ // The software version runs up to the first space (start of an
+ // optional comment) or the end of the line, whichever is first.
+ std::string_view rest = line.substr(dash + 1);
+ std::size_t space = rest.find(' ');
+ banner.software_version = std::string(space == std::string_view::npos ? rest
+ : rest.substr(0, space));
+ return banner;
+}
+
+class SshDissector : public L7Dissector {
+public:
+ std::uint16_t port() const override { return kSshPort; }
+
+ std::optional<std::string> summarize(std::span<const unsigned char> payload) const override {
+ auto banner = parse_ssh_banner(payload);
+ if (!banner) return std::nullopt;
+ return "SSH " + banner->proto_version + " " + banner->software_version;
+ }
+};
+
+} // namespace wireframe::net
diff --git a/include/wireframe/packet_diagnostics.hpp b/include/wireframe/packet_diagnostics.hpp
new file mode 100644
index 0000000..4b9b0c6
--- /dev/null
+++ b/include/wireframe/packet_diagnostics.hpp
@@ -0,0 +1,92 @@
+#pragma once
+
+#include <optional>
+#include <pcap.h>
+#include <span>
+#include <string>
+
+#include "wireframe/l7/http.hpp"
+#include "wireframe/net/checksum.hpp"
+#include "wireframe/net/ethernet.hpp"
+#include "wireframe/net/ipv4.hpp"
+#include "wireframe/net/tcp.hpp"
+#include "wireframe/net/tcp_reassembly.hpp"
+
+// Checksum validation and TCP stream reassembly are both deliberately
+// kept out of summarize_packet()'s shared per-packet output - see
+// wireframe/net/checksum.hpp and wireframe/net/tcp_reassembly.hpp for
+// why each is opt-in (checksum offload false positives; reassembly's
+// per-flow state and extra per-packet work). Shared between the CLI
+// (-c/-a) and GUI frontends so they don't hand-roll two separate
+// Ethernet/IPv4/TCP walks down to the same byte spans - the same
+// reasoning wireframe::CaptureSession exists for at the setup layer.
+namespace wireframe {
+
+inline std::string checksum_status(std::span<const unsigned char> bytes, int datalink) {
+ std::span<const unsigned char> ip_bytes;
+ if (datalink == DLT_RAW) {
+ ip_bytes = bytes;
+ } else {
+ auto eth = net::parse_ethernet(bytes);
+ if (!eth || eth->header.ethertype != net::kEthertypeIPv4) return "";
+ ip_bytes = eth->payload;
+ }
+ if (ip_bytes.empty() || (ip_bytes[0] >> 4) != 4) return ""; // only IPv4 checksums, for now
+
+ auto ip = net::parse_ipv4(ip_bytes);
+ if (!ip) return "";
+
+ std::size_t header_len = static_cast<std::size_t>(ip->header.ihl) * 4;
+ std::string out = "checksums: IP=";
+ out += net::verify_ipv4_checksum(ip_bytes.first(header_len)) ? "ok" : "BAD";
+
+ using net::ChecksumResult;
+ if (ip->header.protocol == net::kProtoTcp) {
+ auto result = net::verify_tcp_checksum_ipv4(ip->header.src, ip->header.dst, ip->payload);
+ out += result == ChecksumResult::kValid ? " TCP=ok" : " TCP=BAD";
+ } else if (ip->header.protocol == net::kProtoUdp) {
+ auto result = net::verify_udp_checksum_ipv4(ip->header.src, ip->header.dst, ip->payload);
+ out += result == ChecksumResult::kValid ? " UDP=ok"
+ : result == ChecksumResult::kNotPresent ? " UDP=none"
+ : " UDP=BAD";
+ }
+ return out;
+}
+
+inline std::optional<std::string> reassembled_http_status(std::span<const unsigned char> bytes,
+ int datalink,
+ net::TcpReassembler& reassembler) {
+ std::span<const unsigned char> ip_bytes;
+ if (datalink == DLT_RAW) {
+ ip_bytes = bytes;
+ } else {
+ auto eth = net::parse_ethernet(bytes);
+ if (!eth || eth->header.ethertype != net::kEthertypeIPv4) return std::nullopt;
+ ip_bytes = eth->payload;
+ }
+ if (ip_bytes.empty() || (ip_bytes[0] >> 4) != 4) return std::nullopt; // IPv4 only, for now
+
+ auto ip = net::parse_ipv4(ip_bytes);
+ if (!ip || ip->header.protocol != net::kProtoTcp) return std::nullopt;
+
+ auto tcp = net::parse_tcp(ip->payload);
+ if (!tcp) return std::nullopt;
+
+ auto reassembled = reassembler.process_segment(ip->header.src, tcp->header.src_port,
+ ip->header.dst, tcp->header.dst_port,
+ tcp->header.seq, tcp->header.flags,
+ tcp->payload);
+ if (!reassembled) return std::nullopt;
+
+ auto http = net::parse_http(*reassembled);
+ if (!http) return std::nullopt;
+
+ std::string out = "reassembled ";
+ out += http->is_request ? "request: " : "response: ";
+ out += http->method_or_version + " " + http->target_or_status;
+ if (http->host) out += " Host: " + *http->host;
+ out += " (" + std::to_string(reassembled->size()) + " bytes so far)";
+ return out;
+}
+
+} // namespace wireframe
diff --git a/include/wireframe/summarize.hpp b/include/wireframe/summarize.hpp
index e7e9ae3..840ddf9 100644
--- a/include/wireframe/summarize.hpp
+++ b/include/wireframe/summarize.hpp
@@ -11,6 +11,8 @@
#include "wireframe/l7/dissector.hpp"
#include "wireframe/l7/dns.hpp"
#include "wireframe/l7/http.hpp"
+#include "wireframe/l7/mdns.hpp"
+#include "wireframe/l7/ssh.hpp"
#include "wireframe/l7/tls.hpp"
#include "wireframe/net/ethernet.hpp"
#include "wireframe/net/icmp.hpp"
@@ -57,16 +59,24 @@ inline std::string tcp_flags_to_string(std::uint8_t flags) {
// DNS alone never did, since it only ever runs over UDP port 53. TLS
// (also TCP, port 443) covers what HTTP increasingly can't: most web
// traffic today is encrypted, and SNI is the one piece of a TLS
-// handshake still readable without decrypting anything.
+// handshake still readable without decrypting anything. mDNS reuses
+// DNS's own parser (same wire format, different port/label) at
+// essentially no extra cost. SSH is the first dissector whose *entire*
+// protocol is one cleartext line before everything else encrypts --
+// unlike TLS's SNI, there's nothing further to ever add here.
inline const net::L7Registry& l7_registry() {
static const net::DnsDissector dns_dissector;
static const net::HttpDissector http_dissector;
static const net::TlsSniDissector tls_dissector;
+ static const net::MdnsDissector mdns_dissector;
+ static const net::SshDissector ssh_dissector;
static const net::L7Registry registry = [] {
net::L7Registry r;
r.add(&dns_dissector);
r.add(&http_dissector);
r.add(&tls_dissector);
+ r.add(&mdns_dissector);
+ r.add(&ssh_dissector);
return r;
}();
return registry;