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/afpacket_capture.cpp | 186 +++++++++++++++++++++++++++++++++++++++++++++++ src/gui_main.cpp | 30 ++++++++ src/main.cpp | 166 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 380 insertions(+), 2 deletions(-) create mode 100644 src/afpacket_capture.cpp (limited to 'src') diff --git a/src/afpacket_capture.cpp b/src/afpacket_capture.cpp new file mode 100644 index 0000000..877bc88 --- /dev/null +++ b/src/afpacket_capture.cpp @@ -0,0 +1,186 @@ +// AF_PACKET + PACKET_RX_RING: capture without libpcap's internal buffer +// copy, using a memory-mapped ring buffer shared directly with the +// kernel. This demonstrates PLAN.md's originally-listed alternative +// capture backend ("libpcap, or raw AF_PACKET with an mmap'd ring +// buffer to skip libpcap's copies") as a focused, standalone artifact. +// +// Deliberately NOT wired into CaptureSession/the main pipeline: doing +// that would mean reimplementing filtering (SO_ATTACH_FILTER instead +// of pcap_setfilter), kernel stats (raw sockopts instead of +// pcap_stats), and datalink detection (ARPHRD_* mapping instead of +// pcap_datalink) at every one of CaptureSession's already-tested call +// sites - real risk to working, verified functionality for a +// copy-avoidance benefit modern libpcap on Linux already gets much of +// internally. What this file actually explores - a std::span reading +// packet bytes directly out of kernel-shared mapped memory, with zero +// copies between the NIC and this process at all - is a more direct +// exploration of this project's actual point (the C++ memory model) +// than anything routed through libpcap's own abstraction, and doesn't +// need to touch the rest of the tool to demonstrate that. +// +// Linux-only: AF_PACKET is a Linux-specific socket family, unlike the +// portable libpcap path the rest of this project uses. + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "wireframe/privileges.hpp" +#include "wireframe/summarize.hpp" + +namespace { + +// TPACKET_V2: a simpler one-frame-per-slot layout than TPACKET_V3's +// block-batching, still genuinely mmap'd and zero-copy. The right +// complexity level for demonstrating the technique clearly, not for +// maximizing throughput. +constexpr std::size_t kFrameSize = 2048; // room for a max-size Ethernet frame + header + padding +constexpr std::size_t kFramesPerBlock = 2; +constexpr std::size_t kBlockSize = kFrameSize * kFramesPerBlock; // must be a page-size multiple +constexpr std::size_t kBlockCount = 64; +constexpr std::size_t kFrameCount = kFramesPerBlock * kBlockCount; + +std::atomic g_stop{false}; +void handle_stop_signal(int) { g_stop.store(true); } + +} // namespace + +int main(int argc, char** argv) { + if (argc < 2) { + std::fprintf(stderr, "usage: %s \n", argv[0]); + return 1; + } + const char* ifname = argv[1]; + + long page_size = sysconf(_SC_PAGESIZE); + if (page_size <= 0 || kBlockSize % static_cast(page_size) != 0) { + std::fprintf(stderr, + "kBlockSize (%zu) isn't a multiple of this system's page size (%ld) - " + "TPACKET_V2 requires it to be\n", + kBlockSize, page_size); + return 1; + } + + int sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); + if (sock == -1) { + std::fprintf(stderr, "socket(AF_PACKET) failed: %s\n", std::strerror(errno)); + return 1; + } + + int version = TPACKET_V2; + if (setsockopt(sock, SOL_PACKET, PACKET_VERSION, &version, sizeof(version)) == -1) { + std::fprintf(stderr, "setsockopt(PACKET_VERSION) failed: %s\n", std::strerror(errno)); + close(sock); + return 1; + } + + tpacket_req req{}; + req.tp_block_size = kBlockSize; + req.tp_block_nr = kBlockCount; + req.tp_frame_size = kFrameSize; + req.tp_frame_nr = kFrameCount; + if (setsockopt(sock, SOL_PACKET, PACKET_RX_RING, &req, sizeof(req)) == -1) { + std::fprintf(stderr, "setsockopt(PACKET_RX_RING) failed: %s\n", std::strerror(errno)); + close(sock); + return 1; + } + + std::size_t ring_size = req.tp_block_size * req.tp_block_nr; + // This mapping *is* the ring buffer: the kernel writes captured + // frames into these same pages, and every packet read below is a + // pointer straight into this mapping - no read()/recv() call, no + // buffer of our own, no copy of the packet data at any point + // between the NIC and summarize_packet() seeing it. + void* ring = mmap(nullptr, ring_size, PROT_READ | PROT_WRITE, MAP_SHARED, sock, 0); + if (ring == MAP_FAILED) { + std::fprintf(stderr, "mmap failed: %s\n", std::strerror(errno)); + close(sock); + return 1; + } + + unsigned int ifindex = if_nametoindex(ifname); + if (ifindex == 0) { + std::fprintf(stderr, "if_nametoindex(%s) failed: %s\n", ifname, std::strerror(errno)); + munmap(ring, ring_size); + close(sock); + return 1; + } + + sockaddr_ll addr{}; + addr.sll_family = AF_PACKET; + addr.sll_protocol = htons(ETH_P_ALL); + addr.sll_ifindex = static_cast(ifindex); + if (bind(sock, reinterpret_cast(&addr), sizeof(addr)) == -1) { + std::fprintf(stderr, "bind failed: %s\n", std::strerror(errno)); + munmap(ring, ring_size); + close(sock); + return 1; + } + + // Everything CAP_NET_RAW was needed for is done: socket created, + // ring mapped, bound to the interface. Same drop-after-open + // principle as CaptureSession (wireframe/privileges.hpp). + if (auto err = wireframe::drop_privileges_if_root()) { + std::fprintf(stderr, "failed to drop privileges: %s\n", err->c_str()); + munmap(ring, ring_size); + close(sock); + return 1; + } + + std::signal(SIGINT, handle_stop_signal); + std::signal(SIGTERM, handle_stop_signal); + + std::printf( + "capturing on %s via AF_PACKET/mmap ring buffer (%zu frames x %zu bytes, ctrl-c to " + "stop)\n", + ifname, kFrameCount, kFrameSize); + + std::size_t frame_index = 0; + while (!g_stop.load()) { + // The status byte at the start of each slot is how the kernel + // and this process hand a frame back and forth without ever + // copying the packet itself: TP_STATUS_KERNEL means "not + // written yet, keep waiting"; the kernel flips it once a + // packet lands, and only then are these bytes safe to read. + auto* header = reinterpret_cast(static_cast(ring) + + frame_index * kFrameSize); + + if (header->tp_status == TP_STATUS_KERNEL) { + pollfd pfd{}; + pfd.fd = sock; + pfd.events = POLLIN; + poll(&pfd, 1, /*timeout_ms=*/200); // bounded so g_stop is still checked promptly + continue; + } + + // tp_mac is the offset from the start of this header to the + // start of the actual frame data - still inside the same + // mmap'd page, never copied elsewhere. + const auto* packet_start = + reinterpret_cast(header) + header->tp_mac; + std::span bytes(packet_start, header->tp_snaplen); + + std::printf("%s\n", wireframe::summarize_packet(bytes, DLT_EN10MB).c_str()); + std::fflush(stdout); + + // Hand the slot back to the kernel so it can reuse it for a + // future packet - the mirror image of the status flip above. + header->tp_status = TP_STATUS_KERNEL; + frame_index = (frame_index + 1) % kFrameCount; + } + + munmap(ring, ring_size); + close(sock); + return 0; +} diff --git a/src/gui_main.cpp b/src/gui_main.cpp index d5d4518..16ee769 100644 --- a/src/gui_main.cpp +++ b/src/gui_main.cpp @@ -75,9 +75,39 @@ void consumer_loop(wireframe::CaptureSession& session, wireframe::CaptureQueue& } } +void print_usage(const char* argv0) { + std::printf( + "wireframe - terminal packet capture and analysis tool (GUI)\n" + "\n" + "Usage: %s [options] [interface]\n" + "\n" + "If no interface is given, the first available device is used.\n" + "Search is available interactively in the window itself.\n" + "\n" + "Options:\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" + " -h, --help Show this help and exit\n" + "\n" + "Examples:\n" + " %s eth0\n" + " %s eth0 -f \"tcp port 443\"\n" + " %s -r out.pcapng\n", + 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; for (int i = 1; i < argc; ++i) { if (std::strcmp(argv[i], "-w") == 0 && i + 1 < argc) { 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