srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/afpacket_capture.cpp186
-rw-r--r--src/gui_main.cpp30
-rw-r--r--src/main.cpp166
3 files changed, 380 insertions, 2 deletions
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 <linux/if_ether.h>
+#include <linux/if_packet.h>
+#include <net/if.h>
+#include <poll.h>
+#include <sys/mman.h>
+#include <sys/socket.h>
+#include <unistd.h>
+
+#include <atomic>
+#include <cerrno>
+#include <csignal>
+#include <cstdio>
+#include <cstring>
+#include <span>
+
+#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<bool> 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 <interface>\n", argv[0]);
+ return 1;
+ }
+ const char* ifname = argv[1];
+
+ long page_size = sysconf(_SC_PAGESIZE);
+ if (page_size <= 0 || kBlockSize % static_cast<std::size_t>(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<int>(ifindex);
+ if (bind(sock, reinterpret_cast<sockaddr*>(&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<tpacket2_hdr*>(static_cast<unsigned char*>(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<const unsigned char*>(header) + header->tp_mac;
+ std::span<const unsigned char> 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 <file> Write the capture to <file> as pcapng (Wireshark-compatible)\n"
+ " -r <file> Replay a saved pcapng file instead of a live device\n"
+ " -f <expr> 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 <ftxui/dom/elements.hpp>
#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<const unsigned char> 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<const unsigned char> bytes, int datalink) {
+ std::span<const unsigned char> 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<std::size_t>(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<std::string> reassembled_http_status(std::span<const unsigned char> bytes,
+ int datalink,
+ wireframe::net::TcpReassembler& reassembler) {
+ std::span<const unsigned char> 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 <file> Write the capture to <file> as pcapng (Wireshark-compatible)\n"
+ " -r <file> Replay a saved pcapng file instead of a live device\n"
+ " -f <expr> Kernel-level capture filter (tcpdump/BPF syntax); also\n"
+ " applies to what -w writes. Can't be combined with -r.\n"
+ " -g <term> Display filter: only show packets whose summary contains\n"
+ " <term> (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(),