diff --git a/PROJECTS/beginner/network-traffic-analyzer/cpp/CMakeLists.txt b/PROJECTS/beginner/network-traffic-analyzer/cpp/CMakeLists.txt new file mode 100644 index 00000000..f9070c3d --- /dev/null +++ b/PROJECTS/beginner/network-traffic-analyzer/cpp/CMakeLists.txt @@ -0,0 +1,48 @@ +cmake_minimum_required(VERSION 3.31) +project(network-traffic-analyzer) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +link_libraries(pcap) + +find_package(Boost REQUIRED COMPONENTS program_options) +add_executable(network-traffic-analyzer main.cpp + "include/capture/pcapCapture.hpp" + src/capture/pcapCapture.cpp + "include/cli/argsParse.hpp" + include/packet/packet.hpp + include/packet/IP.hpp + src/packet/IP.cpp + include/stats/protocolStats.hpp + src/stats/protocolStats.cpp + src/packet/packet.cpp + src/cli/argsParse.cpp + include/cli/filter.hpp + src/cli/filter.cpp + include/TUI/view.hpp + src/TUI/view.cpp +) + +include(FetchContent) + +set(FETCHCONTENT_UPDATES_DISCONNECTED ON) +set(FETCHCONTENT_QUIET OFF) + +FetchContent_Declare(ftxui + GIT_REPOSITORY https://github.com/arthursonzogni/ftxui.git + GIT_TAG v5.0.0 + GIT_PROGRESS TRUE + GIT_SHALLOW TRUE + EXCLUDE_FROM_ALL +) +FetchContent_MakeAvailable(ftxui) + + +target_link_libraries(network-traffic-analyzer + Boost::program_options + ftxui::screen + ftxui::dom + ftxui::component +) diff --git a/PROJECTS/beginner/network-traffic-analyzer/cpp/CMakePresets.json b/PROJECTS/beginner/network-traffic-analyzer/cpp/CMakePresets.json new file mode 100644 index 00000000..1c9700f5 --- /dev/null +++ b/PROJECTS/beginner/network-traffic-analyzer/cpp/CMakePresets.json @@ -0,0 +1,22 @@ +{ + "version": 6, + "configurePresets": [ + { + "name": "debug", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/debug", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } + }, + { + "name": "release", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/release", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + } + ] +} diff --git a/PROJECTS/beginner/network-traffic-analyzer/cpp/Justfile b/PROJECTS/beginner/network-traffic-analyzer/cpp/Justfile new file mode 100644 index 00000000..2fe2ca33 --- /dev/null +++ b/PROJECTS/beginner/network-traffic-analyzer/cpp/Justfile @@ -0,0 +1,30 @@ +set shell := ["bash", "-uc"] + +default: + @just --list --unsorted + +build: + cmake -B build/debug -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + cmake --build build/debug + +run *ARGS: + sudo ./build/release/network-traffic-analyzer {{ARGS}} + +run-debug *ARGS: + sudo ./build/debug/network-traffic-analyzer {{ARGS}} + +capture interface="eth0": + sudo ./build/release/network-traffic-analyzer -i {{interface}} + +interfaces: + sudo ./build/release/network-traffic-analyzer --interfaces + +lint: + @sed -i 's/-fdeps-format=p1689r5//g; s/-fmodule-mapper=[^ ]*//g; s/-fmodules-ts//g' build/release/compile_commands.json + clang-tidy -p build/release src/**/*.cpp + +format: + find src include \( -name '*.cpp' -o -name '*.hpp' \) | xargs clang-format -i + +clean: + rm -rf build/ diff --git a/PROJECTS/beginner/network-traffic-analyzer/cpp/README.md b/PROJECTS/beginner/network-traffic-analyzer/cpp/README.md new file mode 100644 index 00000000..4cdd2f9d --- /dev/null +++ b/PROJECTS/beginner/network-traffic-analyzer/cpp/README.md @@ -0,0 +1,90 @@ +```ruby +▄▄▄▄▄▄ ▄▄▄▄ ▄▄▄ ▄▄▄▄▄ ▄▄▄▄▄ ▄▄ ▄▄▄▄ ▄▄▄ ▄▄ ▄▄ ▄▄▄ ▄▄ ▄▄ ▄▄ ▄▄▄▄▄ ▄▄▄▄▄ ▄▄▄▄ + ██ ██▄█▄ ██▀██ ██▄▄ ██▄▄ ██ ██▀▀▀ ██▀██ ███▄██ ██▀██ ██ ▀███▀ ▄█▀ ██▄▄ ██▄█▄ + ██ ██ ██ ██▀██ ██ ██ ██ ▀████ ██▀██ ██ ▀██ ██▀██ ██▄▄▄ █ ▄██▄▄ ██▄▄▄ ██ ██ + +``` + +>A high-performance CLI network analyzer built with libpcap for raw packet capture and FTXUI for a fully interactive terminal UI. +The application captures packets directly from a network interface, parses protocol headers manually, aggregates statistics in real time + +--- +![Preview](example.png) + +> [!IMPORTANT] +> Packet capture requires elevated privileges. + +Run with: + +```bash +sudo ./network-traffic-analyzer +``` + +Or grant capabilities: + +```bash +sudo setcap cap_net_raw,cap_net_admin=eip ./network-traffic-analyzer +``` + +Or you can use `just` command +``` +just run + ``` +--- + +# Features +1) ## Live Packet Capture +- Capture traffic from a selected network interface +- Support for BPF filters (e.g. tcp, port 80, udp) +- Real-time processing using libpcap + +2) ## Real-Time Statistics Engine +- Total packets & traffic volume +- Transport protocol distribution (TCP / UDP / ICMP) +- Application-level classification (port-based) +- Top IP addresses +- Top source > destination pairs + +3) ## Flexible Capture Modes +- Live capture from selected network interface (-i, --interface) +- Offline analysis from .pcap file (-r, --offline) +- Packet count limit (-c) +- Time limit for capture (-t) +- Interface discovery (--interfaces) + +> [!TIP] +> For the complete list of CLI options, use: +> `--help` + +# Technologies +- C++20+ +- Boost::program_options +- libpcap +- FTXUI +- CMake + +# Setup +## 1. clone the repo then +```bash +cd network-traffic-analyzer +./install.sh +``` + +# Usage Example + +### Live capture on eth0 +``` +just capture -i eth0 +``` +### Capture 100 packets +``` +just run -i wlan0 -c 100 +``` +### Analyze offline pcap file +``` +just run --offline traffic.pcap +``` +### Export results (json / csv) +``` +just run --json result.json --csv result.csv +``` diff --git a/PROJECTS/beginner/network-traffic-analyzer/cpp/example.png b/PROJECTS/beginner/network-traffic-analyzer/cpp/example.png new file mode 100644 index 00000000..0cc88ba0 Binary files /dev/null and b/PROJECTS/beginner/network-traffic-analyzer/cpp/example.png differ diff --git a/PROJECTS/beginner/network-traffic-analyzer/cpp/include/TUI/view.hpp b/PROJECTS/beginner/network-traffic-analyzer/cpp/include/TUI/view.hpp new file mode 100644 index 00000000..c6d9fb69 --- /dev/null +++ b/PROJECTS/beginner/network-traffic-analyzer/cpp/include/TUI/view.hpp @@ -0,0 +1,25 @@ +#ifndef VIEW_HPP +#define VIEW_HPP +#include "../stats/protocolStats.hpp" +#include + +class View { + public: + ftxui::Element render(const StatsSnapshot &data, const std::string &interface, const std::string &filter, + bool capture_finished, std::chrono::seconds timer); + + private: + ftxui::Element render_header(const StatsSnapshot &data, const std::string &interface, const std::string &filter); + ftxui::Element render_stats(const StatsSnapshot &data); + + ftxui::Element render_transport(const StatsSnapshot &data); + ftxui::Element render_application(const StatsSnapshot &data); + ftxui::Element render_ip(const StatsSnapshot &data); + ftxui::Element render_pairs(const StatsSnapshot &data); + ftxui::Element render_bandwidth(const StatsSnapshot &data); + ftxui::Element render_packets(const StatsSnapshot &data); + + ftxui::Element render_footer(bool capture_finished, std::chrono::seconds timer); +}; + +#endif // VIEW_HPP diff --git a/PROJECTS/beginner/network-traffic-analyzer/cpp/include/capture/pcapCapture.hpp b/PROJECTS/beginner/network-traffic-analyzer/cpp/include/capture/pcapCapture.hpp new file mode 100644 index 00000000..9ace9b43 --- /dev/null +++ b/PROJECTS/beginner/network-traffic-analyzer/cpp/include/capture/pcapCapture.hpp @@ -0,0 +1,102 @@ +#ifndef PCAPCAPTURE_HPP +#define PCAPCAPTURE_HPP + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#define SNAP_LEN 1518 + +#include "../../include/stats/protocolStats.hpp" +#include "../packet/IP.hpp" +#include "../packet/packet.hpp" + +/** + * Packet capture engine based on libpcap + * + * Supports: + * - Live capture from network interface + * - Offline capture from .pcap file + * - BPF filtering + * - Separate capture thread (for live mode) + * + * Workflow: + * initialize() -> load interfaces + * set_capabilities()-> configure capture parameters + * start() -> start live capture (threaded) + * start_offline() -> process file synchronously + * stop() -> stop capture and cleanup + */ + +class PcapCapture { + private: + /* libpcap error buffer */ + char errbuf[PCAP_ERRBUF_SIZE]; + + /* filter expression (compiled before capture) */ + std::string filter_exp = ""; + /* compiled filter program (expression) */ + struct bpf_program fp = {}; + /* Active pcap handle */ + std::unique_ptr handle{nullptr, &pcap_close}; + void datalink_type(int type); + uint16_t offset = 0; + std::function get_ether_type; + + /* Network mask and IP */ + bpf_u_int32 mask = 0; + bpf_u_int32 net = 0; + + /* Number of packets to capture */ + int num_packets = 0; + + /* Linked list of available interfaces */ + pcap_if_t *interfaces = nullptr; + + /* Selected network interface */ + std::string interface; + /** + * Static wrapper required by C-style libpcap callback. + * + * Since libpcap expects a C function pointer, + * we use a static function and forward the call + * to the class instance. + */ + static void callback(u_char *args, const struct pcap_pkthdr *header, const u_char *packet); + // packet processing logic + void got_packet(const struct pcap_pkthdr *header, const u_char *packet); + + /* Separate thread used for live capture */ + std::thread thread; + std::atomic running{false}; + void stop(); + Stats *stats; + + public: + ~PcapCapture(); + void print_interfaces(); + + bool isRunning() { return running; } + void setRunning(bool running) { this->running = running; } + /* Pointer to statistics engine */ + + void set_capabilities(const std::string &interface, int num_packets, const std::string &filter_exp, + int packets_limit, Stats *stats); + void initialize(); + + void start(); + void start_offline(const std::string &fpath); +}; + +#endif // PCAPCAPTURE_HPP diff --git a/PROJECTS/beginner/network-traffic-analyzer/cpp/include/cli/argsParse.hpp b/PROJECTS/beginner/network-traffic-analyzer/cpp/include/cli/argsParse.hpp new file mode 100644 index 00000000..03b8e742 --- /dev/null +++ b/PROJECTS/beginner/network-traffic-analyzer/cpp/include/cli/argsParse.hpp @@ -0,0 +1,14 @@ +#ifndef ARGSPARSE_HPP +#define ARGSPARSE_HPP +#include + +namespace po = boost::program_options; +struct argsParser { + po::options_description desc; + po::variables_map vm; + void print_help() const; + + argsParser(int argc, char **argv); +}; + +#endif // ARGSPARSE_HPP diff --git a/PROJECTS/beginner/network-traffic-analyzer/cpp/include/cli/filter.hpp b/PROJECTS/beginner/network-traffic-analyzer/cpp/include/cli/filter.hpp new file mode 100644 index 00000000..da42f85e --- /dev/null +++ b/PROJECTS/beginner/network-traffic-analyzer/cpp/include/cli/filter.hpp @@ -0,0 +1,16 @@ +#ifndef FILTER_HPP +#define FILTER_HPP +#include +#include + +enum filter_type { PROTOCOL, PORT, IP_TYPE, IP_SRC, IP_DEST, NONE }; + +struct filter { + filter_type type; + std::string val; +}; + +filter parse(const std::string &str); +std::string get_bpf_filter(const std::vector &f); + +#endif // FILTER_HPP diff --git a/PROJECTS/beginner/network-traffic-analyzer/cpp/include/packet/IP.hpp b/PROJECTS/beginner/network-traffic-analyzer/cpp/include/packet/IP.hpp new file mode 100644 index 00000000..0ac5c288 --- /dev/null +++ b/PROJECTS/beginner/network-traffic-analyzer/cpp/include/packet/IP.hpp @@ -0,0 +1,90 @@ +#ifndef IP_HPP +#define IP_HPP +#include "packet.hpp" +#include +#include +#include +#include + +/* virtual class for our IPv4, IPv6 classes */ +class IP_class { + protected: + virtual void handle_tcp() = 0; + virtual void handle_udp() = 0; + virtual void handle_icmp() = 0; + virtual void handle_icmpv6() = 0; + virtual void handle_igmp() = 0; + + uint16_t payload_len = 0; + TransportProtocol protocol = TransportProtocol::UNKNOWN; + std::string src; + std::string dst; + + public: + std::string get_source(); + std::string get_dest(); + // getters + /*virtual std::string get_source() = 0; + virtual std::string get_dest() = 0;*/ + virtual uint16_t get_src_port() = 0; + virtual uint16_t get_dest_port() = 0; + + TransportProtocol get_protocol() const; + uint16_t get_payload_len() const; + + const uint8_t *payload_ptr = nullptr; + const uint8_t *get_payload_ptr() const { return payload_ptr; } + + virtual ~IP_class() = default; +}; +/*** ip4 ***/ +class IPv4 : public IP_class { + private: + const ip *ip_hdr = nullptr; + int ip_hdr_len = 0; + uint16_t src_port = 0; + uint16_t dest_port = 0; + + protected: + void handle_tcp() override; + void handle_udp() override; + void handle_icmp() override; + void handle_icmpv6() override; + void handle_igmp() override; + + public: + uint16_t get_src_port() override; + uint16_t get_dest_port() override; + + explicit IPv4(const u_char *data); +}; + +/*** ipv6 ***/ +class IPv6 : public IP_class { + protected: + void handle_tcp() override; + void handle_udp() override; + void handle_icmp() override; + void handle_icmpv6() override; + void handle_igmp() override; + + private: + const ip6_hdr *ip_hdr = nullptr; + int ip_hdr_len = 40; + + in6_addr ip_source; + in6_addr ip_dest; + + uint16_t src_port; + uint16_t dest_port; + + const uint8_t *ptr = nullptr; + + public: + explicit IPv6(const u_char *data); + + uint16_t get_src_port() override; + uint16_t get_dest_port() override; +}; + +#endif // IP_HPP diff --git a/PROJECTS/beginner/network-traffic-analyzer/cpp/include/packet/packet.hpp b/PROJECTS/beginner/network-traffic-analyzer/cpp/include/packet/packet.hpp new file mode 100644 index 00000000..80bd8e94 --- /dev/null +++ b/PROJECTS/beginner/network-traffic-analyzer/cpp/include/packet/packet.hpp @@ -0,0 +1,59 @@ +#ifndef PACKET_HPP +#define PACKET_HPP +#include +#include +#include +enum IPVersion { + v4, + v6, +}; + +enum class TransportProtocol { + TCP = 1, + UDP = 2, + ICMP = 3, + ICMP6 = 4, + IGMP = 5, + UNKNOWN = -1, +}; + +enum class ApplicationProtocol { + HTTP, + HTTPS, + DNS, + FTP, + SSH, + SMTP, + QUIC, + NTP, + UNKNOWN, +}; + +struct Packet { + IPVersion ip_version; + TransportProtocol transport_protocol; + ApplicationProtocol application_protocol; + // src address + std::string src; + // dest address + std::string dst; + uint16_t src_port; + uint16_t dst_port; + + uint32_t total_len; + uint16_t payload_len; + + const uint8_t *payload_ptr; + + Packet(IPVersion version, TransportProtocol protocol, std::string src, std::string dst, uint16_t src_port, + uint16_t dst_port, uint32_t total_len, uint16_t payload, const uint8_t *payload_ptr) + : ip_version(version), transport_protocol(protocol), src(std::move(src)), dst(std::move(dst)), + src_port(src_port), dst_port(dst_port), total_len(total_len), payload_len(payload), payload_ptr(payload_ptr) { + application_protocol = get_application_protocol(); + this->payload_ptr = nullptr; + } + + private: + ApplicationProtocol get_application_protocol(); +}; +#endif // PACKET_HPP diff --git a/PROJECTS/beginner/network-traffic-analyzer/cpp/include/stats/protocolStats.hpp b/PROJECTS/beginner/network-traffic-analyzer/cpp/include/stats/protocolStats.hpp new file mode 100644 index 00000000..dd40dad4 --- /dev/null +++ b/PROJECTS/beginner/network-traffic-analyzer/cpp/include/stats/protocolStats.hpp @@ -0,0 +1,113 @@ +#ifndef PROTOCOLSTATS_HPP +#define PROTOCOLSTATS_HPP + +#include "../packet/packet.hpp" +#include "ftxui/dom/elements.hpp" +#include +#include +#include +#include +#include + +struct protocolStats { + uint32_t packets = 0; + uint32_t bytes = 0; +}; + +struct trafficStats { + uint32_t total_packets = 0; + uint32_t total_bytes = 0; +}; + +struct IPStats { + uint32_t bytes_sent = 0; + uint32_t bytes_received = 0; + + uint32_t packets_sent = 0; + uint32_t packets_received = 0; +}; + +struct BandwidthPoint { + double timestamp; + double bytes_per_sec; +}; + +struct StatsSnapshot { + std::vector> transport_rows; + std::vector> app_rows; + std::vector> rows; + std::vector> pairs_rows; + std::vector> packets_rows; + + uint32_t total_p = 0, total_b = 0; + // bandwidth + std::vector bandwidth_history; + double bandwidth = 0; + double max_bandwidth = 0; +}; + +/** + * @brief Thread-safe statistics engine. + * + * Responsible for: + * - Aggregating packet statistics + * - Maintaining protocol distributions + * - Tracking IP traffic + * - Calculating bandwidth + * - Providing snapshot for UI rendering + * + * All write operations are protected by mutex. + */ +class Stats { + private: + std::mutex mtx; + + uint32_t last_b = 0; + + std::chrono::steady_clock::time_point last_tick; + + std::unordered_map transport_map; + std::unordered_map application_map; + std::unordered_map ip_map; + + std::map, protocolStats> pairs; + + std::deque packets; + int limit_packets = 10; + + StatsSnapshot snapshot; + + public: + void push(const Packet &p) { + std::lock_guard lock(mtx); + if (packets.size() > static_cast(limit_packets)) { + packets.pop_front(); + } + packets.push_back(p); + } + + StatsSnapshot get_snapshot() { + std::lock_guard lock(mtx); + return snapshot; + } + void update_bandwidth(); + double smooth_value(size_t i, size_t start); + double smooth_bandwidth = 0.0; + + void set_packets_limit(int limit) { limit_packets = limit; } + + void add_packet(const Packet &packet); + + void update_transport_stats(); + void update_application_stats(); + void update_ip_stats(size_t limit); + void update_pairs(size_t limit = 10); + void update_packets(); + + void export_csv(const std::string &filename); + void export_json(const std::string &filename); + + Stats(); +}; + +#endif // PROTOCOLSTATS_HPP diff --git a/PROJECTS/beginner/network-traffic-analyzer/cpp/install.sh b/PROJECTS/beginner/network-traffic-analyzer/cpp/install.sh new file mode 100644 index 00000000..cb1cbbc7 --- /dev/null +++ b/PROJECTS/beginner/network-traffic-analyzer/cpp/install.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +set -euo pipefail + +install_just() { + if command -v just &>/dev/null; then + echo "==> just already installed, skipping." + return + fi + + echo "==> Installing just..." + if command -v apt &>/dev/null; then + sudo apt install -y just || { + curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin + } + elif command -v dnf &>/dev/null; then + sudo dnf install -y just + elif command -v pacman &>/dev/null; then + sudo pacman -S --needed just + else + curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin + fi +} + +install_deps() { + echo "==> Installing build dependencies..." + if command -v apt &>/dev/null; then + sudo apt install -y libboost-program-options-dev libpcap-dev cmake ninja-build g++ clang-tidy clang-format + elif command -v dnf &>/dev/null; then + sudo dnf install -y boost-devel libpcap-devel cmake ninja-build gcc-c++ clang-tools-extra + elif command -v pacman &>/dev/null; then + sudo pacman -S --needed boost libpcap cmake ninja gcc clang + else + echo "Unsupported package manager. Install manually: boost, libpcap, cmake, ninja, g++, clang-tidy, clang-format" + exit 1 + fi + echo "==> Dependencies ready." +} + +build() { + echo "==> Configuring..." + cmake -B build/release -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + echo "==> Building..." + cmake --build build/release + echo "" + echo "Setup complete! Run 'just' to see available commands." +} + +install_just +install_deps +build diff --git a/PROJECTS/beginner/network-traffic-analyzer/cpp/main.cpp b/PROJECTS/beginner/network-traffic-analyzer/cpp/main.cpp new file mode 100644 index 00000000..e161f318 --- /dev/null +++ b/PROJECTS/beginner/network-traffic-analyzer/cpp/main.cpp @@ -0,0 +1,154 @@ +#include "include/capture/pcapCapture.hpp" +#include "include/cli/filter.hpp" +#include +#include +#include +#include +#include +#include + +#include "include/TUI/view.hpp" +#include "include/cli/argsParse.hpp" + +int main(int argc, char **argv) { + /* initialize stats */ + Stats stats; + /* initialize capture */ + PcapCapture capture; + capture.initialize(); + + /* initializing the command line parser */ + argsParser parser(argc, argv); + + /* check helper flags */ + if (parser.vm.contains("help")) { + parser.print_help(); + return 0; + } + if (parser.vm.contains("interfaces")) { + capture.print_interfaces(); + return 0; + } + /* take the arguments of the flags into variables */ + std::string interface = parser.vm["interface"].as(); + int count = parser.vm["count"].as(); + int limit = parser.vm["limit"].as(); + int time = parser.vm["time"].as(); + std::string filterString = ""; + + /* get a filter, use vector for multiple */ + 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 + " "; + } + } + /* converting the filter to a pcap readable string */ + std::string expression = get_bpf_filter(filters); + + bool isOffline = parser.vm.contains("offline"); + + /* set the flags to capture engine */ + capture.set_capabilities(interface, count, expression, limit, &stats); + + std::atomic capture_finished = false; + std::atomic ui_running = true; + /* if we capture packets offline, we read the file in full, then print the result */ + if (isOffline) { + + capture.start_offline(parser.vm["offline"].as()); + + /* full recalculation of statistics after file processing */ + stats.update_packets(); + stats.update_application_stats(); + stats.update_transport_stats(); + stats.update_ip_stats(10); + stats.update_pairs(); + stats.update_bandwidth(); + } + /* otherwise start live capture */ + else { + capture.start(); + } + + /* UI */ + // Timer + std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now(); + std::atomic timer; + auto screen = ftxui::ScreenInteractive::Fullscreen(); + + /* our class for UI */ + View view; + std::mutex render_mtx; + std::mutex event_mtx; + ftxui::Element current_render = isOffline + ? view.render(stats.get_snapshot(), interface, filterString, true, timer.load()) + : ftxui::text("Starting capture..."); + + std::mutex screen_mtx; + + 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; + }); + std::thread application_thread; + if (!isOffline) { + 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(), interface, filterString, capture_finished, timer.load()); + { + std::lock_guard lock(render_mtx); + current_render = new_frame; + } + if (ui_running) { + screen.PostEvent(ftxui::Event::Custom); + } + + // std::this_thread::sleep_for(std::chrono::milliseconds(500)); + } + }); + } + + screen.Loop(component); + + ui_running = false; + capture_finished = true; + + if (application_thread.joinable()) + application_thread.join(); + + // Export stats if needed + if (parser.vm.contains("csv")) + stats.export_csv(parser.vm["csv"].as()); + if (parser.vm.contains("json")) + stats.export_json(parser.vm["json"].as()); + + return 0; +} diff --git a/PROJECTS/beginner/network-traffic-analyzer/cpp/src/TUI/view.cpp b/PROJECTS/beginner/network-traffic-analyzer/cpp/src/TUI/view.cpp new file mode 100644 index 00000000..90c171c3 --- /dev/null +++ b/PROJECTS/beginner/network-traffic-analyzer/cpp/src/TUI/view.cpp @@ -0,0 +1,188 @@ +#include "../../include/TUI/view.hpp" +#include "ftxui/dom/table.hpp" + +using namespace ftxui; +ftxui::Element View::render(const StatsSnapshot &data, const std::string &interface, const std::string &filter, + bool capture_finished, std::chrono::seconds timer) { + auto header = render_header(data, interface, filter); + + 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 left_panel = vbox({ + transport_section, + separator(), + ip_section, + }) | + flex_grow; + + auto right_panel = render_packets(data) | border | size(WIDTH, EQUAL, 100) | frame | vscroll_indicator; + + auto body = hbox({ + left_panel, + separator(), + right_panel, + }) | + flex; + + auto footer = render_footer(capture_finished, timer); + + return vbox({ + header, + separator(), + body, + separator(), + footer, + }); +} +/** + * @brief Renders top header section. + * + * Displays: + * - Application title + * - Active interface + * - Active filter + * - Traffic summary + */ +ftxui::Element View::render_header(const StatsSnapshot &data, const std::string &interface, const std::string &filter) { + return hbox({ + vbox({ + text("Network Traffic Analyzer") | bold, + text("Interface: " + interface), + text("Filter: " + filter), + }) | flex, + separator(), + render_stats(data) | flex, + }) | + border; +} + +ftxui::Element View::render_stats(const StatsSnapshot &data) { + return vbox({text("=== Traffic summary ===") | bold, text("Total packets: " + std::to_string(data.total_p)), + text(std::format("Total bytes : {:.2f} MB", data.total_b / (1024.0 * 1024.0)))}) | + flex; +} + +ftxui::Element View::render_footer(bool capture_finished, std::chrono::seconds timer) { + return Element({capture_finished + ? text("Capture finished (" + std::format("{}", timer) + "). Press 'q' or Esc to exit.") | + bold | color(Color::Yellow) | center + : text("time: " + std::format("{}", timer) + ". Press 'q' or Esc to exit.") | center | + size(HEIGHT, EQUAL, 1)}); +} +ftxui::Element View::render_transport(const StatsSnapshot &data) { + Table table(data.transport_rows); + table.SelectAll().Border(LIGHT); + + table.SelectRow(0).Decorate(bold); + table.SelectRow(0).SeparatorVertical(LIGHT); + table.SelectRow(0).Decorate(bold); + table.SelectRow(0).Border(DOUBLE); + + return vbox({text("=== Transport protocols === ") | bold, table.Render()}) | flex; +} +ftxui::Element View::render_application(const StatsSnapshot &data) { + Table table(data.app_rows); + table.SelectAll().Border(LIGHT); + + table.SelectRow(0).Decorate(bold); + table.SelectRow(0).SeparatorVertical(LIGHT); + table.SelectRow(0).Decorate(bold); + table.SelectRow(0).Border(DOUBLE); + + return vbox({text("=== Application protocols ===") | bold, table.Render()}) | flex; +} +ftxui::Element View::render_ip(const StatsSnapshot &data) { + + Table table(data.rows); + table.SelectAll().Border(LIGHT); + + table.SelectRow(0).Decorate(bold); + table.SelectRow(0).SeparatorVertical(LIGHT); + table.SelectRow(0).Decorate(bold); + table.SelectRow(0).Border(DOUBLE); + + return vbox({text("=== Top IP addresses ===") | bold, + + table.Render()}) | + flex; +} +ftxui::Element View::render_pairs(const StatsSnapshot &data) { + Table table(data.pairs_rows); + table.SelectAll().Border(LIGHT); + + table.SelectRow(0).Decorate(bold); + table.SelectRow(0).SeparatorVertical(LIGHT); + table.SelectRow(0).Decorate(bold); + table.SelectRow(0).Border(DOUBLE); + + return vbox({text("=== Top communication pairs ===") | bold, table.Render()}) | flex; +} +/** + * @brief Renders bandwidth graph. + * + * Displays last 50 samples. + * Scales dynamically based on max bandwidth. + */ + +ftxui::Element View::render_bandwidth(const StatsSnapshot &data) { + + GraphFunction fn = [this, data](int width, int height) { + std::vector output(width, 0); + + if (data.bandwidth_history.size() < 2) + return output; + + size_t n = data.bandwidth_history.size(); + size_t start = n > 50 ? n - 50 : 0; + + 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; + + double v = bw / max_bw; + output[x] = static_cast(v * (height - 1)); + } + + return output; + }; + return vbox({ + text(std::format(" Bandwidth: {:.2f} KB / max: {:.2f} KB", data.bandwidth, data.max_bandwidth)) | bold, + graph(fn) | size(HEIGHT, EQUAL, 20) | size(WIDTH, EQUAL, 60) | border | color(Color::Green), + }); +} +ftxui::Element View::render_packets(const StatsSnapshot &data) { + Table table(data.packets_rows); + table.SelectAll().Border(LIGHT); + + table.SelectRow(0).Decorate(bold); + table.SelectRow(0).SeparatorVertical(LIGHT); + table.SelectRow(0).Decorate(bold); + table.SelectRow(0).Border(DOUBLE); + return vbox({text("=== Packets ===") | bold, + + table.Render() | flex}) | + flex; +} \ No newline at end of file diff --git a/PROJECTS/beginner/network-traffic-analyzer/cpp/src/capture/pcapCapture.cpp b/PROJECTS/beginner/network-traffic-analyzer/cpp/src/capture/pcapCapture.cpp new file mode 100644 index 00000000..33d541b9 --- /dev/null +++ b/PROJECTS/beginner/network-traffic-analyzer/cpp/src/capture/pcapCapture.cpp @@ -0,0 +1,212 @@ +#include "../../include/capture/pcapCapture.hpp" +#include "../../include/stats/protocolStats.hpp" + +/* get a list of all available network interfaces */ +void PcapCapture::initialize() { + /* find all devs available in network, save them to pcap_if_t struct (interfaces) */ + if (pcap_findalldevs(&interfaces, errbuf) == -1) { + throw std::runtime_error("Error: pcap_findalldevs has been failed"); + /*fprintf(stderr, "Error: pcap_findalldevs has been failed - %s\n", errbuf);*/ + } +} + +void PcapCapture::datalink_type(int type) { + switch (type) { + + case DLT_EN10MB: { + offset = 14; + get_ether_type = [](const u_char *p) { + auto *eth = reinterpret_cast(p); + return ntohs(eth->ether_type); + }; + break; + } + + case DLT_LINUX_SLL: { + offset = 16; + get_ether_type = [](const u_char *p) { return ntohs(*reinterpret_cast(p + 14)); }; + break; + } + + case DLT_LINUX_SLL2: { + offset = 20; + get_ether_type = [](const u_char *p) { return ntohs(*reinterpret_cast(p + 18)); }; + break; + } + default: + throw std::runtime_error("Unsupported datalink type"); + } +} + +/** + * Start live packet capture. + * + * Steps: + * 1. Resolve network mask + * 2. Open device in promiscuous mode + * 3. Compile and apply BPF filter (if provided) + * 4. Start pcap_loop in a separate thread + */ +void PcapCapture::start() { + // getting the netmask of the interface + if (pcap_lookupnet(interface.c_str(), &net, &mask, errbuf) == -1) { + fprintf(stderr, "Couldn't get netmask for device %s: %s\n", interface.c_str(), errbuf); + net = 0; + mask = 0; + } + + /* open capture device */ + handle.reset(pcap_open_live(interface.c_str(), SNAP_LEN, 1, 1000, errbuf)); + if (handle == nullptr) { + throw std::runtime_error("Couldn't open device " + interface + ": " + errbuf); + } + + datalink_type(pcap_datalink(handle.get())); + + if (!filter_exp.empty()) { + /* compile the filter expression */ + if (pcap_compile(handle.get(), &fp, filter_exp.c_str(), 0, net) == -1) { + throw std::runtime_error("Couldn't parse filter " + filter_exp + ": " + pcap_geterr(handle.get())); + } + + /* apply the compiled filter */ + if (pcap_setfilter(handle.get(), &fp) == -1) { + throw std::runtime_error("Couldn't install filter " + filter_exp + ": " + pcap_geterr(handle.get())); + } + } + + /* start a separate thread */ + running = true; + thread = std::thread([this]() { + if (pcap_loop(handle.get(), num_packets, &PcapCapture::callback, reinterpret_cast(this)) < 0) { + // fprintf(stderr, "Error in pcap_loop: %s\n", pcap_geterr(handle)); + // pcap_close(handle); + // throw std::runtime_error("Couldn't start capture"); + } + running = false; + }); +} +PcapCapture::~PcapCapture() { stop(); } +void PcapCapture::stop() { + pcap_freecode(&fp); + if (!handle) + return; + + running = false; + + pcap_breakloop(handle.get()); + + if (thread.joinable()) + thread.join(); + + handle.reset(); + + if (interfaces) { + pcap_freealldevs(interfaces); + interfaces = nullptr; + } +} + +/* print all available interfaces */ +void PcapCapture::print_interfaces() { + int i = 0; + for (pcap_if_t *dev = interfaces; dev; dev = dev->next) { + printf("%d. %s ", ++i, dev->name); + if (dev->description) { + printf("(%s)\n", dev->description); + } else { + printf("\n"); + } + } +} +/** + * @brief Static wrapper required by libpcap C API. + * + * Since libpcap expects a C-style function pointer, + * we forward the call to the current class instance. + * + * @param user Pointer to PcapCapture instance + * @param header Packet metadata + * @param packet Raw packet bytes + */ +void PcapCapture::callback(u_char *user, const struct pcap_pkthdr *header, const u_char *packet) { + auto *self = reinterpret_cast(user); + if (!self->isRunning()) + return; + self->got_packet(header, packet); +} + +/** + * @brief Parses a single captured packet. + * + * Responsibilities: + * - Parse Ethernet header + * - Detect IP version (IPv4 / IPv6) + * - Extract transport & application protocols + * - Construct Packet abstraction + * - Forward packet to Stats engine + * + * Only IPv4 and IPv6 are currently processed. + * Other Ethernet types are ignored. + */ +void PcapCapture::got_packet(const struct pcap_pkthdr *header, const u_char *packet) { + if (!running) + return; + + // --- Ethernet header --- + // const auto* ethernet = reinterpret_cast(packet + offset); + uint16_t ether_type = get_ether_type(packet); + + /* if we have a ipv4 type */ + 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); + } + /* ipv6 type */ + else if (ether_type == ETHERTYPE_IPV6) { + IPv6 ip(packet + offset); + TransportProtocol prot = ip.get_protocol(); + Packet packetView(v6, 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); + } +} + +void PcapCapture::set_capabilities(const std::string &interface, int num_packets, const std::string &filter_exp, + const int packets_limit, Stats *stats) { + this->interface = interface; + this->num_packets = num_packets; + this->filter_exp = filter_exp; + this->stats = stats; + this->stats->set_packets_limit(packets_limit); +} +/** + * @brief Processes packets from an offline .pcap file. + * + * Differences from live mode: + * - Runs synchronously + * - No additional thread is created + * - Blocks until entire file is processed + * + * Used for post-capture analysis and exporting results. + */ +void PcapCapture::start_offline(const std::string &fpath) { + handle.reset(pcap_open_offline(fpath.c_str(), errbuf)); + if (handle == nullptr) { + fprintf(stderr, "Error opening offline file: %s\n", errbuf); + return; + } + datalink_type(pcap_datalink(handle.get())); + + running = true; + + pcap_loop(handle.get(), num_packets, &PcapCapture::callback, reinterpret_cast(this)); + + running = false; +} \ No newline at end of file diff --git a/PROJECTS/beginner/network-traffic-analyzer/cpp/src/cli/argsParse.cpp b/PROJECTS/beginner/network-traffic-analyzer/cpp/src/cli/argsParse.cpp new file mode 100644 index 00000000..413a4af0 --- /dev/null +++ b/PROJECTS/beginner/network-traffic-analyzer/cpp/src/cli/argsParse.cpp @@ -0,0 +1,54 @@ +#include "../../include/cli/argsParse.hpp" +#include + +argsParser::argsParser(int argc, char **argv) { + desc.add_options()("help,h", "Display this help message and exit")("interfaces, interfaces", + "Display all possible interfaces")( + "interface,i", po::value()->default_value("wlan0"), + "Network interface to capture packets from (e.g. eth0, wlan0, any)") + + ("count,c", po::value()->default_value(0), "Number of packets to capture (0 = unlimited)")( + "time, t", po::value()->default_value(INT_MAX), "Working time (in seconds)") + + ("offline,r", po::value(), "Read packets from an offline pcap file") + + ("filter,f", po::value>()->composing(), + "Traffic filter (can be used multiple times)\n" + " proto: tcp | udp | icmp | dns\n" + " src: Source IP address\n" + " dst: Destination IP address\n" + " port: Source or destination port") + + ("sort,s", po::value()->default_value("bytes"), "Sort field: bytes | packets | ip") + + ("order,o", po::value()->default_value("desc"), "Sort order: asc | desc") + + ("limit,n", po::value()->default_value(43), "Limit number of displayed entries") + + ("csv", po::value(), "Export analysis results to CSV file") + + ("json", po::value(), "Export analysis results to JSON file"); + + po::store(po::parse_command_line(argc, argv, desc), vm); + po::notify(vm); +} + +void argsParser::print_help() const { + std::cout << "Network Traffic Analyzer\n" + "========================\n\n" + "Usage:\n" + " ./network-traffic-analyzer [options]\n\n" + "Description:\n" + " Captures and analyzes network traffic from live interfaces or\n" + " offline pcap files. Provides protocol statistics, top talkers,\n" + " and bandwidth usage information.\n\n"; + + std::cout << desc << "\n"; + + std::cout << "Examples:\n" + " ./network-traffic-analyzer -i wlan0 --count 100 --time 10\n" + " ./network-traffic-analyzer -i any --filter port:54\n" + " ./network-traffic-analyzer --offline traffic.pcap --json result.json\n\n"; + + std::cout << "To end the program, press 'q' or Esc to exit.\n"; +} \ No newline at end of file diff --git a/PROJECTS/beginner/network-traffic-analyzer/cpp/src/cli/filter.cpp b/PROJECTS/beginner/network-traffic-analyzer/cpp/src/cli/filter.cpp new file mode 100644 index 00000000..dc85929d --- /dev/null +++ b/PROJECTS/beginner/network-traffic-analyzer/cpp/src/cli/filter.cpp @@ -0,0 +1,97 @@ +#include "../../include/cli/filter.hpp" +#include +#include + +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}; +} + +std::string get_bpf_filter(const std::vector &f) { + 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"); + else if (x.val == "https") + groups[PROTOCOL].emplace_back("port 443"); + else if (x.val == "ssh") + groups[PROTOCOL].emplace_back("port 22"); + else if (x.val == "ftp") + groups[PROTOCOL].emplace_back("port 21"); + else if (x.val == "smtp") + groups[PROTOCOL].emplace_back("port 25"); + else + groups[PROTOCOL].push_back(x.val); + break; + + case IP_DEST: + groups[IP_DEST].push_back("dst host " + x.val); + break; + + case IP_SRC: + groups[IP_SRC].push_back("src host " + x.val); + break; + + case PORT: + groups[PORT].push_back("port " + x.val); + 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" || x.val == "6" || x.val == "ipv6") + groups[IP_TYPE].emplace_back("ip6"); + else + throw std::invalid_argument("Unknown IP type: '" + x.val + "'"); + break; + } + + default: + break; + } + } + + std::string result; + bool first_group = true; + + for (auto &[type, parts] : groups) { + if (!first_group) + result += " and "; + first_group = false; + + 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 += ")"; + } + + return result; +} diff --git a/PROJECTS/beginner/network-traffic-analyzer/cpp/src/packet/IP.cpp b/PROJECTS/beginner/network-traffic-analyzer/cpp/src/packet/IP.cpp new file mode 100644 index 00000000..eb9eaf1f --- /dev/null +++ b/PROJECTS/beginner/network-traffic-analyzer/cpp/src/packet/IP.cpp @@ -0,0 +1,176 @@ +#include "../../include/packet/IP.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +uint16_t IP_class::get_payload_len() const { return payload_len; } + +TransportProtocol IP_class::get_protocol() const { return protocol; } +std::string IP_class::get_source() { return src; } +std::string IP_class::get_dest() { return dst; } + +/*** Ipv4 ***/ +IPv4::IPv4(const u_char *data) { + ip_hdr = reinterpret_cast(data); + + src = inet_ntoa(ip_hdr->ip_src); + dst = inet_ntoa(ip_hdr->ip_dst); + + ip_hdr_len = ip_hdr->ip_hl * 4; + if (ip_hdr_len < 20) { + throw std::runtime_error("Failed to initial IPv4 "); + } + 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; + + case IPPROTO_ICMPV6: + IPv4::handle_icmpv6(); + break; + case IPPROTO_IGMP: + IPv4::handle_igmp(); + break; + default: + protocol = TransportProtocol::UNKNOWN; + break; + } +} + +void IPv4::handle_tcp() { + 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); + + protocol = TransportProtocol::TCP; +} +void IPv4::handle_udp() { + const auto *udp = reinterpret_cast(reinterpret_cast(ip_hdr) + ip_hdr_len); + dest_port = ntohs(udp->dest); + src_port = ntohs(udp->source); + + payload_ptr = reinterpret_cast(udp) + sizeof(udphdr); + payload_len = ntohs(udp->len) - sizeof(udphdr); + + protocol = TransportProtocol::UDP; +} +void IPv4::handle_icmp() { protocol = TransportProtocol::ICMP; } +void IPv4::handle_icmpv6() { protocol = TransportProtocol::ICMP6; } +void IPv4::handle_igmp() { protocol = TransportProtocol::IGMP; } + +uint16_t IPv4::get_src_port() { return src_port; } +uint16_t IPv4::get_dest_port() { return dest_port; } + +/*** Ipv6 ***/ + +IPv6::IPv6(const u_char *data) { + ip_hdr = reinterpret_cast(data); + uint8_t hdr = ip_hdr->ip6_nxt; + std::array src{}; + inet_ntop(AF_INET6, &ip_hdr->ip6_src, src.data(), sizeof(src)); + this->src = src.data(); + + std::array dst{}; + inet_ntop(AF_INET6, &ip_hdr->ip6_dst, dst.data(), sizeof(dst)); + this->dst = dst.data(); + + ptr = reinterpret_cast(ip_hdr + 1); + while (true) { + switch (hdr) { + case IPPROTO_TCP: + IPv6::handle_tcp(); + return; + case IPPROTO_UDP: + IPv6::handle_udp(); + return; + case IPPROTO_ICMP: + IPv6::handle_icmp(); + return; + case IPPROTO_ICMPV6: + IPv6::handle_icmpv6(); + return; + + case IPPROTO_IGMP: + IPv6::handle_igmp(); + return; + /* if we have an extension headers, dont leave from loop, + * keep find a protocol type + */ + 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; + } + } + ptr = nullptr; +} + +void IPv6::handle_tcp() { + const auto tcp = reinterpret_cast(ptr); + dest_port = ntohs(tcp->dest); + src_port = ntohs(tcp->source); + + payload_ptr = reinterpret_cast(tcp) + tcp->doff * 4; + payload_len = ntohs(ip_hdr->ip6_plen) - tcp->doff * 4; + + protocol = TransportProtocol::TCP; + ptr = nullptr; +} +void IPv6::handle_udp() { + const auto udp = reinterpret_cast(ptr); + dest_port = ntohs(udp->dest); + src_port = ntohs(udp->source); + + payload_ptr = reinterpret_cast(udp) + sizeof(udphdr); + payload_len = ntohs(udp->len) - sizeof(udphdr); + + protocol = TransportProtocol::UDP; + ptr = nullptr; +} +void IPv6::handle_icmp() { + protocol = TransportProtocol::ICMP; + ptr = nullptr; +} +void IPv6::handle_icmpv6() { + protocol = TransportProtocol::ICMP6; + payload_len = ntohs(ip_hdr->ip6_plen) - sizeof(icmp6_hdr); + ptr = nullptr; +} +void IPv6::handle_igmp() { + protocol = TransportProtocol::IGMP; + ptr = nullptr; +} + +uint16_t IPv6::get_src_port() { return src_port; } + +uint16_t IPv6::get_dest_port() { return dest_port; } diff --git a/PROJECTS/beginner/network-traffic-analyzer/cpp/src/packet/packet.cpp b/PROJECTS/beginner/network-traffic-analyzer/cpp/src/packet/packet.cpp new file mode 100644 index 00000000..c41711cc --- /dev/null +++ b/PROJECTS/beginner/network-traffic-analyzer/cpp/src/packet/packet.cpp @@ -0,0 +1,57 @@ +#include "../../include/packet/packet.hpp" +#include + +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) || !memcmp(payload_ptr, "HEAD", 4) || + !memcmp(payload_ptr, "PUT ", 4) || !memcmp(payload_ptr, "HTTP", 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; + if (transport_protocol == TransportProtocol::TCP) { + switch (port) { + case 21: + return ApplicationProtocol::FTP; + case 22: + return ApplicationProtocol::SSH; + case 25: + return ApplicationProtocol::SMTP; + case 53: + return ApplicationProtocol::DNS; + case 80: + return ApplicationProtocol::HTTP; + case 443: + return ApplicationProtocol::HTTPS; + default: + return ApplicationProtocol::UNKNOWN; + } + } + + if (transport_protocol == TransportProtocol::UDP) { + switch (port) { + case 53: + return ApplicationProtocol::DNS; + case 443: + return ApplicationProtocol::QUIC; + case 123: + return ApplicationProtocol::NTP; + default: + return ApplicationProtocol::UNKNOWN; + } + } + + return ApplicationProtocol::UNKNOWN; +} diff --git a/PROJECTS/beginner/network-traffic-analyzer/cpp/src/stats/protocolStats.cpp b/PROJECTS/beginner/network-traffic-analyzer/cpp/src/stats/protocolStats.cpp new file mode 100644 index 00000000..0ce92ed8 --- /dev/null +++ b/PROJECTS/beginner/network-traffic-analyzer/cpp/src/stats/protocolStats.cpp @@ -0,0 +1,392 @@ +#include "../../include/stats/protocolStats.hpp" +#include "ftxui/dom/table.hpp" +#include + +Stats::Stats() { last_tick = std::chrono::steady_clock::now(); } +/** + * @brief Aggregates a newly captured packet. + * + * Updates: + * - Total packet/byte counters + * - Transport protocol stats + * - Application protocol stats + * - IP-level statistics + * - Communication pairs + * + * Must be called only from capture thread. + * Protected by mutex. + */ +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; +} + +const char *transport_to_str(TransportProtocol p) { + switch (p) { + case TransportProtocol::TCP: + return "TCP"; + case TransportProtocol::UDP: + return "UDP"; + case TransportProtocol::ICMP: + return "ICMP"; + case TransportProtocol::ICMP6: + return "ICMP6"; + case TransportProtocol::IGMP: + return "IGMP"; + default: + return "UNKNOWN"; + } +} + +const char *app_to_str(ApplicationProtocol p) { + switch (p) { + case ApplicationProtocol::HTTP: + return "HTTP"; + case ApplicationProtocol::HTTPS: + return "HTTPS"; + case ApplicationProtocol::DNS: + return "DNS"; + case ApplicationProtocol::FTP: + return "FTP"; + case ApplicationProtocol::SSH: + return "SSH"; + case ApplicationProtocol::SMTP: + return "SMTP"; + case ApplicationProtocol::QUIC: + return "QUIC"; + case ApplicationProtocol::NTP: + return "NTP"; + default: + return "UNKNOWN"; + } +} + +/** + * @brief Rebuilds transport protocol snapshot table. + * + * Sorts protocols by packet count (descending). + * Calculates percentage relative to total traffic. + * + * Called periodically by UI update thread. + */ + +void Stats::update_transport_stats() { + std::lock_guard lock(mtx); + snapshot.transport_rows.clear(); + snapshot.transport_rows.push_back({"Proto", "Packets", "Bytes", "%"}); + + 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; }); + + for (const auto &[proto, stats] : tps) { + double percent = snapshot.total_b ? stats.bytes * 100.0 / snapshot.total_b : 0.0; + snapshot.transport_rows.push_back({transport_to_str(proto), std::to_string(stats.packets), + std::format("{:.2f}", stats.bytes / (1024.0 * 1024.0)), + std::format("{:.2f}", percent)}); + } +} + +/** + * @brief Rebuilds application protocol snapshot. + * + * Sorted by packet count. + * Percent calculated relative to total bytes. + */ +void Stats::update_application_stats() { + std::lock_guard lock(mtx); + std::vector> apps(application_map.begin(), application_map.end()); + + std::sort(apps.begin(), apps.end(), [](auto &a, auto &b) { return a.second.packets > b.second.packets; }); + + snapshot.app_rows.clear(); + snapshot.app_rows.push_back({"Proto", "Packets", "Bytes (MB)", "%"}); + + for (const auto &[proto, s] : apps) { + double percent = snapshot.total_b ? s.bytes * 100.0 / snapshot.total_b : 0.0; + + snapshot.app_rows.push_back({app_to_str(proto), std::to_string(s.packets), + std::format("{:.2f}", s.bytes / (1024.0 * 1024.0)), + std::format("{:.2f}", percent)}); + } +} + +/** + * @brief Generates snapshot for top IP addresses. + * + * @param limit Maximum number of IPs to display. + * + * Sorted by transmitted packets (descending). + */ +void Stats::update_ip_stats(size_t limit) { + std::lock_guard lock(mtx); + snapshot.rows.clear(); + + snapshot.rows.push_back({"IP Address", "Packets TX", "Packets RX"}); + std::vector> ips(ip_map.begin(), ip_map.end()); + + std::sort(ips.begin(), ips.end(), [](auto &a, auto &b) { return a.second.packets_sent > b.second.packets_sent; }); + + size_t count = 0; + for (const auto &[ip, s] : ips) { + if (count++ >= limit) + break; + snapshot.rows.push_back( + {ip, "TX: " + std::to_string(s.packets_sent), "RX: " + std::to_string(s.packets_received)}); + } +} + +/** + * @brief Builds snapshot of top communication pairs. + * + * @param limit Maximum number of pairs to include. + * + * Sorted by total bytes transferred. + */ + +void Stats::update_pairs(size_t limit) { + std::lock_guard lock(mtx); + std::vector, protocolStats>> vec(pairs.begin(), pairs.end()); + std::sort(vec.begin(), vec.end(), [](auto &a, auto &b) { return a.second.bytes > b.second.bytes; }); + + snapshot.pairs_rows.clear(); + snapshot.pairs_rows.push_back({"Source", "Destination", "bytes received", "%"}); + size_t count = 0; + for (const auto &[pair, s] : vec) { + + if (count++ >= limit) + break; + double percent = snapshot.total_b ? (s.bytes * 100.0 / snapshot.total_b) : 0.0; + snapshot.pairs_rows.push_back({ + pair.first, + pair.second, + std::format("{:.0f}", s.bytes * 1.0), + std::format("{:.2f}", percent), + + }); + } +} + +void Stats::update_packets() { + std::lock_guard lock(mtx); + snapshot.packets_rows.clear(); + snapshot.packets_rows.push_back({"IPVersion", "Transport protocol", "Source", "Destination", "App protocol"}); + + for (auto &packet : packets) { + snapshot.packets_rows.push_back({ + packet.ip_version == IPVersion::v4 ? "IPv4" : "IPv6", + transport_to_str(packet.transport_protocol), + packet.src, + packet.dst, + app_to_str(packet.application_protocol), + + }); + } +} + +double Stats::smooth_value(size_t i, size_t start) { + const int window = 3; + double sum = 0.0; + int count = 0; + + for (int k = -window; k <= window; ++k) { + long idx = (long)i + k; + if (idx >= (long)start && idx < (long)snapshot.bandwidth_history.size()) { + sum += snapshot.bandwidth_history[idx].bytes_per_sec; + count++; + } + } + return count ? sum / count : 0.0; +} +/** + * @brief Calculates current bandwidth (bytes/sec). + * + * Uses: + * - Delta bytes since last tick + * - Time elapsed + * - Exponential smoothing to reduce noise + * + * Stores history for graph rendering. + */ +void Stats::update_bandwidth() { + std::lock_guard lock(mtx); + using namespace std::chrono; + + auto now = steady_clock::now(); + double ts = duration_cast>(now.time_since_epoch()).count(); + 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; // bytes per second + + 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); +} + +/** + * @brief Exports current statistics to CSV file. + * + * Includes: + * - Summary + * - Transport protocols + * - Application protocols + * - IP statistics + * - Bandwidth history + */ + +void Stats::export_csv(const std::string &filename) { + std::lock_guard lock(mtx); + std::ofstream file(filename); + if (!file.is_open()) + return; + + file << "summary\n"; + file << "total_packets,total_bytes,bandwidth\n"; + file << snapshot.total_p << "," << snapshot.total_b << "," << snapshot.bandwidth << "\n\n"; + + // ===== Transport protocols ===== + file << "transport_protocols\n"; + file << "protocol,packets,bytes,percent\n"; + + for (const auto &[proto, s] : transport_map) { + double percent = snapshot.total_b ? (s.bytes * 100.0 / snapshot.total_b) : 0.0; + file << transport_to_str(proto) << "," << s.packets << "," << s.bytes << "," << percent << "\n"; + } + file << "\n"; + + // ===== Application protocols ===== + file << "application_protocols\n"; + file << "protocol,packets,payload_bytes\n"; + + for (const auto &[proto, s] : application_map) { + file << static_cast(proto) << "," << s.packets << "," << s.bytes << "\n"; + } + file << "\n"; + + // ===== IP stats ===== + file << "ip_stats\n"; + file << "ip,packets_sent,packets_received,bytes_sent,bytes_received\n"; + + for (const auto &[ip, s] : ip_map) { + file << ip << "," << s.packets_sent << "," << s.packets_received << "," << s.bytes_sent << "," + << s.bytes_received << "\n"; + } + + // bandwidth + file << "time,bandwidth\n"; + + for (const auto &p : snapshot.bandwidth_history) { + file << p.timestamp << "," << p.bytes_per_sec << "\n"; + } + + file.close(); +} +/** + * @brief Exports statistics to JSON format. + * + * Designed for: + * - External processing + * - Visualization tools + * - Data pipelines + */ +void Stats::export_json(const std::string &filename) { + std::lock_guard lock(mtx); + std::ofstream file(filename); + if (!file.is_open()) + return; + + file << "{\n"; + + // ===== Summary ===== + file << " \"summary\": {\n"; + file << " \"total_packets\": " << snapshot.total_p << ",\n"; + file << " \"total_bytes\": " << snapshot.total_b << ",\n"; + file << " \"bandwidth\": " << snapshot.bandwidth << "\n"; + file << " },\n"; + + // ===== Transport ===== + file << " \"transport\": [\n"; + bool first = true; + for (const auto &[proto, s] : transport_map) { + if (!first) + file << ",\n"; + first = false; + + double percent = snapshot.total_b ? (s.bytes * 100.0 / snapshot.total_b) : 0.0; + + file << " {\n"; + file << " \"protocol\": \"" << transport_to_str(proto) << "\",\n"; + file << " \"packets\": " << s.packets << ",\n"; + file << " \"bytes\": " << s.bytes << ",\n"; + file << " \"percent\": " << percent << "\n"; + file << " }"; + } + file << "\n ],\n"; + + // ===== IP stats ===== + file << " \"top_ips\": [\n"; + first = true; + for (const auto &[ip, s] : ip_map) { + if (!first) + file << ",\n"; + first = false; + + file << " {\n"; + file << " \"ip\": \"" << ip << "\",\n"; + file << " \"packets_sent\": " << s.packets_sent << ",\n"; + file << " \"packets_received\": " << s.packets_received << ",\n"; + file << " \"bytes_sent\": " << s.bytes_sent << ",\n"; + file << " \"bytes_received\": " << s.bytes_received << "\n"; + file << " }"; + } + file << "\n ]\n"; + + file << ",\n \"communication_pairs\": [\n"; + first = true; + + for (const auto &[pair, s] : pairs) { + if (!first) + file << ",\n"; + first = false; + + file << " {\n"; + file << " \"src\": \"" << pair.first << "\",\n"; + file << " \"dst\": \"" << pair.second << "\",\n"; + file << " \"packets\": " << s.packets << ",\n"; + file << " \"bytes\": " << s.bytes << "\n"; + file << " }"; + } + + file << "\n ]"; + + file << "}\n"; + file.close(); +} diff --git a/PROJECTS/beginner/network-traffic-analyzer/LICENSE b/PROJECTS/beginner/network-traffic-analyzer/python/LICENSE similarity index 100% rename from PROJECTS/beginner/network-traffic-analyzer/LICENSE rename to PROJECTS/beginner/network-traffic-analyzer/python/LICENSE diff --git a/PROJECTS/beginner/network-traffic-analyzer/README.md b/PROJECTS/beginner/network-traffic-analyzer/python/README.md similarity index 100% rename from PROJECTS/beginner/network-traffic-analyzer/README.md rename to PROJECTS/beginner/network-traffic-analyzer/python/README.md diff --git a/PROJECTS/beginner/network-traffic-analyzer/justfile b/PROJECTS/beginner/network-traffic-analyzer/python/justfile similarity index 100% rename from PROJECTS/beginner/network-traffic-analyzer/justfile rename to PROJECTS/beginner/network-traffic-analyzer/python/justfile diff --git a/PROJECTS/beginner/network-traffic-analyzer/learn/00-OVERVIEW.md b/PROJECTS/beginner/network-traffic-analyzer/python/learn/00-OVERVIEW.md similarity index 100% rename from PROJECTS/beginner/network-traffic-analyzer/learn/00-OVERVIEW.md rename to PROJECTS/beginner/network-traffic-analyzer/python/learn/00-OVERVIEW.md diff --git a/PROJECTS/beginner/network-traffic-analyzer/learn/01-CONCEPTS.md b/PROJECTS/beginner/network-traffic-analyzer/python/learn/01-CONCEPTS.md similarity index 100% rename from PROJECTS/beginner/network-traffic-analyzer/learn/01-CONCEPTS.md rename to PROJECTS/beginner/network-traffic-analyzer/python/learn/01-CONCEPTS.md diff --git a/PROJECTS/beginner/network-traffic-analyzer/learn/02-ARCHITECTURE.md b/PROJECTS/beginner/network-traffic-analyzer/python/learn/02-ARCHITECTURE.md similarity index 100% rename from PROJECTS/beginner/network-traffic-analyzer/learn/02-ARCHITECTURE.md rename to PROJECTS/beginner/network-traffic-analyzer/python/learn/02-ARCHITECTURE.md diff --git a/PROJECTS/beginner/network-traffic-analyzer/learn/03-IMPLEMENTATION.md b/PROJECTS/beginner/network-traffic-analyzer/python/learn/03-IMPLEMENTATION.md similarity index 100% rename from PROJECTS/beginner/network-traffic-analyzer/learn/03-IMPLEMENTATION.md rename to PROJECTS/beginner/network-traffic-analyzer/python/learn/03-IMPLEMENTATION.md diff --git a/PROJECTS/beginner/network-traffic-analyzer/learn/04-CHALLENGES.md b/PROJECTS/beginner/network-traffic-analyzer/python/learn/04-CHALLENGES.md similarity index 100% rename from PROJECTS/beginner/network-traffic-analyzer/learn/04-CHALLENGES.md rename to PROJECTS/beginner/network-traffic-analyzer/python/learn/04-CHALLENGES.md diff --git a/PROJECTS/beginner/network-traffic-analyzer/pyproject.toml b/PROJECTS/beginner/network-traffic-analyzer/python/pyproject.toml similarity index 100% rename from PROJECTS/beginner/network-traffic-analyzer/pyproject.toml rename to PROJECTS/beginner/network-traffic-analyzer/python/pyproject.toml diff --git a/PROJECTS/beginner/network-traffic-analyzer/src/netanal/__init__.py b/PROJECTS/beginner/network-traffic-analyzer/python/src/netanal/__init__.py similarity index 100% rename from PROJECTS/beginner/network-traffic-analyzer/src/netanal/__init__.py rename to PROJECTS/beginner/network-traffic-analyzer/python/src/netanal/__init__.py diff --git a/PROJECTS/beginner/network-traffic-analyzer/src/netanal/__main__.py b/PROJECTS/beginner/network-traffic-analyzer/python/src/netanal/__main__.py similarity index 100% rename from PROJECTS/beginner/network-traffic-analyzer/src/netanal/__main__.py rename to PROJECTS/beginner/network-traffic-analyzer/python/src/netanal/__main__.py diff --git a/PROJECTS/beginner/network-traffic-analyzer/src/netanal/analyzer.py b/PROJECTS/beginner/network-traffic-analyzer/python/src/netanal/analyzer.py similarity index 100% rename from PROJECTS/beginner/network-traffic-analyzer/src/netanal/analyzer.py rename to PROJECTS/beginner/network-traffic-analyzer/python/src/netanal/analyzer.py diff --git a/PROJECTS/beginner/network-traffic-analyzer/src/netanal/capture.py b/PROJECTS/beginner/network-traffic-analyzer/python/src/netanal/capture.py similarity index 100% rename from PROJECTS/beginner/network-traffic-analyzer/src/netanal/capture.py rename to PROJECTS/beginner/network-traffic-analyzer/python/src/netanal/capture.py diff --git a/PROJECTS/beginner/network-traffic-analyzer/src/netanal/constants.py b/PROJECTS/beginner/network-traffic-analyzer/python/src/netanal/constants.py similarity index 100% rename from PROJECTS/beginner/network-traffic-analyzer/src/netanal/constants.py rename to PROJECTS/beginner/network-traffic-analyzer/python/src/netanal/constants.py diff --git a/PROJECTS/beginner/network-traffic-analyzer/src/netanal/exceptions.py b/PROJECTS/beginner/network-traffic-analyzer/python/src/netanal/exceptions.py similarity index 100% rename from PROJECTS/beginner/network-traffic-analyzer/src/netanal/exceptions.py rename to PROJECTS/beginner/network-traffic-analyzer/python/src/netanal/exceptions.py diff --git a/PROJECTS/beginner/network-traffic-analyzer/src/netanal/export.py b/PROJECTS/beginner/network-traffic-analyzer/python/src/netanal/export.py similarity index 100% rename from PROJECTS/beginner/network-traffic-analyzer/src/netanal/export.py rename to PROJECTS/beginner/network-traffic-analyzer/python/src/netanal/export.py diff --git a/PROJECTS/beginner/network-traffic-analyzer/src/netanal/filters.py b/PROJECTS/beginner/network-traffic-analyzer/python/src/netanal/filters.py similarity index 100% rename from PROJECTS/beginner/network-traffic-analyzer/src/netanal/filters.py rename to PROJECTS/beginner/network-traffic-analyzer/python/src/netanal/filters.py diff --git a/PROJECTS/beginner/network-traffic-analyzer/src/netanal/main.py b/PROJECTS/beginner/network-traffic-analyzer/python/src/netanal/main.py similarity index 100% rename from PROJECTS/beginner/network-traffic-analyzer/src/netanal/main.py rename to PROJECTS/beginner/network-traffic-analyzer/python/src/netanal/main.py diff --git a/PROJECTS/beginner/network-traffic-analyzer/src/netanal/models.py b/PROJECTS/beginner/network-traffic-analyzer/python/src/netanal/models.py similarity index 100% rename from PROJECTS/beginner/network-traffic-analyzer/src/netanal/models.py rename to PROJECTS/beginner/network-traffic-analyzer/python/src/netanal/models.py diff --git a/PROJECTS/beginner/network-traffic-analyzer/src/netanal/output.py b/PROJECTS/beginner/network-traffic-analyzer/python/src/netanal/output.py similarity index 100% rename from PROJECTS/beginner/network-traffic-analyzer/src/netanal/output.py rename to PROJECTS/beginner/network-traffic-analyzer/python/src/netanal/output.py diff --git a/PROJECTS/beginner/network-traffic-analyzer/src/netanal/statistics.py b/PROJECTS/beginner/network-traffic-analyzer/python/src/netanal/statistics.py similarity index 100% rename from PROJECTS/beginner/network-traffic-analyzer/src/netanal/statistics.py rename to PROJECTS/beginner/network-traffic-analyzer/python/src/netanal/statistics.py diff --git a/PROJECTS/beginner/network-traffic-analyzer/src/netanal/visualization.py b/PROJECTS/beginner/network-traffic-analyzer/python/src/netanal/visualization.py similarity index 100% rename from PROJECTS/beginner/network-traffic-analyzer/src/netanal/visualization.py rename to PROJECTS/beginner/network-traffic-analyzer/python/src/netanal/visualization.py diff --git a/PROJECTS/beginner/network-traffic-analyzer/tests/__init__.py b/PROJECTS/beginner/network-traffic-analyzer/python/tests/__init__.py similarity index 100% rename from PROJECTS/beginner/network-traffic-analyzer/tests/__init__.py rename to PROJECTS/beginner/network-traffic-analyzer/python/tests/__init__.py diff --git a/PROJECTS/beginner/network-traffic-analyzer/tests/test_filters.py b/PROJECTS/beginner/network-traffic-analyzer/python/tests/test_filters.py similarity index 100% rename from PROJECTS/beginner/network-traffic-analyzer/tests/test_filters.py rename to PROJECTS/beginner/network-traffic-analyzer/python/tests/test_filters.py diff --git a/PROJECTS/beginner/network-traffic-analyzer/tests/test_models.py b/PROJECTS/beginner/network-traffic-analyzer/python/tests/test_models.py similarity index 100% rename from PROJECTS/beginner/network-traffic-analyzer/tests/test_models.py rename to PROJECTS/beginner/network-traffic-analyzer/python/tests/test_models.py diff --git a/PROJECTS/beginner/network-traffic-analyzer/uv.lock b/PROJECTS/beginner/network-traffic-analyzer/python/uv.lock similarity index 100% rename from PROJECTS/beginner/network-traffic-analyzer/uv.lock rename to PROJECTS/beginner/network-traffic-analyzer/python/uv.lock