15 KiB
System Architecture
High-Level Overview
┌─────────────────────────────────────────────────────────────┐
│ main.cpp │
│ • arg parsing • thread coordination │
│ • offline vs live • CSV/JSON export on exit │
└───────────┬─────────────────────────────┬───────────────────┘
│ │
┌──────▼──────┐ ┌────────▼────────┐
│ PcapCapture │ │ FTXUI screen │
│ (libpcap) │ │ (main thread) │
└──────┬──────┘ └────────▲────────┘
│ got_packet() │ PostEvent()
│ ┌────────┴────────┐
┌──────▼──────┐ │ application_ │
│ IPv4/IPv6 │ │ thread │
│ (parser) │ │ (UI update loop)│
└──────┬──────┘ └────────▲────────┘
│ Packet │ get_snapshot()
┌──────▼──────────────────────────┐ │
│ Stats │──┘
│ add_packet() push() │
│ transport_map application_map │
│ ip_map pairs packets deque │
│ bandwidth_history │
│ StatsSnapshot (under mutex) │
└─────────────────────────────────┘
Threading Model
There are three concurrent execution contexts:
Capture thread — spawned by PcapCapture::start() at pcapCapture.cpp:80. Runs pcap_loop() which calls callback() → got_packet() for each packet. Calls Stats::add_packet() and Stats::push(). Never touches the UI.
UI update thread (application_thread, main.cpp:107) — runs a loop that:
- Advances the elapsed timer
- Checks stop conditions (time limit, capture finished)
- Calls all
Stats::update_*()methods to rebuild snapshot tables - Calls
view.render(stats.get_snapshot(), ...)to build a new FTXUI element tree - Stores the element in
current_renderunderrender_mtx - Posts a
Customevent to the FTXUI screen to trigger a repaint
FTXUI event loop — runs on the main thread via screen.Loop(component) at main.cpp:139. The Renderer lambda (main.cpp:92) reads current_render under render_mtx and returns it. The CatchEvent lambda handles q and Escape to set ui_running = false and call screen.Exit().
Synchronization Points
| Shared resource | Protector | Access pattern |
|---|---|---|
Stats internal maps |
Stats::mtx |
Capture thread writes; UI thread reads via update_* methods |
StatsSnapshot inside Stats |
Stats::mtx |
Both threads; snapshot is updated in place under lock |
current_render |
render_mtx |
UI thread writes; FTXUI renderer reads |
capture_finished, ui_running |
std::atomic<bool> |
Multiple threads read/write |
timer |
std::atomic<std::chrono::seconds> |
UI thread writes; render lambda reads |
Components
PcapCapture (include/capture/pcapCapture.hpp, src/capture/pcapCapture.cpp)
Wraps the entire libpcap lifecycle. Owns the pcap handle as a unique_ptr<pcap_t, decltype(&pcap_close)> so it's released on destruction regardless of how the object exits.
Key responsibilities:
initialize()— discover all network interfaces viapcap_findalldevs()datalink_type()— detect the link-layer header type and set the byte offset + EtherType extractorstart()— open device in promiscuous mode, compile and install BPF filter, spawn capture threadstart_offline()— open pcap file, process synchronously (no extra thread)got_packet()— parse each raw frame: extract EtherType, constructIPv4orIPv6, buildPacket, forward toStats~PcapCapture()— callsstop(): breaks pcap loop, joins thread, frees filter program and interface list
The C-style pcap_loop callback requires a static function. callback() (line 132) uses the user pointer (which holds this cast to u_char*) to forward to the instance method got_packet().
IP_class / IPv4 / IPv6 (include/packet/IP.hpp, src/packet/IP.cpp)
Polymorphic IP header parser. IP_class is an abstract base declaring pure virtual transport handlers (handle_tcp(), handle_udp(), etc.). Both IPv4 and IPv6 inherit from it.
Parsing is constructor-based: both IPv4(const u_char *data) and IPv6(const u_char *data) accept a pointer to the IP header (already offset past the link-layer header) and complete all parsing in the constructor. After construction, the object exposes only pure accessors: get_source(), get_dest(), get_src_port(), get_dest_port(), get_protocol(), get_payload_len(), get_payload_ptr().
IPv4 transport dispatch: switch(ip_hdr->ip_p) at line 28, dispatching to handle_tcp/udp/icmp/icmpv6/igmp.
IPv6 transport dispatch: a while(true) loop at line 94 that either handles a transport protocol (and returns) or walks past a known extension header and continues.
Packet (include/packet/packet.hpp, src/packet/packet.cpp)
A value type holding everything extracted from a single frame:
ip_version—v4orv6(enumIPVersion)transport_protocol— TCP/UDP/ICMP/ICMP6/IGMP/UNKNOWN (enum classTransportProtocol)application_protocol— HTTP/HTTPS/DNS/SSH/etc (enum classApplicationProtocol)src,dst— IP addresses as stringssrc_port,dst_port— port numberstotal_len— full frame length from pcap headerpayload_len— transport payload lengthpayload_ptr— pointer into pcap's buffer, nulled afterget_application_protocol()runs
The constructor computes application_protocol via get_application_protocol() (packet.cpp:4) then immediately nulls payload_ptr. This prevents callers from dereferencing a pointer that's only valid during the callback.
get_application_protocol() uses payload inspection first (HTTP verbs, TLS record header bytes), then falls back to port-based identification.
Stats (include/stats/protocolStats.hpp, src/stats/protocolStats.cpp)
Thread-safe statistics engine. Internal state:
transport_map—unordered_map<TransportProtocol, protocolStats>application_map—unordered_map<ApplicationProtocol, protocolStats>ip_map—unordered_map<string, IPStats>(per-IP bidirectional counters)pairs—map<pair<string,string>, protocolStats>(per src→dst pair)packets—deque<Packet>(bounded ring of recent packets)snapshot—StatsSnapshot(pre-built display rows, updated byupdate_*methods)bandwidth_history—vector<BandwidthPoint>(time-series)
add_packet() (line 19) takes a lock and updates all raw maps in a single critical section. The update_*() methods take the lock, sort/format the data, and rebuild the corresponding snapshot.*_rows vectors. get_snapshot() returns a copy of the snapshot under the lock.
View (include/TUI/view.hpp, src/TUI/view.cpp)
Stateless FTXUI layout composer. render() (view.cpp:5) builds the full terminal layout from a StatsSnapshot:
┌─────── header ──────────────────────────────────────┐
│ title | interface | filter │ traffic summary │
├─────────────────────────────────────────────────────┤
│ transport table │ app table │ pairs table │ ← hbox, bordered
├──────────────────────────────────────────────────────┤
│ IP table (scrollable) │ bandwidth graph │ ← hbox
├─────────────────────────────────────────────────────┤
│ packets table (right panel, scrollable, width=100) │
├─────────────────────────────────────────────────────┤
│ footer: timer + exit hint │
└─────────────────────────────────────────────────────┘
render_bandwidth() (line 138) defines a GraphFunction — a lambda that receives the graph widget's pixel dimensions and returns a vector<int> mapping each x-pixel to a y-height. It interpolates between the last 50 bandwidth samples and scales by max_bandwidth.
All table sections use ftxui::Table with header row styling (DOUBLE border on row 0, LIGHT on rest).
Filter (include/cli/filter.hpp, src/cli/filter.cpp)
Two-function module:
parse(str) (filter.cpp:5) — splits a key:value string at the first :. Maps known key names (protocol, port, src, dst, ip) to the filter_type enum. Throws std::invalid_argument if no : is present.
get_bpf_filter(filters) (filter.cpp:27) — groups multiple filters by type into a map<filter_type, vector<string>>. Maps user-facing values to BPF syntax (e.g., protocol:dns → port 53, ip:v4 → ip). Combines same-type filters with or, different types with and. Returns the resulting BPF expression string.
argsParser (include/cli/argsParse.hpp, src/cli/argsParse.cpp)
Thin wrapper around Boost.Program_options. Defines all CLI options in the constructor and stores parsed results in a public po::variables_map vm. Options:
| Flag | Default | Description |
|---|---|---|
-i, --interface |
wlan0 |
Network interface |
-c, --count |
0 (unlimited) |
Packet count limit |
--time, -t |
INT_MAX |
Capture duration (seconds) |
-r, --offline |
— | Read from pcap file |
-f, --filter |
— | Filter expressions (composing, multiple) |
-n, --limit |
43 |
Max displayed entries |
--csv / --json |
— | Export paths |
Data Flow
Live Capture
User: just run -i eth0 -f protocol:tcp
↓
main.cpp:33-47 Parse args, build filter vector
main.cpp:49 get_bpf_filter() → "tcp" BPF string
main.cpp:54 capture.set_capabilities(interface, count, "tcp", limit, &stats)
main.cpp:73 capture.start()
↓
pcapCapture.cpp:52-64 pcap_lookupnet → pcap_open_live (promiscuous, SNAP_LEN=1518)
pcapCapture.cpp:64 datalink_type() → set offset + get_ether_type lambda
pcapCapture.cpp:68-75 pcap_compile + pcap_setfilter (BPF "tcp" installed in kernel)
pcapCapture.cpp:80-87 spawn thread → pcap_loop(callback)
↓
[Capture thread: per-packet]
pcapCapture.cpp:132-136 callback() → got_packet()
pcapCapture.cpp:158 get_ether_type(packet) → ETHERTYPE_IP or ETHERTYPE_IPV6
pcapCapture.cpp:162 IPv4 ip(packet + offset) — constructor parses headers
IP.cpp:18-50 extract src/dst, walk to TCP/UDP handler
IP.cpp:52-61 handle_tcp: ports, payload_ptr, payload_len
pcapCapture.cpp:165-168 Packet packetView(...) — constructor runs get_application_protocol()
packet.cpp:4-57 memcmp payload bytes, port-based fallback
payload_ptr = nullptr
pcapCapture.cpp:167 stats->add_packet(packetView) — lock, update all maps
pcapCapture.cpp:168 stats->push(packetView) — lock, push to deque
↓
[UI update thread: every loop iteration]
main.cpp:117-121 update_transport_stats(), update_application_stats(),
update_ip_stats(10), update_pairs(), update_bandwidth()
main.cpp:124-126 view.render(stats.get_snapshot(), ...) → Element
main.cpp:127-130 store in current_render under render_mtx, PostEvent to FTXUI
↓
[FTXUI main thread: on Custom event]
main.cpp:92-95 Renderer lambda reads current_render under render_mtx → display
Offline Analysis
main.cpp:61-70 capture.start_offline(pcap_file) — runs synchronously
pcapCapture.cpp:199-211 pcap_open_offline → pcap_loop (no thread)
[same per-packet flow as above]
main.cpp:64-69 stats.update_packets() + update_application_stats() + ...
↓
main.cpp:86-88 view.render(stats.get_snapshot(), ...) → initial current_render
screen.Loop() FTXUI displays static result (no UI update thread spawned)
Design Decisions
Decision: Constructor-Based Parsing vs Lazy Getters
The original IP_class design had get_protocol() as the entry point to all parsing — a getter with side effects. This creates hidden order dependencies: get_src_port() before get_protocol() returns 0 on IPv4 or garbage on IPv6.
The merged code (visible in the current IPv4/IPv6 constructors) parses everything at construction time. Getters return already-computed values. No ordering requirements for callers.
Decision: StatsSnapshot as Value Type
StatsSnapshot holds the pre-formatted display data as vector<vector<string>> rows. The UI thread calls get_snapshot() which copies this struct out under the mutex. The FTXUI renderer then works from its own copy with no need to hold any lock.
The alternative — having the renderer lock Stats directly — would mean the render mutex and the stats mutex interact, risking deadlock or blocking the capture thread on UI work.
Decision: RAII Handle for pcap_t
pcap_t* is a C resource with pcap_close() as its destructor. Storing it as unique_ptr<pcap_t, decltype(&pcap_close)> means it's released automatically when PcapCapture is destroyed, even if exceptions fly. handle.reset() in stop() explicitly releases it early when the capture ends.
Decision: Offset + Lambda for Link Types
Rather than if-chains scattered across got_packet(), the datalink_type() method sets both the offset integer and the get_ether_type function object once when the device opens. got_packet() stays clean: uint16_t ether_type = get_ether_type(packet).
This makes adding a new link type a single case addition in datalink_type() rather than a change to the hot path.