srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/src/main.cpp
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2024-05-17 19:54:00 +0200
committersrdusr <[email protected]>2024-05-17 19:54:00 +0200
commite0f4c701028aa81026a17cf9ebfb36112184f4bc (patch)
tree31c05e4ccbba0dd2ab4c0567630275ebfc6cd264 /src/main.cpp
parent08332a4195956611db80a2cfe3710d760cbd6acf (diff)
downloadpacketeer-e0f4c701028aa81026a17cf9ebfb36112184f4bc.tar.gz
packeteer-e0f4c701028aa81026a17cf9ebfb36112184f4bc.zip
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.
Diffstat (limited to 'src/main.cpp')
-rw-r--r--src/main.cpp166
1 files changed, 164 insertions, 2 deletions
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(),