From e0f4c701028aa81026a17cf9ebfb36112184f4bc Mon Sep 17 00:00:00 2001 From: srdusr <99972264+srdusr@users.noreply.github.com> Date: Fri, 17 May 2024 19:54:00 +0200 Subject: Add privilege dropping, AF_PACKET demo, ICMP, checksum validation, --help, and TCP reassembly Rounds out the build order in PLAN.md with six incremental additions: drop root privileges immediately after opening the capture handle; a standalone AF_PACKET/mmap ring-buffer demo (kept separate from CaptureSession, see its header comment for why); ICMPv4/ICMPv6 type and code decoding; opt-in IPv4/TCP/UDP checksum validation (-c); CLI --help; and opt-in, in-order-only TCP stream reassembly (-a) so HTTP requests/responses split across segments can be seen whole. Each addition is unit-tested and, where it touches live traffic behavior, verified against real captured packets - see PLAN.md's Decisions section for the verification notes on each. --- src/main.cpp | 166 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 164 insertions(+), 2 deletions(-) (limited to 'src/main.cpp') diff --git a/src/main.cpp b/src/main.cpp index 3a3e925..31fca73 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -48,6 +48,12 @@ #include #include "wireframe/capture_session.hpp" +#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" #include "wireframe/search.hpp" #include "wireframe/summarize.hpp" @@ -66,11 +72,102 @@ void hex_dump(std::span bytes) { std::printf("\n"); } +// -c only: checksum validation isn't part of summarize_packet()'s +// shared output (see wireframe/net/checksum.hpp for why - checksum +// offload makes it noise, not signal, on most of the interfaces this +// project has actually been tested against). IPv4 only for now; this +// does its own minimal walk down to the IP/TCP/UDP byte spans the +// checksum functions need, reusing the existing decoders rather than +// duplicating their parsing logic. +std::string checksum_status(std::span bytes, int datalink) { + std::span ip_bytes; + if (datalink == DLT_RAW) { + ip_bytes = bytes; + } else { + auto eth = wireframe::net::parse_ethernet(bytes); + if (!eth || eth->header.ethertype != wireframe::net::kEthertypeIPv4) return ""; + ip_bytes = eth->payload; + } + if (ip_bytes.empty() || (ip_bytes[0] >> 4) != 4) return ""; // only IPv4 checksums, for now + + auto ip = wireframe::net::parse_ipv4(ip_bytes); + if (!ip) return ""; + + std::size_t header_len = static_cast(ip->header.ihl) * 4; + std::string out = " checksums: IP="; + out += wireframe::net::verify_ipv4_checksum(ip_bytes.first(header_len)) ? "ok" : "BAD"; + + using wireframe::net::ChecksumResult; + if (ip->header.protocol == wireframe::net::kProtoTcp) { + auto result = + wireframe::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 == wireframe::net::kProtoUdp) { + auto result = + wireframe::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; +} + +// -a only: TCP stream reassembly (wireframe/net/tcp_reassembly.hpp), +// re-run through the same HTTP dissector summarize_packet() already +// uses for a single segment - reassembly only helps when a message is +// actually split across packets, and HTTP is the L7 dissector in this +// project that's structured around lines/headers rather than one fixed +// datagram (DNS/TLS ClientHello are each their own single UDP datagram +// or first TCP segment already). Printed as its own line rather than +// folded into the per-packet summary: it reflects accumulated flow +// state, not just this one packet. In-order-only reassembly (see the +// header's own comment) means this can legitimately fire again on a +// later packet of the same request with an unchanged result once the +// headers are already complete - an honest simplification, not +// deduplicated further. +std::optional reassembled_http_status(std::span bytes, + int datalink, + wireframe::net::TcpReassembler& reassembler) { + std::span ip_bytes; + if (datalink == DLT_RAW) { + ip_bytes = bytes; + } else { + auto eth = wireframe::net::parse_ethernet(bytes); + if (!eth || eth->header.ethertype != wireframe::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 = wireframe::net::parse_ipv4(ip_bytes); + if (!ip || ip->header.protocol != wireframe::net::kProtoTcp) return std::nullopt; + + auto tcp = wireframe::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 = wireframe::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; +} + struct RenderOptions { bool verbose_hex; + bool verbose_checksums; int datalink; wireframe::pcapng::Writer* pcapng_writer; std::string search_term; // display filter - see wireframe/search.hpp + wireframe::net::TcpReassembler* reassembler; // -a only; nullptr means disabled }; void render_packet(const wireframe::CapturedPacket& packet, const RenderOptions& opts) { @@ -88,7 +185,13 @@ void render_packet(const wireframe::CapturedPacket& packet, const RenderOptions& if (!wireframe::matches_search(line, opts.search_term)) return; + if (opts.verbose_checksums) line += checksum_status(bytes, opts.datalink); std::printf("%s\n", line.c_str()); + if (opts.reassembler) { + if (auto status = reassembled_http_status(bytes, opts.datalink, *opts.reassembler)) { + std::printf("%s\n", status->c_str()); + } + } if (opts.verbose_hex) hex_dump(bytes); // Flush per packet: stdout is fully buffered off a tty, and this is @@ -250,17 +353,73 @@ void run_tui(wireframe::CaptureSession& session, wireframe::CaptureQueue& queue, consumer_thread.join(); } +void print_usage(const char* argv0) { + std::printf( + "wireframe - terminal packet capture and analysis tool\n" + "\n" + "Usage: %s [options] [interface]\n" + "\n" + "If no interface is given, the first available device is used.\n" + "\n" + "Options:\n" + " -t, --tui Launch the interactive TUI instead of plain-text output\n" + " -x Show a hex dump under each summary (plain-text mode only)\n" + " -c Show IPv4/TCP/UDP checksum validity (plain-text mode only).\n" + " Off by default: checksum offload means many outbound and\n" + " loopback packets show as invalid even when nothing is\n" + " actually wrong - the NIC computes the real checksum in\n" + " hardware after most capture points already saw the packet.\n" + " -a Reassemble TCP streams and re-run HTTP parsing on the\n" + " joined bytes (plain-text mode only), catching a\n" + " request/response split across multiple segments that\n" + " single-packet HTTP dissection alone would miss. In-order\n" + " segments only - out-of-order/retransmitted segments are\n" + " dropped rather than buffered for reordering.\n" + " -w Write the capture to as pcapng (Wireshark-compatible)\n" + " -r Replay a saved pcapng file instead of a live device\n" + " -f Kernel-level capture filter (tcpdump/BPF syntax); also\n" + " applies to what -w writes. Can't be combined with -r.\n" + " -g Display filter: only show packets whose summary contains\n" + " (case-insensitive). Doesn't affect -w. In TUI mode,\n" + " press '/' to search interactively instead.\n" + " -h, --help Show this help and exit\n" + "\n" + "Examples:\n" + " %s eth0 capture on eth0, print each packet\n" + " %s eth0 -t capture on eth0 in the interactive TUI\n" + " %s eth0 -f \"tcp port 443\" only capture HTTPS traffic\n" + " %s eth0 -w out.pcapng capture and save to out.pcapng\n" + " %s -r out.pcapng -t replay a saved capture in the TUI\n", + argv0, argv0, argv0, argv0, argv0, argv0); +} + } // namespace int main(int argc, char** argv) { + for (int i = 1; i < argc; ++i) { + if (std::strcmp(argv[i], "-h") == 0 || std::strcmp(argv[i], "--help") == 0) { + print_usage(argv[0]); + return 0; + } + } + wireframe::CaptureSessionOptions options; bool tui_mode = false; - RenderOptions opts{ - .verbose_hex = false, .datalink = 0, .pcapng_writer = nullptr, .search_term = ""}; + bool enable_reassembly = false; + RenderOptions opts{.verbose_hex = false, + .verbose_checksums = false, + .datalink = 0, + .pcapng_writer = nullptr, + .search_term = "", + .reassembler = nullptr}; for (int i = 1; i < argc; ++i) { if (std::strcmp(argv[i], "-x") == 0) { opts.verbose_hex = true; + } else if (std::strcmp(argv[i], "-c") == 0) { + opts.verbose_checksums = true; + } else if (std::strcmp(argv[i], "-a") == 0) { + enable_reassembly = true; } else if (std::strcmp(argv[i], "-t") == 0 || std::strcmp(argv[i], "--tui") == 0) { tui_mode = true; } else if (std::strcmp(argv[i], "-w") == 0 && i + 1 < argc) { @@ -285,6 +444,9 @@ int main(int argc, char** argv) { opts.pcapng_writer = session.pcapng_writer(); session.install_signal_handlers(); + wireframe::net::TcpReassembler reassembler; + if (enable_reassembly) opts.reassembler = &reassembler; + if (!tui_mode) { if (session.is_replay()) { std::printf("replaying %s (%s)\n", session.device().c_str(), -- cgit v1.2.3