diff options
| author | srdusr <[email protected]> | 2024-05-28 01:55:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2024-05-28 01:55:00 +0200 |
| commit | f8fc8806401596b08779cf8c88da31408c9b0547 (patch) | |
| tree | 36d873799695bb23ad6bb0989e1b0f05b734041d /include | |
| parent | b565d7d9c47ca1ec5af0effd828431ee96027d60 (diff) | |
| download | packeteer-f8fc8806401596b08779cf8c88da31408c9b0547.tar.gz packeteer-f8fc8806401596b08779cf8c88da31408c9b0547.zip | |
Add ARP decoding and bring the TUI up to -c/-a parity
ARP had zero treatment until now - its ethertype just fell through
summarize_packet's "not IPv4/IPv6" branch, on traffic that appears on
essentially every real LAN capture. Scoped to Ethernet/IPv4 addressing
(the case that covers virtually all real ARP traffic), with tcpdump's
own "who-has X tell Y" / "X is-at Y" phrasing rather than inventing
new wording. Live-verified by flushing this machine's real gateway
ARP entry and capturing the resulting request/reply on wlp1s0.
TUI -c/-a were being parsed into RenderOptions but silently did
nothing: run_tui()'s consumer thread had its own loop that never read
them, unlike plain-text mode's render_packet(). Fixed to match, and
caught a real bug while doing it - pushing up to two rows per packet
(summary + reassembly line) against a single pop_front() would let
the row deque grow past its cap under sustained -a activity; needed a
while loop instead. Verified under tmux against the same split-segment
HTTP scenario used to verify -a on the CLI and GUI.
Diffstat (limited to 'include')
| -rw-r--r-- | include/packeteer/net/arp.hpp | 73 | ||||
| -rw-r--r-- | include/packeteer/summarize.hpp | 29 |
2 files changed, 102 insertions, 0 deletions
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; |