srdusr
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt1
-rw-r--r--PLAN.md38
-rw-r--r--include/packeteer/net/arp.hpp73
-rw-r--r--include/packeteer/summarize.hpp29
-rw-r--r--src/main.cpp21
-rw-r--r--tests/test_arp.cpp89
-rw-r--r--tests/test_summarize.cpp20
7 files changed, 265 insertions, 6 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 0db3e85..0216e4c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -97,6 +97,7 @@ add_executable(packeteer_tests
tests/main.cpp
tests/test_byteio.cpp
tests/test_net.cpp
+ tests/test_arp.cpp
tests/test_ipv6.cpp
tests/test_dns.cpp
tests/test_mdns.cpp
diff --git a/PLAN.md b/PLAN.md
index 20450d1..5c0f2f1 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -368,3 +368,41 @@ None currently open.
pass; summarize's own harness also now exercises the ICMP decode path
added earlier this session and the new mDNS/SSH dissectors, since all
of that lives inside summarize_packet()'s call graph already.
+- Renamed wireframe -> packeteer (packet + -eer). See NAMES.md for the
+ reasoning and the collisions checked before committing to it.
+- ARP (packeteer/net/arp.hpp): the one real-traffic L2 protocol that
+ had zero treatment until now - an ARP frame's ethertype (0x0806)
+ just fell through summarize_packet's "not IPv4/IPv6, stop after the
+ Ethernet line" branch, on traffic that shows up on essentially every
+ real LAN capture. Scoped to Ethernet hardware addresses (hlen=6) and
+ IPv4 protocol addresses (plen=4) - the case that accounts for
+ virtually all real ARP traffic; other combinations still decode the
+ fixed header (hwtype/protype/opcode) without guessing at a different
+ address width. Summary phrasing deliberately matches tcpdump's own
+ "who-has X tell Y" / "X is-at Y" convention rather than inventing new
+ wording, since that phrasing is already how anyone reading ARP
+ traffic expects to see it. Verified against real traffic: flushed
+ this machine's ARP cache entry for its actual default gateway and
+ captured the resulting request/reply on wlp1s0 - "who-has
+ 192.168.1.1 tell 192.168.1.104 (28:39:26:71:cd:dd)" followed by
+ "192.168.1.1 is-at bc:07:1d:ff:54:e9", both addresses matching this
+ machine's real interface/gateway.
+- TUI parity for -c/-a: unlike -x (hex dump, never ported to the TUI),
+ -c/-a were already being parsed into RenderOptions for every mode --
+ main()'s arg parsing doesn't distinguish TUI from plain-text - but
+ run_tui()'s consumer thread had its own separate loop that never
+ read opts.verbose_checksums/opts.reassembler, so the flags silently
+ did nothing in TUI mode despite appearing to be accepted. Fixed by
+ mirroring plain-text render_packet()'s logic: checksum status
+ appended inline to the row, a reassembled-HTTP line pushed as a
+ second row right after, both via the same packeteer::checksum_status/
+ reassembled_http_status() the CLI and GUI already share. Caught a
+ real bug while wiring this in, before it ever ran: pushing up to two
+ rows per packet against a single `if (rows.size() > kMaxRows)
+ pop_front()` would let the row deque grow unboundedly under
+ sustained -a activity, since one pop can't offset two pushes --
+ changed to a while loop. Verified under tmux (capture-pane, not raw
+ pty - see the search-bug methodology note above) against the same
+ split-segment HTTP scenario used to verify -a on the CLI and GUI:
+ both "checksums: IP=ok TCP=..." and "[reassembled request: ... Host:
+ ... (72 bytes so far)]" appeared correctly inline in the packet list.
diff --git a/include/packeteer/net/arp.hpp b/include/packeteer/net/arp.hpp
new file mode 100644
index 0000000..470dc9d
--- /dev/null
+++ b/include/packeteer/net/arp.hpp
@@ -0,0 +1,73 @@
+#pragma once
+
+#include <algorithm>
+#include <cstdint>
+#include <optional>
+#include <span>
+
+#include "packeteer/byteio.hpp"
+#include "packeteer/net/ethernet.hpp"
+#include "packeteer/net/ipv4.hpp"
+
+// RFC 826 ARP, scoped to the case that accounts for essentially all
+// real traffic on a modern LAN: Ethernet hardware addresses (hlen=6)
+// and IPv4 protocol addresses (plen=4). Other hardware/protocol
+// combinations still decode the fixed header (hwtype/protype/opcode),
+// just without sender/target addresses - there's no safe way to guess
+// an address width other than what hardware_len/protocol_len actually
+// say.
+namespace packeteer::net {
+
+inline constexpr std::uint16_t kArpOpRequest = 1;
+inline constexpr std::uint16_t kArpOpReply = 2;
+
+struct ArpHeader {
+ std::uint16_t hardware_type;
+ std::uint16_t protocol_type;
+ std::uint8_t hardware_len;
+ std::uint8_t protocol_len;
+ std::uint16_t opcode;
+};
+
+struct ArpPacket {
+ ArpHeader header;
+ // All four populated together only when hardware_len == 6 and
+ // protocol_len == 4; left as nullopt otherwise.
+ std::optional<MacAddress> sender_mac;
+ std::optional<Ipv4Address> sender_ip;
+ std::optional<MacAddress> target_mac;
+ std::optional<Ipv4Address> target_ip;
+};
+
+inline std::optional<ArpPacket> parse_arp(std::span<const unsigned char> bytes) {
+ if (bytes.size() < 8) return std::nullopt;
+
+ ArpHeader header{};
+ header.hardware_type = read_be16(bytes, 0);
+ header.protocol_type = read_be16(bytes, 2);
+ header.hardware_len = bytes[4];
+ header.protocol_len = bytes[5];
+ header.opcode = read_be16(bytes, 6);
+
+ ArpPacket packet{header, std::nullopt, std::nullopt, std::nullopt, std::nullopt};
+
+ if (header.hardware_len == 6 && header.protocol_len == 4 && bytes.size() >= 28) {
+ MacAddress sender_mac{};
+ std::copy_n(bytes.begin() + 8, 6, sender_mac.bytes.begin());
+ Ipv4Address sender_ip{};
+ std::copy_n(bytes.begin() + 14, 4, sender_ip.bytes.begin());
+ MacAddress target_mac{};
+ std::copy_n(bytes.begin() + 18, 6, target_mac.bytes.begin());
+ Ipv4Address target_ip{};
+ std::copy_n(bytes.begin() + 24, 4, target_ip.bytes.begin());
+
+ packet.sender_mac = sender_mac;
+ packet.sender_ip = sender_ip;
+ packet.target_mac = target_mac;
+ packet.target_ip = target_ip;
+ }
+
+ return packet;
+}
+
+} // namespace packeteer::net
diff --git a/include/packeteer/summarize.hpp b/include/packeteer/summarize.hpp
index 77d9ec3..7ff06a4 100644
--- a/include/packeteer/summarize.hpp
+++ b/include/packeteer/summarize.hpp
@@ -14,6 +14,7 @@
#include "packeteer/l7/mdns.hpp"
#include "packeteer/l7/ssh.hpp"
#include "packeteer/l7/tls.hpp"
+#include "packeteer/net/arp.hpp"
#include "packeteer/net/ethernet.hpp"
#include "packeteer/net/icmp.hpp"
#include "packeteer/net/ipv4.hpp"
@@ -40,6 +41,29 @@ inline std::string ipv4_to_string(const net::Ipv4Address& ip) {
return buf;
}
+// tcpdump's own "who-has X tell Y" / "X is-at Y" phrasing - a
+// deliberate match, not a coincidence, since anyone who's ever read
+// ARP traffic in tcpdump/Wireshark will recognize it instantly. Target
+// hardware/protocol addresses aren't shown even when present: a
+// request's target MAC is unknown by definition (that's what's being
+// asked), and a reply's target is just "whoever asked", which tcpdump
+// itself omits from this line too.
+inline std::string arp_summary(const net::ArpPacket& arp) {
+ if (!arp.sender_ip || !arp.sender_mac || !arp.target_ip) {
+ return "ARP opcode=" + std::to_string(arp.header.opcode) +
+ " (non-Ethernet/IPv4 addressing)";
+ }
+ if (arp.header.opcode == net::kArpOpRequest) {
+ return "ARP who-has " + ipv4_to_string(*arp.target_ip) + " tell " +
+ ipv4_to_string(*arp.sender_ip) + " (" + mac_to_string(*arp.sender_mac) + ")";
+ }
+ if (arp.header.opcode == net::kArpOpReply) {
+ return "ARP " + ipv4_to_string(*arp.sender_ip) + " is-at " + mac_to_string(*arp.sender_mac);
+ }
+ return "ARP opcode=" + std::to_string(arp.header.opcode) + " " +
+ ipv4_to_string(*arp.sender_ip) + " (" + mac_to_string(*arp.sender_mac) + ")";
+}
+
inline std::string tcp_flags_to_string(std::uint8_t flags) {
using namespace net;
std::string out;
@@ -187,6 +211,11 @@ inline std::string summarize_packet(std::span<const unsigned char> bytes, int da
std::snprintf(eth_buf, sizeof(eth_buf), " ethertype=0x%04x", eth->header.ethertype);
out += eth_buf;
+ if (eth->header.ethertype == net::kEthertypeArp) {
+ if (auto arp = net::parse_arp(eth->payload)) out += " | " + arp_summary(*arp);
+ return out;
+ }
+
if (eth->header.ethertype != net::kEthertypeIPv4 &&
eth->header.ethertype != net::kEthertypeIPv6) {
return out;
diff --git a/src/main.cpp b/src/main.cpp
index 82b07a9..833af52 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -152,10 +152,25 @@ void run_tui(packeteer::CaptureSession& session, packeteer::CaptureQueue& queue,
packet->ts_usec, bytes, packet->original_len);
}
+ if (opts.verbose_checksums) {
+ std::string status = packeteer::checksum_status(bytes, opts.datalink);
+ if (!status.empty()) line += " " + status;
+ }
+ std::optional<std::string> reassembled;
+ if (opts.reassembler) {
+ reassembled = packeteer::reassembled_http_status(bytes, opts.datalink,
+ *opts.reassembler);
+ }
+
{
std::lock_guard<std::mutex> lock(state_mutex);
rows.push_back(std::move(line));
- if (rows.size() > kMaxRows) rows.pop_front();
+ if (reassembled) rows.push_back(" [" + *reassembled + "]");
+ // A while loop, not if: up to two rows can be pushed per
+ // packet now (the summary plus an optional reassembly
+ // line), so a single pop_front() would let the deque
+ // grow past kMaxRows under sustained -a activity.
+ while (rows.size() > kMaxRows) rows.pop_front();
++packet_count;
}
screen.PostEvent(Event::Custom);
@@ -275,13 +290,13 @@ void print_usage(const char* argv0) {
"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"
+ " -c Show IPv4/TCP/UDP checksum validity (plain-text and TUI).\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"
+ " joined bytes (plain-text and TUI), 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"
diff --git a/tests/test_arp.cpp b/tests/test_arp.cpp
new file mode 100644
index 0000000..247782b
--- /dev/null
+++ b/tests/test_arp.cpp
@@ -0,0 +1,89 @@
+#include <doctest/doctest.h>
+
+#include <vector>
+
+#include "packeteer/net/arp.hpp"
+
+using namespace packeteer::net;
+
+namespace {
+
+std::vector<unsigned char> arp_request() {
+ return {
+ 0x00, 0x01, // hardware type = Ethernet
+ 0x08, 0x00, // protocol type = IPv4
+ 0x06, // hardware len = 6
+ 0x04, // protocol len = 4
+ 0x00, 0x01, // opcode = request
+ 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0x01, // sender MAC
+ 10, 0, 0, 1, // sender IP
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // target MAC (unknown, all zero)
+ 10, 0, 0, 2, // target IP
+ };
+}
+
+std::vector<unsigned char> arp_reply() {
+ return {
+ 0x00, 0x01,
+ 0x08, 0x00,
+ 0x06,
+ 0x04,
+ 0x00, 0x02, // opcode = reply
+ 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0x02, // sender MAC (the one who was asked)
+ 10, 0, 0, 2, // sender IP
+ 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0x01, // target MAC (the original requester)
+ 10, 0, 0, 1, // target IP
+ };
+}
+
+} // namespace
+
+TEST_CASE("parse_arp decodes a request with Ethernet/IPv4 addressing") {
+ auto arp = parse_arp(arp_request());
+ REQUIRE(arp.has_value());
+ CHECK(arp->header.opcode == kArpOpRequest);
+ REQUIRE(arp->sender_ip.has_value());
+ REQUIRE(arp->sender_mac.has_value());
+ REQUIRE(arp->target_ip.has_value());
+ CHECK(arp->sender_ip->bytes == std::array<unsigned char, 4>{10, 0, 0, 1});
+ CHECK(arp->target_ip->bytes == std::array<unsigned char, 4>{10, 0, 0, 2});
+ CHECK(arp->sender_mac->bytes == std::array<unsigned char, 6>{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0x01});
+}
+
+TEST_CASE("parse_arp decodes a reply") {
+ auto arp = parse_arp(arp_reply());
+ REQUIRE(arp.has_value());
+ CHECK(arp->header.opcode == kArpOpReply);
+ REQUIRE(arp->sender_ip.has_value());
+ CHECK(arp->sender_ip->bytes == std::array<unsigned char, 4>{10, 0, 0, 2});
+}
+
+TEST_CASE("parse_arp rejects a truncated header") {
+ std::vector<unsigned char> bytes(5, 0);
+ CHECK_FALSE(parse_arp(bytes).has_value());
+}
+
+TEST_CASE("parse_arp decodes the header but skips addresses for non-Ethernet/IPv4") {
+ std::vector<unsigned char> bytes = {
+ 0x00, 0x06, // hardware type = IEEE 802 (arbitrary, just not the common case)
+ 0x08, 0x00,
+ 0x08, // hardware len = 8, not 6
+ 0x04,
+ 0x00, 0x01,
+ };
+ auto arp = parse_arp(bytes);
+ REQUIRE(arp.has_value());
+ CHECK(arp->header.hardware_len == 8);
+ CHECK_FALSE(arp->sender_ip.has_value());
+ CHECK_FALSE(arp->sender_mac.has_value());
+}
+
+TEST_CASE("parse_arp treats a header claiming Ethernet/IPv4 but too short to hold it as header-only") {
+ std::vector<unsigned char> bytes = {
+ 0x00, 0x01, 0x08, 0x00, 0x06, 0x04, 0x00, 0x01,
+ 0xaa, 0xbb, 0xcc, // truncated sender MAC
+ };
+ auto arp = parse_arp(bytes);
+ REQUIRE(arp.has_value());
+ CHECK_FALSE(arp->sender_ip.has_value());
+}
diff --git a/tests/test_summarize.cpp b/tests/test_summarize.cpp
index ac6f1c0..27e0dd3 100644
--- a/tests/test_summarize.cpp
+++ b/tests/test_summarize.cpp
@@ -163,13 +163,27 @@ TEST_CASE("summarize_packet reports a truncated Ethernet frame without decoding
CHECK(line == "[10 bytes] truncated ethernet frame");
}
-TEST_CASE("summarize_packet stops after the Ethernet line for a non-IP ethertype") {
+TEST_CASE("summarize_packet stops after the Ethernet line for an unhandled ethertype") {
std::vector<unsigned char> bytes = {
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF,
- 0x08, 0x06, // ARP, not IPv4/IPv6
+ 0x88, 0xCC, // LLDP, not IPv4/IPv6/ARP
};
auto line = packeteer::summarize_packet(bytes, DLT_EN10MB);
- CHECK(line == "ETH aa:bb:cc:dd:ee:ff -> 11:22:33:44:55:66 ethertype=0x0806");
+ CHECK(line == "ETH aa:bb:cc:dd:ee:ff -> 11:22:33:44:55:66 ethertype=0x88cc");
+}
+
+TEST_CASE("summarize_packet decodes an ARP request end to end") {
+ std::vector<unsigned char> bytes = {
+ 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF,
+ 0x08, 0x06, // ARP
+ 0x00, 0x01, 0x08, 0x00, 0x06, 0x04, 0x00, 0x01,
+ 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 10, 0, 0, 1,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 10, 0, 0, 2,
+ };
+ auto line = packeteer::summarize_packet(bytes, DLT_EN10MB);
+ CHECK(line ==
+ "ETH aa:bb:cc:dd:ee:ff -> 11:22:33:44:55:66 ethertype=0x0806 | "
+ "ARP who-has 10.0.0.2 tell 10.0.0.1 (aa:bb:cc:dd:ee:ff)");
}
TEST_CASE("hex_dump_lines produces one line per 16 bytes, with the right byte count") {