diff options
| author | srdusr <[email protected]> | 2024-05-21 22:24:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2024-05-21 22:24:00 +0200 |
| commit | fbedc55d5aa861c381701c9f913b34ee7ab57ec4 (patch) | |
| tree | d34c3648b61d417f2a5d8690e0eb4a76bd64c943 /include | |
| parent | e0f4c701028aa81026a17cf9ebfb36112184f4bc (diff) | |
| download | packeteer-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.hpp | 44 | ||||
| -rw-r--r-- | include/wireframe/l7/ssh.hpp | 65 | ||||
| -rw-r--r-- | include/wireframe/packet_diagnostics.hpp | 92 | ||||
| -rw-r--r-- | include/wireframe/summarize.hpp | 12 |
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; |