Cybersecurity-Projects/PROJECTS/beginner/network-traffic-analyzer/cpp/learn/02-ARCHITECTURE.md

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:

  1. Advances the elapsed timer
  2. Checks stop conditions (time limit, capture finished)
  3. Calls all Stats::update_*() methods to rebuild snapshot tables
  4. Calls view.render(stats.get_snapshot(), ...) to build a new FTXUI element tree
  5. Stores the element in current_render under render_mtx
  6. Posts a Custom event 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 via pcap_findalldevs()
  • datalink_type() — detect the link-layer header type and set the byte offset + EtherType extractor
  • start() — open device in promiscuous mode, compile and install BPF filter, spawn capture thread
  • start_offline() — open pcap file, process synchronously (no extra thread)
  • got_packet() — parse each raw frame: extract EtherType, construct IPv4 or IPv6, build Packet, forward to Stats
  • ~PcapCapture() — calls stop(): 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_versionv4 or v6 (enum IPVersion)
  • transport_protocol — TCP/UDP/ICMP/ICMP6/IGMP/UNKNOWN (enum class TransportProtocol)
  • application_protocol — HTTP/HTTPS/DNS/SSH/etc (enum class ApplicationProtocol)
  • src, dst — IP addresses as strings
  • src_port, dst_port — port numbers
  • total_len — full frame length from pcap header
  • payload_len — transport payload length
  • payload_ptr — pointer into pcap's buffer, nulled after get_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_mapunordered_map<TransportProtocol, protocolStats>
  • application_mapunordered_map<ApplicationProtocol, protocolStats>
  • ip_mapunordered_map<string, IPStats> (per-IP bidirectional counters)
  • pairsmap<pair<string,string>, protocolStats> (per src→dst pair)
  • packetsdeque<Packet> (bounded ring of recent packets)
  • snapshotStatsSnapshot (pre-built display rows, updated by update_* methods)
  • bandwidth_historyvector<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:dnsport 53, ip:v4ip). 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.

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.