| Age | Commit message (Collapse) | Author | Files | Lines |
|
Real switches broadcast this every ~30s, but this project had zero
treatment for it (0x88CC was previously the test suite's own example
of an "unhandled ethertype"). Dispatched by ethertype the same way
ARP is, since LLDP sits directly on Ethernet. TLV-encoded; only the
three mandatory TLVs (Chassis ID, Port ID, TTL) plus System Name are
rendered, while every other TLV is still walked over correctly so
nothing after it is lost.
Live-verified two ways, since this machine is on WiFi (LLDP isn't
relayed to wireless clients even when a real switch sends it) with no
LLDP daemon installed to generate traffic locally either: a 15-second
passive capture confirmed no organic LLDP traffic exists to
accidentally rely on, then a real 802.1AB frame was sent via a raw
AF_PACKET socket onto the actual NIC (not fed directly to parse_lldp()
in a unit test) and captured through the full pipeline, decoding
correctly.
|
|
Architecturally different from every other protocol added so far: RTP
has no fixed well-known port at all - it's negotiated per call via
SDP/SIP/WebRTC signaling this project doesn't parse - so L7Registry's
port-keyed dispatch doesn't apply. Handled instead as a fallback tried
only when a UDP packet's normal port-based lookup finds nothing, with
every match labeled "?" (e.g. "RTCP? SR") to mark it as inferred from
packet shape rather than certain - the same honesty Wireshark itself
applies to heuristic dissection, which is off by default there for
exactly this reason.
The two heuristics aren't equally trusted, and the code says so: RTCP
checks a narrow packet-type range (200-204) plus an exact self-declared
length, both unlikely to occur by chance; RTP leans mostly on the 2-bit
version field, since its other structural checks are trivially
satisfied whenever those bits happen to be zero, the common case even
for unrelated traffic. Shipped anyway - a labeled guess on real
RTP/RTCP traffic is more useful than silence - but this is the first
place in the project where a match doesn't mean certainty.
Live-verified against genuine media traffic: ffmpeg streaming a real
RTP video test pattern to loopback, correctly decoded with incrementing
sequence numbers and a consistent SSRC across the stream, plus a real
RTCP Sender Report ffmpeg sent alongside it.
|
|
First dissector needing actual ASN.1 decoding - a small local
tag/length/value reader, not a general ASN.1 decoder, just enough to
walk SNMP's own SEQUENCE/INTEGER/OCTET STRING structure. v3 wraps the
PDU in its own security-parameters header instead of a plain community
string and can be encrypted, so it's reported by version alone, the
same "don't take on real crypto" call already made for TLS/QUIC.
Community strings are shown as-is, matching FTP's PASS precedent --
v1/v2c send them in the clear regardless.
SnmpDissector takes its port in the constructor so it can be
registered twice, at 161 (agent) and 162 (trap receiver). Unlike
DHCP's 67/68, trap traffic never touches 161 on either side (ephemeral
source port straight to 162), so there's no shared port for
l7_summarize()'s dst-then-src fallback to land on - both ports need
explicit registration.
Live-verified against a real snmpd (net-snmp 5.9.5.2) on loopback: a
real snmpget GetRequest/GetResponse exchange decoded correctly with
matching request-ids across both directions, and a real snmptrap
SNMPv2-Trap on port 162 confirmed the second registered port actually
gets used.
|
|
QUIC decodes only what RFC 9000 sends in cleartext at the framing
level: long/short header form, version, long-packet type, and both
connection IDs. Everything past that is encrypted from the first
protected byte onward, even for Initial packets - decrypting that is
real crypto machinery this project deliberately doesn't take on, the
same call already made for TLS's SNI-only extraction.
Caught a real, previously-latent bug while wiring this in, by design
review rather than live-traffic debugging: L7Registry::dissect()
returned on the first dissector whose port() matched, even if that
dissector's summarize() then failed. Harmless while every registered
port was unique, but QUIC is the first protocol here to genuinely
share a well-known port with something already registered (443: TLS
over TCP, QUIC over UDP - the registry has no transport dimension).
Without the fix, tls_dissector would silently claim every port-443
lookup and QUIC would never be reachable. Fixed to try each same-port
dissector until one actually succeeds, locked in with stub-dissector
tests independent of any real protocol's parsing.
Live-verified thoroughly: real HTTP/3 traffic to google.com via
`curl --http3-only`, captured on wlp1s0, correctly decoding the full
connection lifecycle against Google's actual production QUIC
implementation - Initial packets (including a genuine connection ID
migration mid-handshake), Handshake packets, and 1-RTT short-header
packets. This also confirms the L7Registry fix live: without it none
of this would have decoded at all.
|
|
FTP and SMTP share HTTP's line-based response-code-or-command shape
but keep their own command vocabularies in separate files rather than
sharing a parser. FTP passwords are shown as-is, not redacted - FTP
sends them in the clear regardless, matching Wireshark's own behavior.
TFTP is a small binary opcode protocol (RFC 1350) instead. IGMP sits
directly on IP like ICMP, so it's dispatched by protocol number rather
than through the port-keyed L7Registry the other three use.
Live-verified: FTP/SMTP against minimal real TCP servers written for
this (nothing installed locally), a full command/response exchange
decoded correctly in both directions. TFTP against a real atftpd
server and atftp client - the RRQ decoded correctly even though the
transfer itself didn't complete (an atftpd sandbox issue, not this
code). IGMP against real multicast traffic on wlp1s0, including a
genuine query from the actual router.
That live IGMP traffic caught a real bug before it shipped further:
parse_igmp() read bytes[4:8] as a group address for every message
type, but IGMPv3 reports use those bytes for Reserved+RecordCount
instead - a real V3 report showed "group=0.0.0.1" (0 reserved, 1
record, misread as an IP). Fixed by only populating group for the
types where it's genuinely an address; re-verified against the same
live traffic, and a regression test locks in the exact pattern.
|
|
NTP decodes the fixed header's version/mode/stratum - the timestamp
fields need NTP era/fraction fixed-point math to render meaningfully
and add nothing a one-line summary needs, so they're left alone.
DHCP decodes RFC 2131's BOOTP fixed header + magic cookie, then walks
the TLV options bounds-safely for option 53 (message type), plus
yiaddr when the server has assigned one. It's the first dissector
needing two well-known ports (67 server, 68 client) rather than one;
registering at just 67 still matches both directions since
l7_summarize() already falls back from dst_port to src_port.
Verified live: NTP against a real pool.ntp.org query on wlp1s0 (a
genuine stratum-2 reply came back). DHCP over loopback with a
synthetic-but-wire-format-real DISCOVER/OFFER exchange rather than a
real lease renewal, to avoid disrupting this machine's actual network
state - caught a test-setup mistake in the process (both packets
sent from the same ephemeral port instead of the OFFER actually
originating from port 67), not a dissector bug.
|
|
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.
|
|
Decided on the name after weighing alternatives in NAMES.md: packeteer
(packet + -eer, "one who wields packets") fit the project's actual
scope better than the wire/frame pun once it had grown into full
L2-L7 dissection, reassembly, checksums, privilege dropping, and dual
TUI/GUI frontends. No existing packet-capture project uses the name;
the one real-world collision (Packeteer, Inc., a networking company
acquired and folded into Blue Coat/Symantec by 2008) is long defunct.
Mechanical rename throughout: CMake project/target names, the
wireframe:: namespace and include/wireframe/ directory (git mv,
history preserved), every #include path, CLI/GUI help text, and the
project's own working directory. NAMES.md rewritten to record the
decision instead of leaving stale self-referential etymology behind
from the blind rename pass.
Verified after every step: full rebuild (all four targets, no
warnings) and the full test suite (128/128 cases, 366/366 assertions)
both from a fresh reconfigure and again after the directory move.
|
|
GUI parity: checksum_status()/reassembled_http_status() moved out of
main.cpp into a shared wireframe/packet_diagnostics.hpp so the GUI can
show the same -c/-a diagnostics for the selected packet without
duplicating the Ethernet/IPv4/TCP walk. Visually verified under Xvfb
with the same split-segment scenario used to verify -a on the CLI.
Two new L7 dissectors: mDNS (reuses parse_dns outright - RFC 6762
keeps DNS's wire format, just a different port) and SSH's cleartext
identification banner. Live-verified against this machine's real
sshd and a real DNS-wire-format packet sent to port 5353.
Two new fuzz harnesses (fuzz_checksum, fuzz_tcp_reassembly) covering
code added here that the original nine harnesses never
touched. All 12 run clean across ~90M executions with no crashes.
NAMES.md and PLAN.md updated with this round's decisions and naming
candidates.
|
|
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.
|
|
Terminal packet capture and analysis tool built to learn the C++
memory model (byte layout, alignment, endianness, std::span over
unowned buffers) via a real capture pipeline.
- Hand-rolled L2-L4 decoders (Ethernet, IPv4, IPv6 with extension
header walking, TCP, UDP) over std::span, no struct-casting
- L7 dissector interface with DNS, HTTP, and TLS SNI implementations
- pcapng read/write for Wireshark-compatible capture files
- Bounded capture queue: drop-on-backpressure for live capture,
blocking push for faithful file replay
- Kernel-level BPF filtering (-f) and a separate display-only search
(-g / interactive) that doesn't touch what's captured
- Replay mode (-r) reads a saved pcapng file back through the same
pipeline as live capture, no root or live device needed
- pcap_stats() surfaces kernel/interface drops invisible to the
capture queue's own counter
- Three frontends sharing one CaptureSession setup path: CLI, TUI
(FTXUI, primary), GUI (Dear ImGui + SDL3, secondary)
- 89 unit tests (doctest) plus 9 libFuzzer harnesses covering every
hand-rolled parser; fuzzing found and fixed a real OOM in the
pcapng reader (unbounded allocation from an untrusted length field)
|