diff options
| author | srdusr <[email protected]> | 2024-05-27 22:00:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2024-05-27 22:00:00 +0200 |
| commit | b565d7d9c47ca1ec5af0effd828431ee96027d60 (patch) | |
| tree | fbe0c9c897e78f507443507354d5b1d8fb851099 /include/wireframe/summarize.hpp | |
| parent | fbedc55d5aa861c381701c9f913b34ee7ab57ec4 (diff) | |
| download | packeteer-b565d7d9c47ca1ec5af0effd828431ee96027d60.tar.gz packeteer-b565d7d9c47ca1ec5af0effd828431ee96027d60.zip | |
Rename project from wireframe to packeteer
Decided on the name after weighing alternatives in NAMES.md: packeteer
(packet + -eer, "one who wields packets") fit the project's actual
scope better than the wire/frame pun once it had grown into full
L2-L7 dissection, reassembly, checksums, privilege dropping, and dual
TUI/GUI frontends. No existing packet-capture project uses the name;
the one real-world collision (Packeteer, Inc., a networking company
acquired and folded into Blue Coat/Symantec by 2008) is long defunct.
Mechanical rename throughout: CMake project/target names, the
wireframe:: namespace and include/wireframe/ directory (git mv,
history preserved), every #include path, CLI/GUI help text, and the
project's own working directory. NAMES.md rewritten to record the
decision instead of leaving stale self-referential etymology behind
from the blind rename pass.
Verified after every step: full rebuild (all four targets, no
warnings) and the full test suite (128/128 cases, 366/366 assertions)
both from a fresh reconfigure and again after the directory move.
Diffstat (limited to 'include/wireframe/summarize.hpp')
| -rw-r--r-- | include/wireframe/summarize.hpp | 275 |
1 files changed, 0 insertions, 275 deletions
diff --git a/include/wireframe/summarize.hpp b/include/wireframe/summarize.hpp deleted file mode 100644 index 840ddf9..0000000 --- a/include/wireframe/summarize.hpp +++ /dev/null @@ -1,275 +0,0 @@ -#pragma once - -#include <cstdio> -#include <optional> -#include <span> -#include <string> -#include <vector> - -#include <pcap.h> - -#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" -#include "wireframe/net/ipv4.hpp" -#include "wireframe/net/ipv6.hpp" -#include "wireframe/net/tcp.hpp" -#include "wireframe/net/udp.hpp" - -// Packet -> human-readable summary. Shared by every frontend (plain -// CLI, TUI, GUI) so they can't drift apart on what a given packet -// decodes to - one source of truth, not three copies to keep in sync. -namespace wireframe { - -inline std::string mac_to_string(const net::MacAddress& mac) { - char buf[18]; - std::snprintf(buf, sizeof(buf), "%02x:%02x:%02x:%02x:%02x:%02x", mac.bytes[0], mac.bytes[1], - mac.bytes[2], mac.bytes[3], mac.bytes[4], mac.bytes[5]); - return buf; -} - -inline std::string ipv4_to_string(const net::Ipv4Address& ip) { - char buf[16]; - std::snprintf(buf, sizeof(buf), "%u.%u.%u.%u", ip.bytes[0], ip.bytes[1], ip.bytes[2], - ip.bytes[3]); - return buf; -} - -inline std::string tcp_flags_to_string(std::uint8_t flags) { - using namespace net; - std::string out; - if (flags & kTcpSyn) out += 'S'; - if (flags & kTcpAck) out += 'A'; - if (flags & kTcpFin) out += 'F'; - if (flags & kTcpRst) out += 'R'; - if (flags & kTcpPsh) out += 'P'; - if (flags & kTcpUrg) out += 'U'; - return out.empty() ? "-" : out; -} - -// Registered once. DNS (UDP) was the first L7 dissector, proving the -// interface (wireframe/l7/dissector.hpp) is enough to add a protocol -// without touching the L2-L4 decode path; HTTP (TCP) is the second, -// and the first to actually exercise L7Registry's TCP-payload path -- -// 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. 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; -} - -// Tries the destination port first (the common case: a client talking -// to a well-known server port), then the source port (a server's -// reply, coming from that same well-known port). -inline std::optional<std::string> l7_summarize(std::span<const unsigned char> payload, - std::uint16_t src_port, std::uint16_t dst_port) { - if (auto summary = l7_registry().dissect(dst_port, payload)) return summary; - return l7_registry().dissect(src_port, payload); -} - -// IPv4 and IPv6 headers carry different fields (ttl vs. hop_limit, -// 4-byte vs. 16-byte addresses), but everything above the IP layer -- -// TCP/UDP decode plus the L7 lookup - is identical once normalized to -// this. Keeping that dispatch in one place means TCP/UDP/L7 formatting -// can't drift between the two IP versions. -struct IpInfo { - const char* label; // "IPv4" or "IPv6" - std::string src_str; - std::string dst_str; - std::uint8_t ttl_or_hop_limit; - std::uint8_t proto; - std::span<const unsigned char> payload; -}; - -inline std::string summarize_transport_and_above(const IpInfo& info) { - char ip_buf[160]; - std::snprintf(ip_buf, sizeof(ip_buf), " | %s %s -> %s ttl=%u proto=%u", info.label, - info.src_str.c_str(), info.dst_str.c_str(), info.ttl_or_hop_limit, info.proto); - std::string out = ip_buf; - - if (info.proto == net::kProtoTcp) { - if (auto tcp = net::parse_tcp(info.payload)) { - char tcp_buf[128]; - std::snprintf(tcp_buf, sizeof(tcp_buf), " | TCP %u -> %u [%s] seq=%u ack=%u win=%u", - tcp->header.src_port, tcp->header.dst_port, - tcp_flags_to_string(tcp->header.flags).c_str(), tcp->header.seq, - tcp->header.ack, tcp->header.window); - out += tcp_buf; - if (auto l7 = l7_summarize(tcp->payload, tcp->header.src_port, tcp->header.dst_port)) { - out += " | " + *l7; - } - } - } else if (info.proto == net::kProtoUdp) { - if (auto udp = net::parse_udp(info.payload)) { - char udp_buf[64]; - std::snprintf(udp_buf, sizeof(udp_buf), " | UDP %u -> %u len=%u", - udp->header.src_port, udp->header.dst_port, udp->header.length); - out += udp_buf; - if (auto l7 = l7_summarize(udp->payload, udp->header.src_port, udp->header.dst_port)) { - out += " | " + *l7; - } - } - } else if (info.proto == net::kProtoIcmp) { - if (auto icmp = net::parse_icmpv4(info.payload)) { - out += " | ICMP " + net::icmpv4_type_name(icmp->type); - if (icmp->identifier) { - out += " id=" + std::to_string(*icmp->identifier) + - " seq=" + std::to_string(*icmp->sequence); - } - } - } else if (info.proto == net::kNextHeaderIcmpv6) { - if (auto icmp = net::parse_icmpv6(info.payload)) { - out += " | ICMPv6 " + net::icmpv6_type_name(icmp->type); - if (icmp->identifier) { - out += " id=" + std::to_string(*icmp->identifier) + - " seq=" + std::to_string(*icmp->sequence); - } - } else { - out += " | ICMPv6"; // truncated: at least say what it is - } - } - return out; -} - -// `datalink` is the interface's actual pcap_datalink() type, not an -// assumption: tunnel/VPN interfaces (tailscale0, wireguard, plain -// tun/tap) hand libpcap raw IP with no link-layer header at all -// (DLT_RAW), unlike a real NIC or even `lo` (both DLT_EN10MB on -// Linux). Treating raw IP bytes as an Ethernet frame silently produces -// garbage MACs and ethertypes - verified by actually capturing on -// tailscale0 before this branch existed. -// -// IP version is read from the packet itself (the first nibble), not -// inferred from ethertype/datalink: DLT_RAW has no ethertype to key -// off at all, and even on Ethernet this keeps IPv4/IPv6 dispatch in -// one place. -inline std::string summarize_packet(std::span<const unsigned char> bytes, int datalink) { - std::span<const unsigned char> ip_bytes; - std::string out; - - if (datalink == DLT_RAW) { - out = "RAW"; - ip_bytes = bytes; - } else { - auto eth = net::parse_ethernet(bytes); - if (!eth) { - char buf[64]; - std::snprintf(buf, sizeof(buf), "[%zu bytes] truncated ethernet frame", bytes.size()); - return buf; - } - - out = "ETH " + mac_to_string(eth->header.src) + " -> " + mac_to_string(eth->header.dst); - char eth_buf[32]; - std::snprintf(eth_buf, sizeof(eth_buf), " ethertype=0x%04x", eth->header.ethertype); - out += eth_buf; - - if (eth->header.ethertype != net::kEthertypeIPv4 && - eth->header.ethertype != net::kEthertypeIPv6) { - return out; - } - ip_bytes = eth->payload; - } - - if (ip_bytes.empty()) { - out += " | IP (empty payload)"; - return out; - } - std::uint8_t version = static_cast<std::uint8_t>(ip_bytes[0] >> 4); - - if (version == 4) { - auto ip = net::parse_ipv4(ip_bytes); - if (!ip) { - out += " | IPv4 (truncated)"; - 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}); - } else if (version == 6) { - auto ip6 = net::parse_ipv6(ip_bytes); - if (!ip6) { - out += " | IPv6 (truncated)"; - return out; - } - std::string src_str = net::ipv6_to_string(ip6->header.src); - std::string dst_str = net::ipv6_to_string(ip6->header.dst); - - // next_header may name an extension header (Hop-by-Hop, - // Routing, Dest Options, Fragment, AH) rather than the actual - // transport protocol; walk through those to find it. - auto walked = net::walk_ipv6_extension_headers(ip6->header.next_header, ip6->payload); - if (walked.stopped_at_esp) { - char buf[160]; - std::snprintf(buf, sizeof(buf), " | IPv6 %s -> %s ttl=%u proto=%u | ESP (encrypted)", - src_str.c_str(), dst_str.c_str(), ip6->header.hop_limit, - net::kNextHeaderEsp); - out += buf; - } else { - out += summarize_transport_and_above({"IPv6", src_str, dst_str, ip6->header.hop_limit, - walked.final_next_header, walked.payload}); - } - } else { - out += " | IP version " + std::to_string(version) + " (unsupported)"; - } - return out; -} - -// One formatted line per 16 bytes: offset, hex, ASCII gutter. Returned -// as lines rather than printed so both the CLI's -x output and a GUI -// details pane can use the same formatting. -inline std::vector<std::string> hex_dump_lines(std::span<const unsigned char> bytes) { - std::vector<std::string> lines; - for (std::size_t offset = 0; offset < bytes.size(); offset += 16) { - char offset_buf[32]; - std::snprintf(offset_buf, sizeof(offset_buf), "%06zx ", offset); - std::string line = offset_buf; - - std::size_t line_len = std::min<std::size_t>(16, bytes.size() - offset); - for (std::size_t i = 0; i < 16; ++i) { - if (i < line_len) { - char byte_buf[4]; - std::snprintf(byte_buf, sizeof(byte_buf), "%02x ", bytes[offset + i]); - line += byte_buf; - } else { - line += " "; - } - if (i == 7) line += ' '; - } - - line += " |"; - for (std::size_t i = 0; i < line_len; ++i) { - unsigned char c = bytes[offset + i]; - line += (c >= 0x20 && c < 0x7f) ? static_cast<char>(c) : '.'; - } - line += '|'; - - lines.push_back(std::move(line)); - } - return lines; -} - -} // namespace wireframe |