diff options
Diffstat (limited to 'src/afpacket_capture.cpp')
| -rw-r--r-- | src/afpacket_capture.cpp | 186 |
1 files changed, 186 insertions, 0 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; +} |