# Implementation Walkthrough This document walks through every major component with exact file and line references. Read the source alongside this. ## Entry Point — `main.cpp` ### Initialization (lines 14–18) ```cpp Stats stats; PcapCapture capture; capture.initialize(); argsParser parser(argc, argv); ``` `Stats` default-constructs with all maps empty and `last_tick` set to now. `PcapCapture` initializes its `handle` with `nullptr`. `initialize()` calls `pcap_findalldevs()` to populate the `interfaces` linked list so `--interfaces` can print them. ### Argument Handling (lines 24–47) ```cpp if (parser.vm.contains("help")) { parser.print_help(); return 0; } if (parser.vm.contains("interfaces")) { capture.print_interfaces(); return 0; } ``` Boost's `variables_map::contains()` checks if the flag was passed. Early-exit before any network setup. ```cpp std::vector filters; if (parser.vm.contains("filter")) { auto &f = parser.vm["filter"].as>(); for (auto &x : f) { filters.push_back(parse(x)); filterString += x + " "; } } std::string expression = get_bpf_filter(filters); ``` `--filter` is a composing option — it can appear multiple times and Boost accumulates all values into a `vector`. Each `key:value` string is parsed to a `filter` struct, then `get_bpf_filter()` combines them into a single BPF expression. ### Offline vs Live Path (lines 51–74) ```cpp bool isOffline = parser.vm.contains("offline"); capture.set_capabilities(interface, count, expression, limit, &stats); if (isOffline) { capture.start_offline(parser.vm["offline"].as()); stats.update_packets(); stats.update_application_stats(); // ...all update methods } else { capture.start(); } ``` `set_capabilities()` stores the config inside `PcapCapture` and calls `stats->set_packets_limit(packets_limit)`. For offline mode, `start_offline()` runs synchronously and blocks until the entire file is processed — no threading. All stats are then computed once before the TUI launches. For live mode, `start()` spawns the capture thread and returns immediately. ### FTXUI Setup (lines 78–104) ```cpp auto screen = ftxui::ScreenInteractive::Fullscreen(); View view; std::mutex render_mtx; ftxui::Element current_render = isOffline ? view.render(stats.get_snapshot(), ...) : ftxui::text("Starting capture..."); auto component = ftxui::Renderer([&] { std::lock_guard lock(render_mtx); return current_render; }); component |= ftxui::CatchEvent([&](ftxui::Event e) { if (e == ftxui::Event::Character('q') || e == ftxui::Event::Escape) { ui_running = false; screen.Exit(); return true; } return true; }); ``` `current_render` holds the last-built element. The `Renderer` lambda is called by FTXUI's event loop on every repaint — it just returns whatever's in `current_render`, protected by `render_mtx`. The `CatchEvent` lambda intercepts keyboard events. Returning `true` means "event consumed, don't propagate". Note: `return true` for all events (not just q/Esc) is intentional — it prevents FTXUI's default behavior from processing other keys. ### UI Update Thread (lines 106–137) ```cpp application_thread = std::thread([&] { while (!capture_finished && ui_running) { auto now = std::chrono::steady_clock::now(); timer.store(std::chrono::duration_cast(now - begin)); if (timer.load() >= std::chrono::seconds(time) || !capture.isRunning()) capture_finished = true; stats.update_packets(); stats.update_application_stats(); stats.update_transport_stats(); stats.update_ip_stats(10); stats.update_pairs(); stats.update_bandwidth(); ftxui::Element new_frame = view.render(stats.get_snapshot(), ...); { std::lock_guard lock(render_mtx); current_render = new_frame; } if (ui_running) screen.PostEvent(ftxui::Event::Custom); } }); ``` The loop updates stats, builds a new FTXUI element tree from the snapshot, swaps it into `current_render` (under lock), and tells the FTXUI event loop to repaint. `PostEvent(Custom)` is non-blocking — it enqueues the event and returns. No explicit `sleep_for` in the loop (it's commented out at line 134). The loop runs as fast as the update methods complete, which is bounded by the stats mutex contention. --- ## PcapCapture — `src/capture/pcapCapture.cpp` ### `start()` (lines 50–88) ```cpp handle.reset(pcap_open_live(interface.c_str(), SNAP_LEN, 1, 1000, errbuf)); ``` - `SNAP_LEN = 1518` — maximum bytes to capture per packet (standard Ethernet MTU + headers) - `1` — promiscuous mode on - `1000` — read timeout in milliseconds (how long pcap_loop blocks waiting for packets) ```cpp datalink_type(pcap_datalink(handle.get())); ``` Detects the link type and sets `offset` + `get_ether_type` before any packets arrive. ```cpp if (pcap_compile(handle.get(), &fp, filter_exp.c_str(), 0, net) == -1) throw std::runtime_error(...); if (pcap_setfilter(handle.get(), &fp) == -1) throw std::runtime_error(...); ``` BPF compilation is into `struct bpf_program fp` (stack-allocated). After installation, the kernel runs this BPF program on every incoming frame. ```cpp thread = std::thread([this]() { if (pcap_loop(handle.get(), num_packets, &PcapCapture::callback, reinterpret_cast(this)) < 0) {} running = false; }); ``` `pcap_loop` blocks the thread until `num_packets` are captured (0 = unlimited) or `pcap_breakloop()` is called. `this` is passed as the `user` pointer — the static `callback` function casts it back to `PcapCapture*` to call `got_packet()`. ### `stop()` (lines 90–108) ```cpp pcap_freecode(&fp); // release BPF program memory if (!handle) return; running = false; pcap_breakloop(handle.get()); // signal pcap_loop to exit on next packet if (thread.joinable()) thread.join(); // wait for capture thread to finish handle.reset(); // calls pcap_close via unique_ptr deleter if (interfaces) { pcap_freealldevs(interfaces); interfaces = nullptr; } ``` Called from the destructor. Order matters: break the loop first, then join, then close the handle. ### `got_packet()` (lines 152–179) ```cpp uint16_t ether_type = get_ether_type(packet); if (ether_type == ETHERTYPE_IP) { IPv4 ip(packet + offset); TransportProtocol prot = ip.get_protocol(); Packet packetView(v4, prot, ip.get_source(), ip.get_dest(), ip.get_src_port(), ip.get_dest_port(), header->len, ip.get_payload_len(), ip.get_payload_ptr()); stats->add_packet(packetView); stats->push(packetView); } ``` `packet + offset` skips past the link-layer header (14, 16, or 20 bytes depending on DLT type). `IPv4 ip(packet + offset)` parses all headers in the constructor. `Packet` construction calls `get_application_protocol()` and nulls `payload_ptr`. Then both `add_packet()` (updates all maps) and `push()` (pushes to the recent-packets deque) are called — both take the stats mutex internally. --- ## IP Parsing — `src/packet/IP.cpp` ### IPv4 Constructor (lines 18–50) ```cpp ip_hdr = reinterpret_cast(data); src = inet_ntoa(ip_hdr->ip_src); // converts in_addr to "a.b.c.d" string dst = inet_ntoa(ip_hdr->ip_dst); ip_hdr_len = ip_hdr->ip_hl * 4; // ip_hl is 4-bit field: header length in 32-bit words if (ip_hdr_len < 20) throw std::runtime_error("Failed to initial IPv4 "); ``` `ip_hl * 4`: the Internet Header Length field is 4 bits, measured in 32-bit words. Minimum value is 5 (= 20 bytes, no options). Cast to bytes by multiplying by 4. ```cpp switch (ip_hdr->ip_p) { case IPPROTO_TCP: IPv4::handle_tcp(); break; case IPPROTO_UDP: IPv4::handle_udp(); break; case IPPROTO_ICMP: IPv4::handle_icmp(); break; // ... } ``` `ip_p` is the Protocol field (byte 9 of the IP header). IANA assigns these: 6 = TCP, 17 = UDP, 1 = ICMP. ### IPv4 TCP Handler (lines 52–62) ```cpp const auto *tcp = reinterpret_cast( reinterpret_cast(ip_hdr) + ip_hdr_len ); src_port = ntohs(tcp->source); dest_port = ntohs(tcp->dest); payload_ptr = reinterpret_cast(tcp) + tcp->doff * 4; payload_len = ntohs(ip_hdr->ip_len) - (ip_hdr_len + tcp->doff * 4); ``` `ip_hdr` points to the IP header. Adding `ip_hdr_len` bytes (cast to `u_char*` first for byte arithmetic) lands on the TCP header. `tcp->doff` is the TCP Data Offset: number of 32-bit words in the TCP header. `tcp->doff * 4` gives bytes. Payload starts after the TCP header. `ntohs()` converts from network byte order (big-endian) to host byte order. All multi-byte fields in network protocols are big-endian. ### IPv6 Extension Header Walking (lines 82–136) ```cpp ptr = reinterpret_cast(ip_hdr + 1); // past the 40-byte fixed header while (true) { switch (hdr) { case IPPROTO_TCP: IPv6::handle_tcp(); return; // ... case IPPROTO_HOPOPTS: case IPPROTO_ROUTING: case IPPROTO_DSTOPTS: { const auto *ext = reinterpret_cast(ptr); hdr = ext->ip6e_nxt; ptr += (ext->ip6e_len + 1) * 8; break; } case IPPROTO_FRAGMENT: { const auto *frag = reinterpret_cast(ptr); hdr = frag->ip6f_nxt; ptr += sizeof(ip6_frag); break; } default: protocol = TransportProtocol::UNKNOWN; return; } } ``` `ip_hdr + 1` — pointer arithmetic on `ip6_hdr*` advances by `sizeof(ip6_hdr) = 40` bytes, landing exactly at the first extension header or transport header. `(ext->ip6e_len + 1) * 8` — RFC 2460 formula. `ip6e_len` is the length in 8-byte units not counting the first 8 bytes. So total bytes = `(len + 1) * 8`. --- ## Application Protocol Detection — `src/packet/packet.cpp` ### Two-Phase Identification (lines 4–57) ```cpp ApplicationProtocol Packet::get_application_protocol() { if (!payload_ptr || payload_len < 4) goto check_port; if (transport_protocol == TransportProtocol::TCP) { if (!memcmp(payload_ptr, "GET ", 4) || !memcmp(payload_ptr, "POST", 4) || ...) return ApplicationProtocol::HTTP; } if ((src_port == 53 || dst_port == 53) && payload_len >= 12) return ApplicationProtocol::DNS; if (transport_protocol == TransportProtocol::TCP && payload_len >= 3) { if (payload_ptr[0] == 0x16 && payload_ptr[1] == 0x03) return ApplicationProtocol::HTTPS; } check_port: uint16_t port = (src_port < dst_port) ? src_port : dst_port; // switch on port ... } ``` Phase 1 — payload inspection. `memcmp(payload_ptr, "GET ", 4)` compares the first 4 payload bytes against the literal string. HTTP/1.x requests always start with a verb. TLS records start with `0x16 0x03` (Content-Type=Handshake, Version=3.x). Phase 2 — port fallback via `goto check_port`. Using `goto` to jump past Phase 1 when payload is null or too short. `port = min(src_port, dst_port)` — for client→server connections, the server port is typically the well-known one and will be numerically smaller. --- ## Statistics Engine — `src/stats/protocolStats.cpp` ### `add_packet()` (lines 19–42) ```cpp void Stats::add_packet(const Packet &packet) { std::lock_guard lock(mtx); ++snapshot.total_p; snapshot.total_b += packet.total_len; auto &t = transport_map[packet.transport_protocol]; t.packets++; t.bytes += packet.total_len; auto &a = application_map[packet.application_protocol]; a.packets++; a.bytes += packet.payload_len; ip_map[packet.src].packets_sent++; ip_map[packet.src].bytes_sent += packet.total_len; ip_map[packet.dst].packets_received++; ip_map[packet.dst].bytes_received += packet.total_len; auto key = std::make_pair(packet.src, packet.dst); pairs[key].packets++; pairs[key].bytes += packet.total_len; } ``` `unordered_map::operator[]` default-constructs the value if the key doesn't exist. `protocolStats` has all members zero-initialized by default, so the first packet for any protocol inserts a zero-struct and then increments. Same pattern for `IPStats` and the `pairs` map. Note: `total_p` and `total_b` are updated directly in `snapshot` (not in a separate struct) so they're immediately visible in `get_snapshot()` without an extra `update_*` call. ### `update_transport_stats()` (lines 93–107) ```cpp std::vector> tps(transport_map.begin(), transport_map.end()); std::sort(tps.begin(), tps.end(), [](auto &a, auto &b) { return a.second.packets > b.second.packets; }); ``` Can't sort `unordered_map` in place — copy to a vector first, then sort. The lambda compares by packet count descending. Each row is then formatted with `std::format("{:.2f}", ...)` for MB and percentage values. ### `update_bandwidth()` (lines 230–252) ```cpp auto now = steady_clock::now(); double elapsed = duration_cast>(now - last_tick).count(); if (elapsed >= 1.0) { uint32_t delta_bytes = snapshot.total_b - last_b; snapshot.bandwidth = delta_bytes / elapsed; last_b = snapshot.total_b; last_tick = now; const double alpha = 0.2; smooth_bandwidth = alpha * snapshot.bandwidth + (1.0 - alpha) * smooth_bandwidth; snapshot.bandwidth_history.push_back({ts, smooth_bandwidth}); } snapshot.max_bandwidth = std::max(snapshot.max_bandwidth, snapshot.bandwidth); ``` Only samples once per second (when `elapsed >= 1.0`). `delta_bytes = current_total - last_snapshot_total`. Divided by elapsed seconds = bytes/sec. The EMA with alpha=0.2 smooths out spikes. `max_bandwidth` is updated every call (not just when a second has elapsed) so it tracks the actual peak. --- ## Filter Builder — `src/cli/filter.cpp` ### `parse()` (lines 5–25) ```cpp filter parse(const std::string &str) { auto pos = str.find(':'); if (pos == std::string::npos) throw std::invalid_argument("Invalid filter format: '" + str + "' (expected key:value)"); std::string type = str.substr(0, pos); std::string value = str.substr(pos + 1); if (type == "protocol") return {PROTOCOL, value}; if (type == "port") return {PORT, value}; if (type == "dest") return {IP_DEST, value}; if (type == "src") return {IP_SRC, value}; if (type == "ip") return {IP_TYPE, value}; return {NONE, value}; } ``` `find(':')` returns `string::npos` (max value of `size_t`) if not found. The throw prevents the `npos + 1` unsigned overflow bug that would otherwise make `substr(npos + 1)` return the whole string as the value. ### `get_bpf_filter()` (lines 27–97) ```cpp std::map> groups; for (const auto &x : f) { switch (x.type) { case PROTOCOL: if (x.val == "dns") groups[PROTOCOL].emplace_back("port 53"); else if (x.val == "http") groups[PROTOCOL].emplace_back("port 80"); // ... break; case IP_TYPE: if (x.val == "v4" || x.val == "4" || x.val == "ipv4") groups[IP_TYPE].emplace_back("ip"); else if (x.val == "v6" || ...) groups[IP_TYPE].emplace_back("ip6"); else throw std::invalid_argument("Unknown IP type: '" + x.val + "'"); break; } } // combine: same-type = OR, different types = AND for (auto &[type, parts] : groups) { if (!first_group) result += " and "; if (parts.size() > 1) result += "("; for (size_t i = 0; i < parts.size(); ++i) { result += parts[i]; if (i + 1 < parts.size()) result += " or "; } if (parts.size() > 1) result += ")"; } ``` `std::map` (ordered) ensures deterministic output order regardless of insertion order. The BPF AND/OR combination follows standard network filter semantics: same filter type with multiple values means "match any of these" (OR), while different filter types must all match (AND). Example: `-f protocol:http -f protocol:https -f port:8080` → `(port 80 or port 443 or port 8080)` --- ## TUI Rendering — `src/TUI/view.cpp` ### Layout Composition (lines 5–47) ```cpp auto transport_section = hbox({ render_transport(data) | flex, separator(), render_application(data) | flex, separator(), render_pairs(data) | flex, }) | border; auto ip_section = hbox({ render_ip(data) | border | size(HEIGHT, LESS_THAN, 10) | frame | vscroll_indicator, render_bandwidth(data) | border | flex }); auto right_panel = render_packets(data) | border | size(WIDTH, EQUAL, 100) | frame | vscroll_indicator; ``` FTXUI uses a declarative layout model. `hbox` places elements side by side. `| flex` makes an element expand to fill available space. `| size(HEIGHT, LESS_THAN, 10)` caps height. `| frame | vscroll_indicator` adds scroll support. `separator()` draws a vertical line between elements. ### Bandwidth Graph (lines 138–175) ```cpp GraphFunction fn = [this, data](int width, int height) { std::vector output(width, 0); size_t n = data.bandwidth_history.size(); size_t start = n > 50 ? n - 50 : 0; // last 50 samples double max_bw = 1.0; for (size_t i = start; i < n; ++i) max_bw = std::max(max_bw, data.bandwidth_history[i].bytes_per_sec); for (int x = 0; x < width; ++x) { double t = (double)x / (width - 1); double idx_f = start + t * (n - start - 1); size_t i0 = (size_t)idx_f; size_t i1 = std::min(i0 + 1, n - 1); double frac = idx_f - i0; double bw = data.bandwidth_history[i0].bytes_per_sec * (1.0 - frac) + data.bandwidth_history[i1].bytes_per_sec * frac; output[x] = static_cast(bw / max_bw * (height - 1)); } return output; }; ``` The `GraphFunction` maps `width` screen columns to `height`-bounded integer heights. For each pixel column `x`, it computes a fractional index into the sample array (mapping the screen width to the sample range), interpolates linearly between adjacent samples, then normalizes to the graph height. `max_bw = 1.0` as the floor prevents division by zero when no traffic has been seen yet. ### Table Rendering (lines 83–93, pattern repeated for all tables) ```cpp Table table(data.transport_rows); table.SelectAll().Border(LIGHT); table.SelectRow(0).Decorate(bold); table.SelectRow(0).SeparatorVertical(LIGHT); table.SelectRow(0).Border(DOUBLE); return vbox({text("=== Transport protocols === ") | bold, table.Render()}) | flex; ``` `data.transport_rows` is a `vector>` where row 0 is the header. `SelectAll().Border(LIGHT)` draws light borders around all cells. `SelectRow(0).Border(DOUBLE)` overrides the header row with a double border to visually distinguish it. `SelectRow(0).Decorate(bold)` makes header text bold.