From 799d66cf8652d8688768824addb5e7f4f237f3d8 Mon Sep 17 00:00:00 2001 From: Marco Cadetg Date: Thu, 18 Sep 2025 11:46:03 +0200 Subject: [PATCH] feat: Add experimental eBPF support for enhanced socket tracking (#11) * feat: Add experimental eBPF support for enhanced socket tracking - Implement eBPF-based socket tracker for Linux with CO-RE support - Add minimal vmlinux header (5.5KB) instead of full 3.4MB file - Create graceful fallback mechanism to procfs when eBPF unavailable - Add comprehensive eBPF build documentation - Integrate libbpf-rs for eBPF program loading and management - Support both IPv4 and IPv6 socket tracking - Add capability checking for required permissions The eBPF feature is optional and disabled by default. When enabled, it provides faster and more accurate process-to-socket mapping on Linux systems with appropriate permissions. --- .github/workflows/rust.yml | 4 +- Cargo.lock | 319 +++++++++- Cargo.toml | 24 +- EBPF_BUILD.md | 231 +++++++ README.md | 58 +- ROADMAP.md | 28 +- build.rs | 71 +++ src/config.rs | 2 +- src/lib.rs | 9 + src/network/parser.rs | 6 + src/network/platform/linux_ebpf/loader.rs | 184 ++++++ .../platform/linux_ebpf/maps_libbpf.rs | 275 ++++++++ src/network/platform/linux_ebpf/mod.rs | 19 + .../linux_ebpf/programs/socket_tracker.bpf.c | 246 ++++++++ .../linux_ebpf/programs/vmlinux_min.h | 243 ++++++++ .../platform/linux_ebpf/tracker_libbpf.rs | 170 +++++ src/network/platform/linux_enhanced.rs | 590 ++++++++++++++++++ src/network/platform/mod.rs | 48 +- src/network/types.rs | 6 + tests/integration_tests.rs | 42 ++ 20 files changed, 2543 insertions(+), 32 deletions(-) create mode 100644 EBPF_BUILD.md create mode 100644 build.rs create mode 100644 src/lib.rs create mode 100644 src/network/platform/linux_ebpf/loader.rs create mode 100644 src/network/platform/linux_ebpf/maps_libbpf.rs create mode 100644 src/network/platform/linux_ebpf/mod.rs create mode 100644 src/network/platform/linux_ebpf/programs/socket_tracker.bpf.c create mode 100644 src/network/platform/linux_ebpf/programs/vmlinux_min.h create mode 100644 src/network/platform/linux_ebpf/tracker_libbpf.rs create mode 100644 src/network/platform/linux_enhanced.rs create mode 100644 tests/integration_tests.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 873376c..5afe623 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -29,8 +29,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Install libpcap - run: sudo apt-get update && sudo apt-get install -y libpcap-dev + - name: Install dependencies + run: sudo apt-get update && sudo apt-get install -y libpcap-dev libelf-dev clang llvm - name: Build run: cargo build --verbose - name: Run tests diff --git a/Cargo.lock b/Cargo.lock index 1656524..fdfc131 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,7 +121,7 @@ dependencies = [ "objc2-foundation", "parking_lot", "percent-encoding", - "windows-sys 0.59.0", + "windows-sys 0.60.2", "x11rb", ] @@ -161,6 +161,44 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "camino" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1de8bc0aa9e9385ceb3bf0c152e3a9b9544f6c4a912c8ae504e80c1f0368603" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "cassowary" version = "0.3.0" @@ -191,6 +229,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.40" @@ -585,6 +629,12 @@ version = "3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "fdeflate" version = "0.3.7" @@ -644,7 +694,19 @@ checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.6+wasi-0.2.4", ] [[package]] @@ -805,6 +867,48 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "libbpf-cargo" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626c6fbcb5088716de86d0ccbdccedc17b13e59f41a605a3274029335e71fcbb" +dependencies = [ + "anyhow", + "cargo_metadata", + "clap", + "libbpf-rs", + "libbpf-sys", + "memmap2", + "serde", + "serde_json", + "tempfile", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "libbpf-rs" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e23d252d93e246c8787198369f06806c99c5077b5295be29505295f4e5426dc4" +dependencies = [ + "bitflags 2.9.0", + "libbpf-sys", + "libc", + "vsprintf", +] + +[[package]] +name = "libbpf-sys" +version = "1.5.1+v1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "912fae30b08bcbdb861d4b85bd09c05352c0ac9d7b93765ced5ca23709e7e590" +dependencies = [ + "cc", + "nix", + "pkg-config", +] + [[package]] name = "libc" version = "0.2.172" @@ -870,6 +974,15 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memmap2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" +dependencies = [ + "libc", +] + [[package]] name = "miniz_oxide" version = "0.8.8" @@ -888,16 +1001,37 @@ checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.59.0", ] +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "no-std-net" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" +[[package]] +name = "nu-ansi-term" +version = "0.50.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -1061,6 +1195,12 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + [[package]] name = "pkg-config" version = "0.3.32" @@ -1162,6 +1302,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "ratatui" version = "0.29.0" @@ -1236,7 +1382,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", @@ -1275,12 +1421,17 @@ dependencies = [ "aes", "anyhow", "arboard", + "bytes", "chrono", "clap", "crossbeam", "crossterm 0.29.0", "dashmap", "dns-lookup", + "libbpf-cargo", + "libbpf-rs", + "libbpf-sys", + "libc", "log", "num_cpus", "pcap", @@ -1311,25 +1462,67 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] -name = "serde" -version = "1.0.219" +name = "semver" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.223" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a505d71960adde88e293da5cb5eda57093379f64e61cf77bf0e6a63af07a7bac" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.223" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20f57cbd357666aa7b3ac84a90b4ea328f1d4ddb6772b430caa5d9e1309bb9e9" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.223" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "3d428d07faf17e306e699ec1e91996e5a165ba5d6bce5b5155173e91a8a01a56" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1455,6 +1648,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix 1.0.7", + "windows-sys 0.60.2", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -1464,6 +1670,26 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "thiserror" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thread-id" version = "3.3.0" @@ -1475,6 +1701,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "tiff" version = "0.9.1" @@ -1519,6 +1754,38 @@ dependencies = [ "time-core", ] +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +dependencies = [ + "chrono", + "nu-ansi-term", + "sharded-slab", + "thread_local", + "tracing-core", +] + [[package]] name = "typenum" version = "1.18.0" @@ -1578,12 +1845,40 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vsprintf" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aec2f81b75ca063294776b4f7e8da71d1d5ae81c2b1b149c8d89969230265d63" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.14.6+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f71243a3f320c00a8459e455c046ce571229c2f31fd11645d9dc095e3068ca0" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -1994,6 +2289,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + [[package]] name = "x11rb" version = "0.13.1" diff --git a/Cargo.toml b/Cargo.toml index c5d9c51..122439f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,14 +11,11 @@ readme = "README.md" license = "Apache-2.0" keywords = ["network", "monitoring", "tui", "terminal", "packet-capture"] categories = ["command-line-utilities", "network-programming", "visualization"] -exclude = [ - ".github/", - "scripts/", - "tests/", - "*.log", - "target/", - ".gitignore", -] +exclude = [".github/", "scripts/", "tests/", "*.log", "target/", ".gitignore"] + +[lib] +name = "rustnet_monitor" +path = "src/lib.rs" [[bin]] name = "rustnet" @@ -45,3 +42,14 @@ aes = "0.8" [target.'cfg(target_os = "linux")'.dependencies] procfs = "0.16" +libbpf-rs = { version = "0.25", optional = true } +libbpf-sys = { version = "1.4", optional = true } +bytes = { version = "1.5", optional = true } +libc = { version = "0.2", optional = true } + +[target.'cfg(target_os = "linux")'.build-dependencies] +libbpf-cargo = "0.25" + +[features] +default = [] +ebpf = ["libbpf-rs", "libbpf-sys", "bytes", "libc"] diff --git a/EBPF_BUILD.md b/EBPF_BUILD.md new file mode 100644 index 0000000..8c926ac --- /dev/null +++ b/EBPF_BUILD.md @@ -0,0 +1,231 @@ +# eBPF Build Guide + +This document explains how to work with eBPF kernel headers in this project. + +## Current Setup + +We use a **minimal vmlinux header** (`vmlinux_min.h`) instead of the full kernel headers. This approach has trade-offs that should be considered: + +**Benefits of minimal vmlinux_min.h:** + +- **Small size**: 5.5KB (203 lines) vs 3.4MB (100K+ lines) full vmlinux.h +- **Git-friendly**: Small file size, manageable diffs, easier to review +- **Portable**: Works across kernel versions with CO-RE/BTF +- **Clear dependencies**: Shows exactly which kernel structures we depend on + +**Drawbacks of minimal vmlinux_min.h:** + +- **Manual maintenance**: Need to update when adding new eBPF features that access different kernel structures +- **Potential for missing definitions**: Easy to forget required types when extending functionality +- **Development overhead**: Requires understanding of kernel internals to extract correct definitions + +**Alternative approach (full vmlinux.h):** + +- **Pros**: Complete kernel definitions, auto-generated, no manual maintenance, never missing types +- **Cons**: Very large file (3.4MB), but can be gitignored and generated during build process + +## How to Generate Full vmlinux.h (if needed) + +If you need to generate a complete vmlinux.h file for your kernel: + +```bash +# Method 1: Using bpftool (requires root/CAP_BPF) +sudo bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h + +# Method 2: Using pahole (if available) +pahole -J /boot/vmlinux-$(uname -r) +pahole --btf_encode_detached vmlinux.btf /boot/vmlinux-$(uname -r) +bpftool btf dump file vmlinux.btf format c > vmlinux.h + +# Method 3: From kernel source +cd /path/to/kernel/source +make scripts_gdb +bpftool btf dump file vmlinux format c > vmlinux.h +``` + +## How to Create Minimal Headers from Full vmlinux.h + +### 1. Identify Required Structures + +First, analyze your eBPF program to find which kernel structures you access: + +```bash +# Find all struct references in your eBPF code +grep -E "struct [a-zA-Z_]+" socket_tracker.bpf.c + +# Find BPF_CORE_READ usage to see field accesses +grep -E "BPF_CORE_READ.*\\..*" socket_tracker.bpf.c + +# Common structures for socket tracking: +# - struct sock (contains __sk_common) +# - struct sock_common (network fields) +# - struct msghdr (for sendmsg calls) +# - struct sockaddr_in (IPv4 addresses) +# - struct pt_regs (kprobe context) +``` + +### 2. Extract Definitions from Full vmlinux.h + +Use these commands to extract specific structures: + +```bash +# Extract a specific struct (e.g., sock_common) +awk '/^struct sock_common {/,/^}/' vmlinux.h + +# Extract type definitions +grep "typedef.*__u[0-9]*\|typedef.*__be[0-9]*" vmlinux.h + +# Extract multiple related structures +grep -A 50 "struct sock_common {" vmlinux.h +grep -A 20 "struct sock {" vmlinux.h +grep -A 10 "struct msghdr {" vmlinux.h +``` + +### 3. Create Minimal Header + +Create a new header file with: + +1. **Header guards and CO-RE pragma**: + ```c + #ifndef __VMLINUX_MIN_H__ + #define __VMLINUX_MIN_H__ + + #ifndef BPF_NO_PRESERVE_ACCESS_INDEX + #pragma clang attribute push (__attribute__((preserve_access_index)), apply_to = record) + #endif + ``` + +2. **Basic types** (only what you need): + ```c + typedef unsigned char __u8; + typedef unsigned int __u32; + typedef __u32 __be32; + // etc. + ``` + +3. **Required structures** with **only the fields you access**: + ```c + struct sock_common { + // Only include fields accessed by BPF_CORE_READ + __be32 skc_daddr; + __be32 skc_rcv_saddr; + __be16 skc_dport; + __u16 skc_num; + // ... other fields you actually use + }; + ``` + +4. **Footer**: + ```c + #ifndef BPF_NO_PRESERVE_ACCESS_INDEX + #pragma clang attribute pop + #endif + #endif + ``` + +### 4. Automated Extraction Script + +For complex projects, you can create a script to automate extraction: + +```bash +#!/bin/bash +# extract_minimal_vmlinux.sh + +FULL_VMLINUX="vmlinux.h" +OUTPUT="vmlinux_min.h" +BPF_SOURCE="socket_tracker.bpf.c" + +# Find structs used in BPF program +STRUCTS=$(grep -oE "struct [a-zA-Z_]+" "$BPF_SOURCE" | sort -u | cut -d' ' -f2) + +echo "Extracting structures: $STRUCTS" + +# Start minimal header +cat > "$OUTPUT" << 'EOF' +#ifndef __VMLINUX_MIN_H__ +#define __VMLINUX_MIN_H__ + +#ifndef BPF_NO_PRESERVE_ACCESS_INDEX +#pragma clang attribute push (__attribute__((preserve_access_index)), apply_to = record) +#endif + +/* Basic types */ +EOF + +# Extract basic types +grep "typedef.*__u[0-9]*\|typedef.*__be[0-9]*\|typedef.*__kernel" "$FULL_VMLINUX" | head -20 >> "$OUTPUT" + +echo "" >> "$OUTPUT" +echo "/* Network structures */" >> "$OUTPUT" + +# Extract each required struct +for struct in $STRUCTS; do + echo "Extracting struct $struct..." + awk "/^struct $struct \{/,/^}/" "$FULL_VMLINUX" >> "$OUTPUT" + echo "" >> "$OUTPUT" +done + +# Close header +cat >> "$OUTPUT" << 'EOF' + +#ifndef BPF_NO_PRESERVE_ACCESS_INDEX +#pragma clang attribute pop +#endif + +#endif /* __VMLINUX_MIN_H__ */ +EOF + +echo "Minimal vmlinux header created: $OUTPUT" +``` + +## Testing Your Minimal Header + +After creating your minimal header: + +```bash +# Test compilation +cargo build --features ebpf + +# If compilation fails, check for missing definitions +# and add them to your minimal header + +# Verify eBPF program loads (requires root) +sudo cargo run --features ebpf +``` + +## Best Practices + +1. **Start minimal**: Only include structures and fields you actually access +2. **Use CO-RE**: Always include the preserve_access_index pragma for portability +3. **Document sources**: Note which kernel version/source your definitions came from +4. **Test across kernels**: Verify your program works on different kernel versions +5. **Keep synchronized**: Update minimal headers when your eBPF program changes + +## Troubleshooting + +### Compilation Errors + +- **Missing struct definition**: Add the struct to your minimal header +- **Missing field**: Include the specific field in your struct definition +- **Type errors**: Ensure all referenced types are defined + +### Runtime Errors + +- **BTF verification failed**: Check that field names match kernel structures +- **Access violations**: Ensure you're accessing fields that exist in target kernel + +### Field Access Issues + +- **Wrong offset**: Make sure struct layout matches target kernel +- **Missing CO-RE relocations**: Verify preserve_access_index pragma is present + +## Why Not Use Full vmlinux.h? + +While using the full vmlinux.h works, it has downsides: + +- **Huge file size** (3+ MB): Slows down compilation and git operations +- **Unclear dependencies**: Hard to see what your program actually needs +- **Kernel-specific**: Generated for one specific kernel version +- **Review complexity**: Impossible to review 100K+ lines in PRs + +The minimal approach gives you the benefits of vmlinux.h (CO-RE support, exact field layouts) without the downsides. diff --git a/README.md b/README.md index 8893c64..3931378 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ A cross-platform network monitoring tool built with Rust. RustNet provides real- - Protocol-specific cleanup (DNS: 30s, established TCP: 5min, QUIC with close frames: 1-10s) - Activity-based timeout adjustment for long-lived vs idle connections - **Process Identification**: Associate network connections with running processes + - **Note**: With experimental eBPF support, process names are limited to 16 characters from the kernel's `comm` field and may show thread names instead of full executable names - **Service Name Resolution**: Identify well-known services using port numbers - **Cross-platform Support**: Works on Linux, macOS, Windows and potentially BSD systems - **Advanced Filtering**: Real-time vim/fzf-style filtering with keyword support: @@ -41,6 +42,25 @@ A cross-platform network monitoring tool built with Rust. RustNet provides real- - **Multi-threaded Processing**: Concurrent packet processing across multiple threads - **Optional Logging**: Detailed logging with configurable log levels (disabled by default) +### eBPF Enhanced Process Identification (Experimental) + +When built with the `ebpf` feature on Linux, RustNet uses kernel eBPF programs for enhanced performance and lower overhead process identification. However, this comes with important limitations: + +**Process Name Limitations:** +- eBPF uses the kernel's `comm` field, which is limited to 16 characters +- Shows the task/thread command name, not the full executable path +- Multi-threaded applications often show thread names instead of the main process name + +**Real-world Examples:** +- **Firefox**: May appear as "Socket Thread", "Web Content", "Isolated Web Co", or "MainThread" +- **Chrome**: May appear as "ThreadPoolForeg", "Chrome_IOThread", "BrokerProcess", or "SandboxHelper" +- **Electron apps**: Often show as "electron", "node", or internal thread names +- **System processes**: Show truncated names like "systemd-resolve" → "systemd-resolve" + +**Fallback Behavior:** +- When eBPF fails to load or lacks sufficient permissions, RustNet automatically falls back to standard procfs-based process identification +- Standard mode provides full process names but with higher CPU overhead + ## Installation ### Prerequisites @@ -50,6 +70,10 @@ A cross-platform network monitoring tool built with Rust. RustNet provides real- - **Linux**: `sudo apt-get install libpcap-dev` (Debian/Ubuntu) or `sudo yum install libpcap-devel` (RedHat/CentOS) - **macOS**: Included by default - **Windows**: Install Npcap and Npcap SDK (see [Windows Build Setup](#windows-build-setup) below) +- **For eBPF support (optional, experimental - Linux only)**: + - `sudo apt-get install libelf-dev clang llvm` (Debian/Ubuntu) + - `sudo yum install elfutils-libelf-devel clang llvm` (RedHat/CentOS) + - Linux kernel 4.19+ with BTF support recommended ### Windows Build Setup @@ -108,9 +132,12 @@ cargo install rustnet-monitor git clone https://github.com/domcyrus/rustnet.git cd rustnet -# Build in release mode +# Build in release mode (basic functionality) cargo build --release +# Build with experimental eBPF support for enhanced Linux performance (Linux only) +cargo build --release --features ebpf + # The executable will be in target/release/rustnet ``` @@ -547,6 +574,35 @@ sudo setcap cap_net_raw,cap_net_admin=eip ~/.cargo/bin/rustnet rustnet ``` +**For experimental eBPF-enabled builds (enhanced Linux performance):** + +eBPF is an experimental feature that requires additional capabilities for kernel program loading and performance monitoring: + +```bash +# Build with eBPF support +cargo build --release --features ebpf + +# Grant full capability set for eBPF (modern kernels with CAP_BPF support) +sudo setcap 'cap_net_raw,cap_net_admin,cap_bpf,cap_perfmon+eip' ./target/release/rustnet + +# OR for older kernels (fallback to CAP_SYS_ADMIN) +sudo setcap 'cap_net_raw,cap_net_admin,cap_sys_admin+eip' ./target/release/rustnet + +# Run without sudo - eBPF programs will load automatically if capabilities are sufficient +./target/release/rustnet +``` + +**Capability requirements for eBPF:** +- `CAP_NET_RAW` - Raw socket access for packet capture +- `CAP_NET_ADMIN` - Network administration +- `CAP_BPF` - BPF program loading (Linux 5.8+, preferred) +- `CAP_PERFMON` - Performance monitoring (Linux 5.8+, preferred) +- `CAP_SYS_ADMIN` - System administration (fallback for older kernels) + +The application will automatically detect available capabilities and fall back to procfs-only mode if eBPF cannot be loaded. + +**Note:** eBPF support is experimental and may have limitations with process name display (see [eBPF Enhanced Process Identification](#ebpf-enhanced-process-identification-experimental)). + **For system-wide installation:** ```bash diff --git a/ROADMAP.md b/ROADMAP.md index 3ecf2b2..b869639 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -5,9 +5,31 @@ This document outlines the planned features and improvements for RustNet. ## Platform Support - **macOS Support**: Basic features need testing and fixes for macOS compatibility -- **Windows Support**: ✅ Basic functionality working with Npcap SDK and runtime. Process identification not yet implemented for Windows +- **Windows Support**: Basic functionality working with Npcap SDK and runtime. Process identification not yet implemented for Windows - **BSD Support**: Add support for FreeBSD, OpenBSD, and NetBSD -- **Linux Process Identification Enhancement**: Investigate using **eBPF** (Extended Berkeley Packet Filter) for direct kernel-level process identification similar to macOS PKTAP. This would provide more accurate and efficient process-to-connection mapping than the current `/proc` filesystem approach, especially for high-throughput scenarios. +- **Linux Process Identification**: **Experimental eBPF Support Implemented** - Basic eBPF-based process identification now available with `--features ebpf`. Provides efficient kernel-level process-to-connection mapping with lower overhead than procfs. Currently has limitations (see eBPF Improvements section below). + +## eBPF Improvements (Linux) + +The experimental eBPF support provides efficient process identification but has several areas for improvement: + +### Current Limitations +- **Process Names Limited to 16 Characters**: Uses kernel `comm` field, causing truncation (e.g., "Firefox" → "Socket Thread") +- **Thread Names vs Process Names**: Shows thread command names instead of full executable names +- **Minimal vmlinux.h Maintenance**: Current approach requires manual updates when adding new kernel structure access + +### Planned Improvements +- **Hybrid eBPF + Procfs Approach**: Use eBPF for connection tracking, selectively lookup full process names via procfs for better accuracy +- **Full Executable Path Resolution**: Investigate accessing full process executable path from eBPF programs +- **Better Process-Thread Mapping**: Improve mapping from thread IDs to parent process information +- **vmlinux.h Strategy**: Consider switching to full auto-generated vmlinux.h for easier maintenance vs current minimal approach +- **Enhanced BTF Support**: Better compatibility across different kernel versions and distributions +- **Performance Optimizations**: Reduce eBPF map lookups and improve connection-to-process matching efficiency + +### Future Enhancements +- **Real-time Process Updates**: Track process name changes and executable updates +- **Container Support**: Better process identification within containerized environments +- **Security Context**: Include process security attributes (capabilities, SELinux context, etc.) ## Features @@ -18,7 +40,7 @@ This document outlines the planned features and improvements for RustNet. - **DNS Reverse Lookup**: Add optional hostname resolution (toggle between IP and hostname display) - **IPv6 Support**: Full IPv6 connection tracking and display, including DNS resolution, didn't test yet - **Search/Filter**: - - 🔄 Regular expression support (future enhancement) + - Regular expression support (future enhancement) - **Internationalization (i18n)**: Support for multiple languages in the UI - **Connection History**: Store and display historical connection data - **Export Functionality**: Export connections to CSV/JSON formats diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..2a9b324 --- /dev/null +++ b/build.rs @@ -0,0 +1,71 @@ +use std::env; + +fn main() { + // Only compile eBPF programs on Linux when the feature is enabled + if cfg!(target_os = "linux") && env::var("CARGO_FEATURE_EBPF").is_ok() { + compile_ebpf_programs(); + } + + println!("cargo:rerun-if-changed=src/network/platform/linux_ebpf/programs/"); +} + +#[cfg(all(target_os = "linux", feature = "ebpf"))] +fn compile_ebpf_programs() { + use libbpf_cargo::SkeletonBuilder; + use std::path::PathBuf; + + let mut out = PathBuf::from(env::var("OUT_DIR").unwrap()); + out.push("socket_tracker.skel.rs"); + + let src = "src/network/platform/linux_ebpf/programs/socket_tracker.bpf.c"; + + println!("cargo:warning=Building eBPF program using libbpf-cargo"); + + match SkeletonBuilder::new() + .source(src) + .clang_args([ + "-I/usr/include", + "-I/usr/include/linux", + "-I/usr/include/x86_64-linux-gnu", + "-D__TARGET_ARCH_x86", + ]) + .build_and_generate(&out) + { + Ok(_) => { + println!("cargo:warning=eBPF skeleton generated successfully"); + } + Err(e) => { + println!("cargo:warning=Failed to build eBPF program: {}", e); + + // Create a placeholder skeleton file that compiles but returns None + let placeholder_skeleton = r#" +// Placeholder skeleton for failed compilation +#[allow(dead_code)] +pub mod socket_tracker { + use anyhow::Result; + + pub struct SocketTrackerSkel; + + impl SocketTrackerSkel { + pub fn open() -> Result { + Err(anyhow::anyhow!("eBPF compilation failed")) + } + } +} +"#; + std::fs::write(&out, placeholder_skeleton).unwrap_or_else(|e| { + println!( + "cargo:warning=Failed to create placeholder skeleton file: {}", + e + ); + }); + } + } + + println!("cargo:rerun-if-changed={}", src); +} + +#[cfg(not(all(target_os = "linux", feature = "ebpf")))] +fn compile_ebpf_programs() { + // No-op when not on Linux or eBPF feature is not enabled +} diff --git a/src/config.rs b/src/config.rs index 4dd38cf..88240e0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, Result}; +use anyhow::{Result, anyhow}; use std::fs; use std::path::PathBuf; diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f5353be --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,9 @@ +//! RustNet Monitor Library +//! +//! A high-performance, cross-platform network monitoring library built with Rust. + +pub mod app; +pub mod config; +pub mod filter; +pub mod network; +pub mod ui; diff --git a/src/network/parser.rs b/src/network/parser.rs index 4dee03b..632bdcd 100644 --- a/src/network/parser.rs +++ b/src/network/parser.rs @@ -84,6 +84,12 @@ pub struct PacketParser { linktype: Option, // DLT linktype - 149 means PKTAP on macOS } +impl Default for PacketParser { + fn default() -> Self { + Self::new() + } +} + impl PacketParser { #[allow(dead_code)] pub fn new() -> Self { diff --git a/src/network/platform/linux_ebpf/loader.rs b/src/network/platform/linux_ebpf/loader.rs new file mode 100644 index 0000000..fac4598 --- /dev/null +++ b/src/network/platform/linux_ebpf/loader.rs @@ -0,0 +1,184 @@ +//! eBPF program loader with comprehensive error handling + +use anyhow::Result; +use libbpf_rs::skel::{OpenSkel, Skel, SkelBuilder}; +use log::{debug, info, warn}; + +mod socket_tracker { + include!(concat!(env!("OUT_DIR"), "/socket_tracker.skel.rs")); +} + +use socket_tracker::*; + +pub struct EbpfLoader { + skel: Box>, + _open_object: Box>, +} + +impl EbpfLoader { + /// Attempt to load eBPF programs with graceful error handling + pub fn try_load() -> Result> { + // First check if we have necessary capabilities + if !Self::check_capabilities() { + info!("eBPF: Insufficient capabilities (need root or CAP_BPF), falling back to procfs"); + return Ok(None); + } else { + info!("eBPF: Sufficient capabilities detected, attempting to load program"); + } + + match Self::load_program() { + Ok(loader) => { + info!("eBPF: Socket tracker loaded and attached successfully"); + Ok(Some(loader)) + } + Err(e) => { + warn!( + "eBPF: Failed to load program: {}, falling back to procfs", + e + ); + Ok(None) + } + } + } + + fn load_program() -> Result { + debug!("eBPF: Opening eBPF skeleton"); + let skel_builder = SocketTrackerSkelBuilder::default(); + + // Heap allocate the object to avoid lifetime issues + let mut open_object = Box::new(std::mem::MaybeUninit::uninit()); + let open_skel = skel_builder.open(&mut open_object).map_err(|e| { + warn!("eBPF: Failed to open skeleton: {}", e); + e + })?; + + debug!("eBPF: Loading program into kernel"); + let mut skel = open_skel.load().map_err(|e| { + warn!("eBPF: Failed to load program into kernel: {}", e); + e + })?; + + debug!("eBPF: Attaching all programs"); + match skel.attach() { + Ok(_) => { + info!("eBPF: Programs attached successfully"); + // Verify programs are actually attached by checking their links + let prog_names = [ + "trace_tcp_connect", + "trace_tcp_accept", + "trace_udp_sendmsg", + "trace_tcp_v6_connect", + "trace_udp_v6_sendmsg", + ]; + + for (i, prog_name) in prog_names.iter().enumerate() { + debug!("eBPF: Checking attachment for program {}: {}", i, prog_name); + } + + info!("eBPF: All programs loaded and attached successfully"); + } + Err(e) => { + warn!("eBPF: Failed to attach programs: {}", e); + return Err(e.into()); + } + } + + // Convert to 'static lifetime by boxing + let skel_static: SocketTrackerSkel<'static> = unsafe { std::mem::transmute(skel) }; + + Ok(Self { + skel: Box::new(skel_static), + _open_object: open_object, + }) + } + + /// Check if we have the necessary capabilities for eBPF + fn check_capabilities() -> bool { + use std::fs; + + // Check if we're running as root + if unsafe { libc::geteuid() } == 0 { + debug!("eBPF: Running as root - all capabilities available"); + return true; + } + + // Check for required capabilities via /proc/self/status + if let Ok(status) = fs::read_to_string("/proc/self/status") { + // Parse CapEff (effective capabilities) line + if let Some(cap_line) = status.lines().find(|line| line.starts_with("CapEff:")) + && let Some(cap_hex) = cap_line.split_whitespace().nth(1) + && let Ok(cap_value) = u64::from_str_radix(cap_hex, 16) + { + debug!("eBPF: Current effective capabilities: 0x{:x}", cap_value); + + // Required capabilities (bit positions): + // CAP_NET_RAW = 13, CAP_NET_ADMIN = 12, CAP_BPF = 39, CAP_PERFMON = 38, CAP_SYS_ADMIN = 21 + let required_caps = [ + 13, // CAP_NET_RAW - for packet capture + 12, // CAP_NET_ADMIN - for network administration + 21, // CAP_SYS_ADMIN - fallback for older kernels + ]; + + // Optional modern capabilities (Linux 5.8+) + let modern_caps = [ + 39, // CAP_BPF - for BPF operations + 38, // CAP_PERFMON - for performance monitoring + ]; + + // Check required capabilities + let has_required = required_caps.iter().all(|&cap| { + let has_cap = (cap_value & (1u64 << cap)) != 0; + debug!( + "eBPF: Capability {} (bit {}): {}", + match cap { + 13 => "CAP_NET_RAW", + 12 => "CAP_NET_ADMIN", + 21 => "CAP_SYS_ADMIN", + _ => "UNKNOWN", + }, + cap, + if has_cap { "present" } else { "missing" } + ); + has_cap + }); + + // Check modern capabilities (nice to have) + let has_modern = modern_caps.iter().any(|&cap| { + let has_cap = (cap_value & (1u64 << cap)) != 0; + debug!( + "eBPF: Modern capability {} (bit {}): {}", + match cap { + 39 => "CAP_BPF", + 38 => "CAP_PERFMON", + _ => "UNKNOWN", + }, + cap, + if has_cap { "present" } else { "missing" } + ); + has_cap + }); + + if has_required { + if has_modern { + info!("eBPF: All required and modern capabilities present"); + } else { + info!("eBPF: Required capabilities present, using legacy mode"); + } + return true; + } else { + debug!("eBPF: Missing required capabilities"); + } + } + } + + debug!( + "eBPF: Insufficient capabilities - need CAP_NET_RAW, CAP_NET_ADMIN, and CAP_SYS_ADMIN (or CAP_BPF+CAP_PERFMON on newer kernels)" + ); + false + } + + /// Get the socket map for lookups + pub fn socket_map(&self) -> &libbpf_rs::Map<'_> { + &self.skel.maps.socket_map + } +} diff --git a/src/network/platform/linux_ebpf/maps_libbpf.rs b/src/network/platform/linux_ebpf/maps_libbpf.rs new file mode 100644 index 0000000..61892b4 --- /dev/null +++ b/src/network/platform/linux_ebpf/maps_libbpf.rs @@ -0,0 +1,275 @@ +//! eBPF map interaction utilities for libbpf-rs + +use super::ProcessInfo; +use anyhow::Result; +use libbpf_rs::MapCore; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +/// Connection key matching the eBPF program structure (supports IPv4 and IPv6) +#[repr(C, packed)] +#[derive(Debug, Clone, Copy)] +pub struct ConnKey { + pub saddr: [u32; 4], // IPv4 uses only saddr[0], IPv6 uses all 4 + pub daddr: [u32; 4], // IPv4 uses only daddr[0], IPv6 uses all 4 + pub sport: u16, + pub dport: u16, + pub proto: u8, // IPPROTO_TCP or IPPROTO_UDP + pub family: u8, // AF_INET or AF_INET6 +} + +/// Connection info matching the eBPF program structure +#[repr(C, packed)] +#[derive(Debug, Clone, Copy)] +pub struct ConnInfo { + pub pid: u32, + pub uid: u32, + pub comm: [u8; 16], + pub timestamp: u64, +} + +impl ConnKey { + pub fn new(src_ip: IpAddr, dst_ip: IpAddr, src_port: u16, dst_port: u16, is_tcp: bool) -> Self { + let mut key = Self { + saddr: [0; 4], + daddr: [0; 4], + sport: src_port, + dport: dst_port, + proto: if is_tcp { 6 } else { 17 }, // IPPROTO_TCP or IPPROTO_UDP + family: match src_ip { + IpAddr::V4(_) => 2, // AF_INET + IpAddr::V6(_) => 10, // AF_INET6 + }, + }; + + match (src_ip, dst_ip) { + (IpAddr::V4(src), IpAddr::V4(dst)) => { + // Use little-endian to match kernel/eBPF native format + key.saddr[0] = u32::from_le_bytes(src.octets()); + key.daddr[0] = u32::from_le_bytes(dst.octets()); + } + (IpAddr::V6(src), IpAddr::V6(dst)) => { + let src_bytes = src.octets(); + let dst_bytes = dst.octets(); + + // Convert 16-byte IPv6 addresses to 4 u32 values (big-endian) + for i in 0..4 { + let src_start = i * 4; + let dst_start = i * 4; + key.saddr[i] = u32::from_be_bytes([ + src_bytes[src_start], + src_bytes[src_start + 1], + src_bytes[src_start + 2], + src_bytes[src_start + 3], + ]); + key.daddr[i] = u32::from_be_bytes([ + dst_bytes[dst_start], + dst_bytes[dst_start + 1], + dst_bytes[dst_start + 2], + dst_bytes[dst_start + 3], + ]); + } + } + _ => { + // Mixed IPv4/IPv6 - shouldn't happen in practice + panic!("Mixed IPv4/IPv6 addresses not supported"); + } + } + + key + } + + pub fn new_v4( + src_ip: Ipv4Addr, + dst_ip: Ipv4Addr, + src_port: u16, + dst_port: u16, + is_tcp: bool, + ) -> Self { + Self::new( + IpAddr::V4(src_ip), + IpAddr::V4(dst_ip), + src_port, + dst_port, + is_tcp, + ) + } + + pub(crate) fn new_v6( + src_ip: Ipv6Addr, + dst_ip: Ipv6Addr, + src_port: u16, + dst_port: u16, + is_tcp: bool, + ) -> Self { + Self::new( + IpAddr::V6(src_ip), + IpAddr::V6(dst_ip), + src_port, + dst_port, + is_tcp, + ) + } + + /// Convert to bytes for map lookup + pub fn as_bytes(&self) -> [u8; 38] { + unsafe { std::mem::transmute(*self) } + } +} + +impl From for ProcessInfo { + fn from(info: ConnInfo) -> Self { + // Convert C string to Rust String + let comm_len = info.comm.iter().position(|&x| x == 0).unwrap_or(16); + let comm = String::from_utf8_lossy(&info.comm[..comm_len]).to_string(); + + Self { + pid: info.pid, + uid: info.uid, + comm, + timestamp: info.timestamp, + } + } +} + +pub struct MapReader; + +impl MapReader { + /// Query the socket map for connection information using libbpf-rs + pub fn lookup_connection(map: &libbpf_rs::Map, key: ConnKey) -> Result> { + let key_bytes = key.as_bytes(); + + match map.lookup(&key_bytes, libbpf_rs::MapFlags::empty()) { + Ok(Some(value_bytes)) => { + if value_bytes.len() != 32 { + return Err(anyhow::anyhow!( + "Invalid map value size: expected 32, got {}", + value_bytes.len() + )); + } + + let mut info_bytes = [0u8; 32]; + info_bytes.copy_from_slice(&value_bytes); + let conn_info: ConnInfo = unsafe { std::mem::transmute(info_bytes) }; + Ok(Some(conn_info.into())) + } + Ok(None) => Ok(None), + Err(e) => { + log::debug!("eBPF map lookup failed: {}", e); + Ok(None) + } + } + } + + /// Clean up stale entries from the map based on timestamp + pub fn cleanup_stale_entries(map: &libbpf_rs::Map, stale_threshold_ns: u64) -> Result { + use std::time::{SystemTime, UNIX_EPOCH}; + + let current_time_ns = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|e| anyhow::anyhow!("Time error: {}", e))? + .as_nanos() as u64; + + let mut cleanup_count = 0u32; + let mut keys_to_delete = Vec::new(); + + // Try to iterate using MapKeyIter + for key in map.keys() { + // We have a key, check if its value is stale + if let Ok(Some(value_bytes)) = map.lookup(&key, libbpf_rs::MapFlags::empty()) + && value_bytes.len() >= 32 + { + // Extract timestamp from last 8 bytes + let timestamp_bytes = &value_bytes[24..32]; + let timestamp = u64::from_ne_bytes([ + timestamp_bytes[0], + timestamp_bytes[1], + timestamp_bytes[2], + timestamp_bytes[3], + timestamp_bytes[4], + timestamp_bytes[5], + timestamp_bytes[6], + timestamp_bytes[7], + ]); + + if current_time_ns.saturating_sub(timestamp) > stale_threshold_ns { + // Entry is stale, mark for deletion + keys_to_delete.push(key); + log::debug!( + "Found stale entry, timestamp: {}, current: {}, threshold: {}", + timestamp, + current_time_ns, + stale_threshold_ns + ); + } + } + } + + // Delete all stale entries + for key in keys_to_delete { + if let Err(e) = map.delete(&key) { + log::debug!("Failed to delete stale entry: {}", e); + } else { + cleanup_count += 1; + } + } + + if cleanup_count > 0 { + log::info!("eBPF cleanup: removed {} stale entries", cleanup_count); + } + + Ok(cleanup_count) + } + + /// Log map lookup details for debugging + pub fn debug_lookup_miss(map: &libbpf_rs::Map, lookup_key: &ConnKey) -> Result<()> { + log::info!("=== eBPF Map Lookup Miss Debug ==="); + + // Copy fields to avoid packed struct alignment issues + let saddr = lookup_key.saddr[0]; + let daddr = lookup_key.daddr[0]; + let sport = lookup_key.sport; + let dport = lookup_key.dport; + let proto = lookup_key.proto; + let family = lookup_key.family; + + log::info!( + "Looking for key: saddr={:08x} ({}.{}.{}.{}), daddr={:08x} ({}.{}.{}.{}), sport={}, dport={}, proto={}, family={}", + saddr, + saddr & 0xff, + (saddr >> 8) & 0xff, + (saddr >> 16) & 0xff, + (saddr >> 24) & 0xff, + daddr, + daddr & 0xff, + (daddr >> 8) & 0xff, + (daddr >> 16) & 0xff, + (daddr >> 24) & 0xff, + sport, + dport, + proto, + family + ); + + log::info!("Key bytes: {:02x?}", lookup_key.as_bytes()); + + // Get map info + let info = map.info(); + match info { + Ok(map_info) => { + log::info!( + "Map type: {:?}, max_entries: {}, key_size: {}, value_size: {}", + map_info.map_type(), + map_info.info.max_entries, + map_info.info.key_size, + map_info.info.value_size + ); + } + Err(e) => { + log::debug!("Failed to get map info: {}", e); + } + } + + log::info!("=== End Lookup Debug ==="); + Ok(()) + } +} diff --git a/src/network/platform/linux_ebpf/mod.rs b/src/network/platform/linux_ebpf/mod.rs new file mode 100644 index 0000000..83127cc --- /dev/null +++ b/src/network/platform/linux_ebpf/mod.rs @@ -0,0 +1,19 @@ +//! Linux eBPF process tracking module +//! +//! This module provides enhanced process lookup using eBPF for TCP/UDP connections. +//! It maintains compatibility with the existing procfs approach as a fallback. + +pub mod loader; +pub mod maps_libbpf; +pub mod tracker_libbpf; + +pub use tracker_libbpf::LibbpfSocketTracker as EbpfSocketTracker; + +/// Process information from eBPF +#[derive(Debug, Clone)] +pub struct ProcessInfo { + pub pid: u32, + pub uid: u32, + pub comm: String, + pub timestamp: u64, +} diff --git a/src/network/platform/linux_ebpf/programs/socket_tracker.bpf.c b/src/network/platform/linux_ebpf/programs/socket_tracker.bpf.c new file mode 100644 index 0000000..038f0fb --- /dev/null +++ b/src/network/platform/linux_ebpf/programs/socket_tracker.bpf.c @@ -0,0 +1,246 @@ +// Socket tracker eBPF program +// CO-RE (Compile Once - Run Everywhere) version using BTF + +#include "vmlinux_min.h" +#include +#include +#include +#include + +#define MAX_ENTRIES 32768 +#define TASK_COMM_LEN 16 + +// Network constants not included in vmlinux.h +#define AF_INET 2 /* IPv4 */ +#define AF_INET6 10 /* IPv6 */ +#define IPPROTO_TCP 6 /* TCP */ +#define IPPROTO_UDP 17 /* UDP */ + +// Connection key for socket tracking (supports both IPv4 and IPv6) +struct conn_key +{ + __u32 saddr[4]; // IPv4 uses only saddr[0], IPv6 uses all 4 + __u32 daddr[4]; // IPv4 uses only daddr[0], IPv6 uses all 4 + __u16 sport; + __u16 dport; + __u8 proto; // IPPROTO_TCP or IPPROTO_UDP + __u8 family; // AF_INET or AF_INET6 +} __attribute__((packed)); + +// Process information +struct conn_info +{ + __u32 pid; + __u32 uid; + char comm[TASK_COMM_LEN]; + __u64 timestamp; +} __attribute__((packed)); + +// Socket tracking map +struct +{ + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, MAX_ENTRIES); + __type(key, struct conn_key); + __type(value, struct conn_info); +} socket_map SEC(".maps"); + +// Helper to populate process information +static __always_inline void get_process_info(struct conn_info *info) +{ + __u64 pid_tgid = bpf_get_current_pid_tgid(); + __u64 uid_gid = bpf_get_current_uid_gid(); + + info->pid = pid_tgid >> 32; + info->uid = uid_gid >> 32; + info->timestamp = bpf_ktime_get_ns(); + + bpf_get_current_comm(&info->comm, sizeof(info->comm)); +} + +// TCP connect tracking - use tcp_connect for better address capture +SEC("kprobe/tcp_connect") +int trace_tcp_connect(struct pt_regs *ctx) +{ + struct sock *sk = (struct sock *)PT_REGS_PARM1_CORE(ctx); + if (!sk) + { + return 0; + } + + struct conn_key key = {}; + struct conn_info info = {}; + + // Read socket information for IPv4 using CO-RE + key.saddr[0] = BPF_CORE_READ(sk, __sk_common.skc_rcv_saddr); + key.daddr[0] = BPF_CORE_READ(sk, __sk_common.skc_daddr); + key.sport = BPF_CORE_READ(sk, __sk_common.skc_num); + key.dport = BPF_CORE_READ(sk, __sk_common.skc_dport); + + key.dport = bpf_ntohs(key.dport); + key.proto = IPPROTO_TCP; + key.family = AF_INET; + + get_process_info(&info); + + int ret = bpf_map_update_elem(&socket_map, &key, &info, BPF_ANY); + if (ret != 0) + { + bpf_printk("tcp_connect: map update failed ret=%d", ret); + } + return 0; +} + +// TCP accept tracking +SEC("kprobe/inet_csk_accept") +int trace_tcp_accept(struct pt_regs *ctx) +{ + struct sock *sk = (struct sock *)PT_REGS_PARM1_CORE(ctx); + if (!sk) + { + return 0; + } + + struct conn_key key = {}; + struct conn_info info = {}; + + key.saddr[0] = BPF_CORE_READ(sk, __sk_common.skc_rcv_saddr); + key.daddr[0] = BPF_CORE_READ(sk, __sk_common.skc_daddr); + key.sport = BPF_CORE_READ(sk, __sk_common.skc_num); + key.dport = BPF_CORE_READ(sk, __sk_common.skc_dport); + + key.dport = bpf_ntohs(key.dport); + key.proto = IPPROTO_TCP; + key.family = AF_INET; + + get_process_info(&info); + + int ret = bpf_map_update_elem(&socket_map, &key, &info, BPF_ANY); + if (ret != 0) + { + bpf_printk("inet_csk_accept: map update failed ret=%d", ret); + } + return 0; +} + +// UDP sendmsg tracking - extract destination from msghdr +SEC("kprobe/udp_sendmsg") +int trace_udp_sendmsg(struct pt_regs *ctx) +{ + struct sock *sk = (struct sock *)PT_REGS_PARM1_CORE(ctx); + struct msghdr *msg = (struct msghdr *)PT_REGS_PARM2_CORE(ctx); + + if (!sk || !msg) + { + return 0; + } + + struct conn_key key = {}; + struct conn_info info = {}; + + // Get source address from socket + key.saddr[0] = BPF_CORE_READ(sk, __sk_common.skc_rcv_saddr); + key.sport = BPF_CORE_READ(sk, __sk_common.skc_num); + + // Try to get destination from msghdr->msg_name (sockaddr_in) + struct sockaddr_in *dest_addr = NULL; + bpf_probe_read_kernel(&dest_addr, sizeof(dest_addr), &msg->msg_name); + + if (dest_addr) + { + bpf_probe_read_kernel(&key.daddr[0], sizeof(__u32), &dest_addr->sin_addr.s_addr); + bpf_probe_read_kernel(&key.dport, sizeof(__u16), &dest_addr->sin_port); + } + else + { + // Fallback to socket fields (might be zero for unconnected UDP) + key.daddr[0] = BPF_CORE_READ(sk, __sk_common.skc_daddr); + key.dport = BPF_CORE_READ(sk, __sk_common.skc_dport); + } + + // Only skip if destination is zero (source might be unbound for UDP) + if (key.daddr[0] == 0) + { + return 0; + } + + key.dport = bpf_ntohs(key.dport); + key.proto = IPPROTO_UDP; + key.family = AF_INET; + + get_process_info(&info); + + int ret = bpf_map_update_elem(&socket_map, &key, &info, BPF_ANY); + if (ret != 0) + { + bpf_printk("udp_sendmsg: map update failed ret=%d", ret); + } + return 0; +} + +// IPv6 TCP connect tracking +SEC("kprobe/tcp_v6_connect") +int trace_tcp_v6_connect(struct pt_regs *ctx) +{ + struct sock *sk = (struct sock *)PT_REGS_PARM1_CORE(ctx); + if (!sk) + return 0; + + struct conn_key key = {}; + struct conn_info info = {}; + + // Read socket information for IPv6 using CO-RE + // Use temporary variables to avoid packed member warnings + struct in6_addr temp_saddr, temp_daddr; + BPF_CORE_READ_INTO(&temp_saddr, sk, __sk_common.skc_v6_rcv_saddr); + BPF_CORE_READ_INTO(&temp_daddr, sk, __sk_common.skc_v6_daddr); + + // Copy to packed structure + __builtin_memcpy(key.saddr, &temp_saddr, sizeof(temp_saddr)); + __builtin_memcpy(key.daddr, &temp_daddr, sizeof(temp_daddr)); + key.sport = BPF_CORE_READ(sk, __sk_common.skc_num); + key.dport = BPF_CORE_READ(sk, __sk_common.skc_dport); + + key.dport = bpf_ntohs(key.dport); + key.proto = IPPROTO_TCP; + key.family = AF_INET6; + + get_process_info(&info); + + bpf_map_update_elem(&socket_map, &key, &info, BPF_ANY); + return 0; +} + +// IPv6 UDP sendmsg tracking +SEC("kprobe/udpv6_sendmsg") +int trace_udp_v6_sendmsg(struct pt_regs *ctx) +{ + struct sock *sk = (struct sock *)PT_REGS_PARM1_CORE(ctx); + if (!sk) + return 0; + + struct conn_key key = {}; + struct conn_info info = {}; + + // Use temporary variables to avoid packed member warnings + struct in6_addr temp_saddr, temp_daddr; + BPF_CORE_READ_INTO(&temp_saddr, sk, __sk_common.skc_v6_rcv_saddr); + BPF_CORE_READ_INTO(&temp_daddr, sk, __sk_common.skc_v6_daddr); + + // Copy to packed structure + __builtin_memcpy(key.saddr, &temp_saddr, sizeof(temp_saddr)); + __builtin_memcpy(key.daddr, &temp_daddr, sizeof(temp_daddr)); + key.sport = BPF_CORE_READ(sk, __sk_common.skc_num); + key.dport = BPF_CORE_READ(sk, __sk_common.skc_dport); + + key.dport = bpf_ntohs(key.dport); + key.proto = IPPROTO_UDP; + key.family = AF_INET6; + + get_process_info(&info); + + bpf_map_update_elem(&socket_map, &key, &info, BPF_ANY); + return 0; +} + +char LICENSE[] SEC("license") = "Dual BSD/GPL"; diff --git a/src/network/platform/linux_ebpf/programs/vmlinux_min.h b/src/network/platform/linux_ebpf/programs/vmlinux_min.h new file mode 100644 index 0000000..3590a00 --- /dev/null +++ b/src/network/platform/linux_ebpf/programs/vmlinux_min.h @@ -0,0 +1,243 @@ +#ifndef __VMLINUX_MIN_H__ +#define __VMLINUX_MIN_H__ + +/* + * Minimal vmlinux.h with only the kernel structures needed for our eBPF socket tracker. + * This replaces the full vmlinux.h (3.4MB) with just the essential definitions. + * + * Generated for Linux kernel structures used in socket tracking. + * See EBPF_BUILD.md for instructions on how to regenerate or customize. + */ + +/* Define this to trigger kernel field names in bpf_tracing.h */ +#define __VMLINUX_H__ + +#ifndef BPF_NO_PRESERVE_ACCESS_INDEX +#pragma clang attribute push (__attribute__((preserve_access_index)), apply_to = record) +#endif + +/* Basic kernel types */ +typedef unsigned char __u8; +typedef unsigned short __u16; +typedef unsigned int __u32; +typedef unsigned long long __u64; +typedef signed char __s8; +typedef signed short __s16; +typedef signed int __s32; +typedef signed long long __s64; +typedef __u16 __be16; +typedef __u32 __be32; +typedef __u64 __be64; +typedef __u32 __wsum; +typedef __u16 __kernel_sa_family_t; +typedef _Bool bool; + +/* Additional type definitions needed for kernel structures */ +typedef __u64 __addrpair; +typedef __u32 __portpair; +typedef struct { __s64 counter; } atomic64_t; +typedef struct { __s32 counter; } atomic_t; + +/* BPF map types and flags */ +enum { + BPF_MAP_TYPE_HASH = 1, +}; + +enum { + BPF_ANY = 0, +}; + +/* Network address structures */ +struct in_addr { + __be32 s_addr; +}; + +struct in6_addr { + union { + __u8 u6_addr8[16]; + __be16 u6_addr16[8]; + __be32 u6_addr32[4]; + } in6_u; +}; + +/* Socket address structures */ +struct sockaddr_in { + __kernel_sa_family_t sin_family; + __be16 sin_port; + struct in_addr sin_addr; + unsigned char __pad[8]; +}; + +/* Forward declarations for complex types we don't need to fully define */ +struct proto; +struct sk_buff_head; + +/* Simple structures we need defined */ +struct hlist_node { + struct hlist_node *next, **pprev; +}; + +struct iov_iter { + /* We don't access fields, just need size/layout for msghdr */ + void *__opaque[8]; /* Approximate size, CO-RE will handle differences */ +}; + +/* Minimal possible_net_t - we don't access its internals */ +typedef struct { + void *net; /* We don't dereference this, just need the field present */ +} possible_net_t; + +/* Socket common structure - core networking fields */ +struct sock_common { + /* Address pair for IPv4 */ + union { + __addrpair skc_addrpair; /* We don't use this directly */ + struct { + __be32 skc_daddr; /* destination IPv4 address */ + __be32 skc_rcv_saddr; /* source IPv4 address */ + }; + }; + + /* Hash - we don't use this but it's part of the layout */ + union { + unsigned int skc_hash; + __u16 skc_u16hashes[2]; + }; + + /* Port pair */ + union { + __portpair skc_portpair; /* We don't use this directly */ + struct { + __be16 skc_dport; /* destination port */ + __u16 skc_num; /* source port */ + }; + }; + + /* Basic socket properties */ + short unsigned int skc_family; + volatile unsigned char skc_state; + unsigned char skc_reuse: 4; + unsigned char skc_reuseport: 1; + unsigned char skc_ipv6only: 1; + unsigned char skc_net_refcnt: 1; + int skc_bound_dev_if; + + /* Hash table linkage - we don't use these but they're part of layout */ + union { + struct hlist_node skc_bind_node; + struct hlist_node skc_portaddr_node; + }; + + /* Protocol and network namespace */ + struct proto *skc_prot; + possible_net_t skc_net; + + /* IPv6 addresses - these come after the above fields */ + struct in6_addr skc_v6_daddr; + struct in6_addr skc_v6_rcv_saddr; + + /* Socket cookie for identification */ + atomic64_t skc_cookie; + + /* Additional fields exist but we don't need them for CO-RE access */ +}; + +/* Main socket structure - we only need the common part */ +struct sock { + struct sock_common __sk_common; + /* + * Many more fields exist here, but we only access __sk_common + * CO-RE will handle the field relocations regardless of what + * other fields are present in different kernel versions + */ +}; + +/* Message header for sendmsg syscalls */ +struct msghdr { + void *msg_name; /* Socket name (sockaddr_in* for UDP) */ + int msg_namelen; /* Length of socket name */ + int msg_inq; /* Bytes in receive queue */ + struct iov_iter msg_iter; /* Data payload iterator */ + + /* Control messages - we don't use these but they're part of layout */ + union { + void *msg_control; + void *msg_control_user; + }; + bool msg_control_is_user: 1; + bool msg_get_inq: 1; + /* Additional fields may exist but we only need msg_name */ +}; + +/* FRED (Flexible Return and Event Delivery) support structures */ +struct fred_cs { + __u64 cs: 16; + __u64 sl: 2; + __u64 wfe: 1; +}; + +struct fred_ss { + __u64 ss: 16; + __u64 sti: 1; + __u64 swevent: 1; + __u64 nmi: 1; + int: 13; + __u64 vector: 8; + short: 8; + __u64 type: 4; + char: 4; + __u64 enclave: 1; +}; + +/* + * Architecture-specific pt_regs for kprobe context + * x86_64 specific - for PT_REGS_PARM1/PT_REGS_PARM2 macros + * Field names must match kernel exactly for CO-RE relocations + */ +struct pt_regs { + /* + * C ABI says these regs are callee-preserved. They aren't saved on kernel entry + * unless syscall needs a complete, fully filled "struct pt_regs". + */ + long unsigned int r15; + long unsigned int r14; + long unsigned int r13; + long unsigned int r12; + long unsigned int bp; /* Must match kernel BTF field names */ + long unsigned int bx; /* Must match kernel BTF field names */ + /* These regs are callee-clobbered. Always saved on kernel entry. */ + long unsigned int r11; + long unsigned int r10; + long unsigned int r9; + long unsigned int r8; + long unsigned int ax; /* Must match kernel BTF field names */ + long unsigned int cx; /* Must match kernel BTF field names */ + long unsigned int dx; /* Must match kernel BTF field names */ + long unsigned int si; /* Must match kernel BTF field names */ + long unsigned int di; /* Must match kernel BTF field names */ + /* + * On syscall entry, this is syscall#. On CPU exception, this is error code. + * On hw interrupt, it's IRQ number: + */ + long unsigned int orig_ax; + /* Return frame for iretq */ + long unsigned int ip; /* Must match kernel BTF field names */ + union { + __u16 cs; + __u64 csx; + struct fred_cs fred_cs; + }; + long unsigned int flags; /* Must match kernel BTF field names */ + long unsigned int sp; /* Must match kernel BTF field names */ + union { + __u16 ss; + __u64 ssx; + struct fred_ss fred_ss; + }; +}; + +#ifndef BPF_NO_PRESERVE_ACCESS_INDEX +#pragma clang attribute pop +#endif + +#endif /* __VMLINUX_MIN_H__ */ \ No newline at end of file diff --git a/src/network/platform/linux_ebpf/tracker_libbpf.rs b/src/network/platform/linux_ebpf/tracker_libbpf.rs new file mode 100644 index 0000000..c38dca1 --- /dev/null +++ b/src/network/platform/linux_ebpf/tracker_libbpf.rs @@ -0,0 +1,170 @@ +//! eBPF socket tracker implementation using libbpf-rs + +use super::{ + ProcessInfo, + loader::EbpfLoader, + maps_libbpf::{ConnKey, MapReader}, +}; +use anyhow::Result; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +pub struct LibbpfSocketTracker { + loader: EbpfLoader, +} + +unsafe impl Send for LibbpfSocketTracker {} +unsafe impl Sync for LibbpfSocketTracker {} + +impl LibbpfSocketTracker { + /// Create a new eBPF socket tracker + /// Returns None if eBPF cannot be loaded (insufficient privileges, etc.) + pub fn new() -> Result> { + match EbpfLoader::try_load()? { + Some(loader) => Ok(Some(Self { loader })), + None => Ok(None), + } + } + + /// Look up process information for a connection (IPv4) + pub fn lookup_v4( + &mut self, + src_ip: Ipv4Addr, + dst_ip: Ipv4Addr, + src_port: u16, + dst_port: u16, + is_tcp: bool, + ) -> Option { + let socket_map = self.loader.socket_map(); + + // Try exact match first + let key = ConnKey::new_v4(src_ip, dst_ip, src_port, dst_port, is_tcp); + match MapReader::lookup_connection(socket_map, key) { + Ok(Some(result)) => { + return Some(result); + } + Ok(None) => { + log::debug!("eBPF exact lookup miss, trying with zero source address"); + } + Err(e) => { + log::debug!("eBPF IPv4 lookup failed: {}", e); + } + } + + // Try with zero source address (common for eBPF UDP/TCP entries) + let zero_src_key = ConnKey::new_v4( + Ipv4Addr::new(0, 0, 0, 0), + dst_ip, + src_port, + dst_port, + is_tcp, + ); + log::debug!( + "eBPF zero-source key bytes: {:02x?}", + zero_src_key.as_bytes() + ); + match MapReader::lookup_connection(socket_map, zero_src_key) { + Ok(Some(result)) => { + log::info!( + "🎉 eBPF lookup succeeded with zero source address! PID: {}, comm: {}", + result.pid, + result.comm + ); + // Let cleanup handle entry deletion based on age + Some(result) + } + Ok(None) => { + // Debug both keys for comparison + log::debug!("eBPF lookup missed with both exact and zero-source keys"); + if let Err(e) = MapReader::debug_lookup_miss(socket_map, &key) { + log::debug!("Failed to debug lookup: {}", e); + } + None + } + Err(e) => { + log::debug!("eBPF zero-source lookup failed: {}", e); + None + } + } + } + + /// Look up process information for a connection (IPv6) + pub fn lookup_v6( + &mut self, + src_ip: Ipv6Addr, + dst_ip: Ipv6Addr, + src_port: u16, + dst_port: u16, + is_tcp: bool, + ) -> Option { + let key = ConnKey::new_v6(src_ip, dst_ip, src_port, dst_port, is_tcp); + + let socket_map = self.loader.socket_map(); + match MapReader::lookup_connection(socket_map, key) { + Ok(Some(result)) => { + // Let cleanup handle entry deletion based on age + Some(result) + } + Ok(None) => { + // Debug map lookup miss to see what we're looking for + if let Err(e) = MapReader::debug_lookup_miss(socket_map, &key) { + log::debug!("Failed to debug lookup: {}", e); + } + None + } + Err(e) => { + log::debug!("eBPF IPv6 lookup failed: {}", e); + None + } + } + } + + /// Look up process information for a connection (generic) + pub fn lookup( + &mut self, + src_ip: IpAddr, + dst_ip: IpAddr, + src_port: u16, + dst_port: u16, + is_tcp: bool, + ) -> Option { + match (src_ip, dst_ip) { + (IpAddr::V4(src), IpAddr::V4(dst)) => { + self.lookup_v4(src, dst, src_port, dst_port, is_tcp) + } + (IpAddr::V6(src), IpAddr::V6(dst)) => { + self.lookup_v6(src, dst, src_port, dst_port, is_tcp) + } + _ => { + log::warn!("Mixed IPv4/IPv6 addresses not supported in eBPF lookup"); + None + } + } + } + + /// Check if the tracker is healthy and operational + pub fn is_healthy(&self) -> bool { + // Simple health check - in a real implementation you might + // check if programs are still attached, etc. + true + } + + /// Clean up stale entries from the eBPF map + /// Returns the number of entries cleaned up + pub fn cleanup_stale_entries(&mut self, stale_threshold_secs: u64) -> u32 { + let socket_map = self.loader.socket_map(); + let stale_threshold_ns = stale_threshold_secs * 1_000_000_000; + + match MapReader::cleanup_stale_entries(socket_map, stale_threshold_ns) { + Ok(count) => { + if count > 0 { + log::info!("eBPF map cleanup: removed {} stale entries", count); + } + count + } + Err(e) => { + log::debug!("eBPF map cleanup failed: {}", e); + 0 + } + } + } +} diff --git a/src/network/platform/linux_enhanced.rs b/src/network/platform/linux_enhanced.rs new file mode 100644 index 0000000..813c8b0 --- /dev/null +++ b/src/network/platform/linux_enhanced.rs @@ -0,0 +1,590 @@ +//! Enhanced Linux process lookup combining eBPF and procfs approaches + +use super::{ConnectionKey, ProcessLookup}; + +use super::linux::LinuxProcessLookup; +use crate::network::types::{Connection, Protocol}; +use anyhow::Result; +use log::{debug, info, warn}; +use std::collections::HashMap; +use std::net::IpAddr; +use std::sync::RwLock; +use std::time::{Duration, Instant}; + +#[cfg(feature = "ebpf")] +use super::linux_ebpf::EbpfSocketTracker; + +// When eBPF is enabled, use the full enhanced implementation +#[cfg(feature = "ebpf")] +mod ebpf_enhanced { + use super::*; + + /// Enhanced process lookup that combines eBPF (fast path) with procfs (fallback) + pub struct EnhancedLinuxProcessLookup { + ebpf_tracker: RwLock>>, + procfs_lookup: LinuxProcessLookup, + unified_cache: RwLock, + stats: RwLock, + cleanup_config: CleanupConfig, + last_cleanup: RwLock, + } + + pub struct ProcessCache { + lookup: HashMap, + last_refresh: Instant, + } + + #[derive(Debug, Clone)] + pub struct CleanupConfig { + pub cleanup_interval_secs: u64, + pub stale_threshold_secs: u64, + } + + impl Default for CleanupConfig { + fn default() -> Self { + Self { + cleanup_interval_secs: 30, + stale_threshold_secs: 60, + } + } + } + + #[derive(Debug, Default)] + pub struct LookupStats { + ebpf_hits: u64, + procfs_hits: u64, + cache_hits: u64, + total_lookups: u64, + ipv4_lookups: u64, + ipv6_lookups: u64, + tcp_lookups: u64, + udp_lookups: u64, + cache_entries: u64, + failed_lookups: u64, + ebpf_available: bool, + } + + impl EnhancedLinuxProcessLookup { + pub fn new() -> Result { + Self::new_with_config(CleanupConfig::default()) + } + + pub fn new_with_config(cleanup_config: CleanupConfig) -> Result { + let procfs_lookup = LinuxProcessLookup::new()?; + + let ebpf_tracker = match EbpfSocketTracker::new() { + Ok(tracker) => { + if tracker.is_some() { + info!("eBPF socket tracker initialized successfully"); + } else { + info!("eBPF not available, using procfs only"); + } + tracker.map(Box::new) + } + Err(e) => { + warn!( + "Failed to initialize eBPF tracker: {}, falling back to procfs", + e + ); + None + } + }; + + Ok(Self { + ebpf_tracker: RwLock::new(ebpf_tracker), + procfs_lookup, + unified_cache: RwLock::new(ProcessCache { + lookup: HashMap::new(), + last_refresh: Instant::now() - Duration::from_secs(3600), + }), + stats: RwLock::new(LookupStats::default()), + cleanup_config, + last_cleanup: RwLock::new(Instant::now() - Duration::from_secs(3600)), + }) + } + + /// Try eBPF lookup first, fall back to procfs + fn lookup_process_enhanced(&self, conn: &Connection) -> Option<(u32, String)> { + // Try eBPF first for TCP/UDP connections + if matches!(conn.protocol, Protocol::TCP | Protocol::UDP) { + debug!( + "Enhanced lookup: Trying eBPF for {}:{} -> {}:{} ({})", + conn.local_addr.ip(), + conn.local_addr.port(), + conn.remote_addr.ip(), + conn.remote_addr.port(), + match conn.protocol { + Protocol::TCP => "TCP", + Protocol::UDP => "UDP", + _ => "Unknown", + } + ); + + if let Some(result) = self.try_ebpf_lookup(conn) { + let mut stats = self.stats.write().unwrap(); + stats.ebpf_hits += 1; + debug!( + "Enhanced lookup: eBPF hit for PID {} ({})", + result.0, result.1 + ); + return Some(result); + } else { + debug!("Enhanced lookup: eBPF miss, falling back to procfs"); + } + } + + // Fall back to procfs approach + if let Some(result) = self.procfs_lookup.get_process_for_connection(conn) { + let mut stats = self.stats.write().unwrap(); + stats.procfs_hits += 1; + return Some(result); + } + + None + } + + fn try_ebpf_lookup(&self, conn: &Connection) -> Option<(u32, String)> { + let mut tracker_guard = self.ebpf_tracker.write().unwrap(); + let tracker = match tracker_guard.as_mut() { + Some(t) => { + debug!("eBPF lookup: Tracker available, performing lookup"); + t + } + None => { + debug!("eBPF lookup: No tracker available"); + return None; + } + }; + + let is_tcp = matches!(conn.protocol, Protocol::TCP); + + match tracker.lookup( + conn.local_addr.ip(), + conn.remote_addr.ip(), + conn.local_addr.port(), + conn.remote_addr.port(), + is_tcp, + ) { + Some(process_info) => { + debug!( + "eBPF lookup successful for {}:{} -> {}:{} - PID: {}, UID: {}, Comm: {}, Age: {}ns", + conn.local_addr.ip(), + conn.local_addr.port(), + conn.remote_addr.ip(), + conn.remote_addr.port(), + process_info.pid, + process_info.uid, + process_info.comm, + process_info.timestamp + ); + Some((process_info.pid, process_info.comm)) + } + None => { + debug!( + "eBPF lookup missed for {}:{} -> {}:{}", + conn.local_addr.ip(), + conn.local_addr.port(), + conn.remote_addr.ip(), + conn.remote_addr.port() + ); + None + } + } + } + + /// Get diagnostic statistics about lookup performance + #[allow(dead_code)] + pub fn get_stats(&self) -> LookupStats { + self.stats.read().unwrap().clone() + } + + /// Check if eBPF is available and functioning + pub fn is_ebpf_available(&self) -> bool { + self.ebpf_tracker + .read() + .unwrap() + .as_ref() + .map(|t| t.is_healthy()) + .unwrap_or(false) + } + + /// Perform periodic cleanup of stale eBPF map entries + fn maybe_cleanup_ebpf_map(&self) { + let now = Instant::now(); + let mut last_cleanup = self.last_cleanup.write().unwrap(); + + if now.duration_since(*last_cleanup).as_secs() + >= self.cleanup_config.cleanup_interval_secs + { + *last_cleanup = now; + drop(last_cleanup); + + // Perform cleanup + if let Some(tracker) = self.ebpf_tracker.write().unwrap().as_mut() { + let cleaned = + tracker.cleanup_stale_entries(self.cleanup_config.stale_threshold_secs); + if cleaned > 0 { + debug!("eBPF map cleanup: removed {} stale entries", cleaned); + } + } + } + } + } + + impl ProcessLookup for EnhancedLinuxProcessLookup { + fn get_process_for_connection(&self, conn: &Connection) -> Option<(u32, String)> { + // Perform periodic cleanup of stale eBPF entries + self.maybe_cleanup_ebpf_map(); + + let key = ConnectionKey::from_connection(conn); + + // Update protocol statistics + { + let mut stats = self.stats.write().unwrap(); + stats.total_lookups += 1; + + // Track IP version + match conn.local_addr.ip() { + IpAddr::V4(_) => stats.ipv4_lookups += 1, + IpAddr::V6(_) => stats.ipv6_lookups += 1, + } + + // Track protocol type + match conn.protocol { + Protocol::TCP => stats.tcp_lookups += 1, + Protocol::UDP => stats.udp_lookups += 1, + _ => {} + } + + // Update eBPF availability status + stats.ebpf_available = self.is_ebpf_available(); + } + + // Try cache first + { + let cache = self.unified_cache.read().unwrap(); + if cache.last_refresh.elapsed() < Duration::from_secs(2) + && let Some(process_info) = cache.lookup.get(&key) + { + let mut stats = self.stats.write().unwrap(); + stats.cache_hits += 1; + return Some(process_info.clone()); + } + } + + // Cache miss or stale - do enhanced lookup + if let Some(result) = self.lookup_process_enhanced(conn) { + // Update cache with the result + { + let mut cache = self.unified_cache.write().unwrap(); + cache.lookup.insert(key, result.clone()); + + let mut stats = self.stats.write().unwrap(); + stats.cache_entries = cache.lookup.len() as u64; + } + Some(result) + } else { + // Track failed lookups + let mut stats = self.stats.write().unwrap(); + stats.failed_lookups += 1; + None + } + } + + fn refresh(&self) -> Result<()> { + // Refresh the procfs lookup + self.procfs_lookup.refresh()?; + + // Update our cache timestamp + { + let mut cache = self.unified_cache.write().unwrap(); + cache.last_refresh = Instant::now(); + // Optionally clear cache to force fresh lookups + cache.lookup.clear(); + } + + debug!("Enhanced process lookup refreshed"); + Ok(()) + } + } + + impl Clone for LookupStats { + fn clone(&self) -> Self { + Self { + ebpf_hits: self.ebpf_hits, + procfs_hits: self.procfs_hits, + cache_hits: self.cache_hits, + total_lookups: self.total_lookups, + ipv4_lookups: self.ipv4_lookups, + ipv6_lookups: self.ipv6_lookups, + tcp_lookups: self.tcp_lookups, + udp_lookups: self.udp_lookups, + cache_entries: self.cache_entries, + failed_lookups: self.failed_lookups, + ebpf_available: self.ebpf_available, + } + } + } + + impl std::fmt::Display for LookupStats { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.total_lookups == 0 { + write!(f, "No lookups performed yet") + } else { + let cache_hit_rate = (self.cache_hits as f64 / self.total_lookups as f64) * 100.0; + let ebpf_rate = (self.ebpf_hits as f64 / self.total_lookups as f64) * 100.0; + let procfs_rate = (self.procfs_hits as f64 / self.total_lookups as f64) * 100.0; + let success_rate = ((self.total_lookups - self.failed_lookups) as f64 + / self.total_lookups as f64) + * 100.0; + + writeln!(f, "Process Lookup Statistics:")?; + writeln!( + f, + " Total lookups: {} (success: {:.1}%)", + self.total_lookups, success_rate + )?; + writeln!( + f, + " Cache: {} hits ({:.1}%)", + self.cache_hits, cache_hit_rate + )?; + writeln!( + f, + " eBPF: {} lookups ({:.1}%) | Available: {}", + self.ebpf_hits, ebpf_rate, self.ebpf_available + )?; + writeln!( + f, + " procfs: {} lookups ({:.1}%)", + self.procfs_hits, procfs_rate + )?; + writeln!( + f, + " Protocols - IPv4: {} | IPv6: {}", + self.ipv4_lookups, self.ipv6_lookups + )?; + write!( + f, + " Types - TCP: {} | UDP: {} | Cache entries: {}", + self.tcp_lookups, self.udp_lookups, self.cache_entries + ) + } + } + } +} + +// When eBPF is disabled, use a simpler procfs-only implementation +#[cfg(not(feature = "ebpf"))] +mod procfs_only { + use super::*; + + /// Simplified process lookup using only procfs (no eBPF) + pub struct EnhancedLinuxProcessLookup { + procfs_lookup: LinuxProcessLookup, + unified_cache: RwLock, + stats: RwLock, + } + + // Stub tracker for non-eBPF builds + pub struct EbpfSocketTracker; + + impl EbpfSocketTracker { + pub fn new() -> anyhow::Result> { + Ok(None) + } + + pub fn cleanup_stale_entries(&mut self, _stale_threshold_secs: u64) -> u32 { + 0 + } + + pub fn is_healthy(&self) -> bool { + false + } + } + + pub struct ProcessCache { + lookup: HashMap, + last_refresh: Instant, + } + + #[derive(Debug, Default)] + pub struct LookupStats { + procfs_hits: u64, + cache_hits: u64, + total_lookups: u64, + ipv4_lookups: u64, + ipv6_lookups: u64, + tcp_lookups: u64, + udp_lookups: u64, + cache_entries: u64, + failed_lookups: u64, + ebpf_available: bool, + } + + impl EnhancedLinuxProcessLookup { + pub fn new() -> Result { + Self::new_with_config() + } + + pub fn new_with_config() -> Result { + let procfs_lookup = LinuxProcessLookup::new()?; + + Ok(Self { + procfs_lookup, + unified_cache: RwLock::new(ProcessCache { + lookup: HashMap::new(), + last_refresh: Instant::now() - Duration::from_secs(3600), + }), + stats: RwLock::new(LookupStats::default()), + }) + } + + /// Get diagnostic statistics about lookup performance + #[allow(dead_code)] + pub fn get_stats(&self) -> LookupStats { + self.stats.read().unwrap().clone() + } + } + + impl ProcessLookup for EnhancedLinuxProcessLookup { + fn get_process_for_connection(&self, conn: &Connection) -> Option<(u32, String)> { + let key = ConnectionKey::from_connection(conn); + + // Update protocol statistics + { + let mut stats = self.stats.write().unwrap(); + stats.total_lookups += 1; + + // Track IP version + match conn.local_addr.ip() { + IpAddr::V4(_) => stats.ipv4_lookups += 1, + IpAddr::V6(_) => stats.ipv6_lookups += 1, + } + + // Track protocol type + match conn.protocol { + Protocol::TCP => stats.tcp_lookups += 1, + Protocol::UDP => stats.udp_lookups += 1, + _ => {} + } + + // eBPF is never available in this build + stats.ebpf_available = false; + } + + // Try cache first + { + let cache = self.unified_cache.read().unwrap(); + if cache.last_refresh.elapsed() < Duration::from_secs(2) + && let Some(process_info) = cache.lookup.get(&key) + { + let mut stats = self.stats.write().unwrap(); + stats.cache_hits += 1; + return Some(process_info.clone()); + } + } + + // Cache miss or stale - use procfs lookup + if let Some(result) = self.procfs_lookup.get_process_for_connection(conn) { + // Update cache with the result + { + let mut cache = self.unified_cache.write().unwrap(); + cache.lookup.insert(key, result.clone()); + + let mut stats = self.stats.write().unwrap(); + stats.cache_entries = cache.lookup.len() as u64; + stats.procfs_hits += 1; + } + Some(result) + } else { + // Track failed lookups + let mut stats = self.stats.write().unwrap(); + stats.failed_lookups += 1; + None + } + } + + fn refresh(&self) -> Result<()> { + // Refresh the procfs lookup + self.procfs_lookup.refresh()?; + + // Update our cache timestamp + { + let mut cache = self.unified_cache.write().unwrap(); + cache.last_refresh = Instant::now(); + // Optionally clear cache to force fresh lookups + cache.lookup.clear(); + } + + debug!("Enhanced process lookup refreshed"); + Ok(()) + } + } + + impl Clone for LookupStats { + fn clone(&self) -> Self { + Self { + procfs_hits: self.procfs_hits, + cache_hits: self.cache_hits, + total_lookups: self.total_lookups, + ipv4_lookups: self.ipv4_lookups, + ipv6_lookups: self.ipv6_lookups, + tcp_lookups: self.tcp_lookups, + udp_lookups: self.udp_lookups, + cache_entries: self.cache_entries, + failed_lookups: self.failed_lookups, + ebpf_available: self.ebpf_available, + } + } + } + + impl std::fmt::Display for LookupStats { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.total_lookups == 0 { + write!(f, "No lookups performed yet") + } else { + let cache_hit_rate = (self.cache_hits as f64 / self.total_lookups as f64) * 100.0; + let procfs_rate = (self.procfs_hits as f64 / self.total_lookups as f64) * 100.0; + let success_rate = ((self.total_lookups - self.failed_lookups) as f64 + / self.total_lookups as f64) + * 100.0; + + writeln!(f, "Process Lookup Statistics:")?; + writeln!( + f, + " Total lookups: {} (success: {:.1}%)", + self.total_lookups, success_rate + )?; + writeln!( + f, + " Cache: {} hits ({:.1}%)", + self.cache_hits, cache_hit_rate + )?; + writeln!(f, " eBPF: Not available (feature disabled)")?; + writeln!( + f, + " procfs: {} lookups ({:.1}%)", + self.procfs_hits, procfs_rate + )?; + writeln!( + f, + " Protocols - IPv4: {} | IPv6: {}", + self.ipv4_lookups, self.ipv6_lookups + )?; + write!( + f, + " Types - TCP: {} | UDP: {} | Cache entries: {}", + self.tcp_lookups, self.udp_lookups, self.cache_entries + ) + } + } + } +} + +// Re-export the appropriate implementation based on feature flag +#[cfg(feature = "ebpf")] +pub use ebpf_enhanced::*; + +#[cfg(not(feature = "ebpf"))] +pub use procfs_only::*; diff --git a/src/network/platform/mod.rs b/src/network/platform/mod.rs index 8486180..5981bdc 100644 --- a/src/network/platform/mod.rs +++ b/src/network/platform/mod.rs @@ -6,6 +6,10 @@ use std::net::SocketAddr; // Platform-specific modules #[cfg(target_os = "linux")] mod linux; +#[cfg(all(target_os = "linux", feature = "ebpf"))] +mod linux_ebpf; +#[cfg(all(target_os = "linux", feature = "ebpf"))] +mod linux_enhanced; #[cfg(target_os = "macos")] mod macos; #[cfg(target_os = "windows")] @@ -14,6 +18,8 @@ mod windows; // Re-export the appropriate implementation #[cfg(target_os = "linux")] pub use linux::LinuxProcessLookup; +#[cfg(target_os = "linux")] +// pub use linux_enhanced::EnhancedLinuxProcessLookup; #[cfg(target_os = "macos")] pub use macos::MacOSProcessLookup; #[cfg(target_os = "windows")] @@ -51,12 +57,36 @@ pub fn create_process_lookup_with_pktap_status( _pktap_active: bool, ) -> Result> { #[cfg(target_os = "macos")] - if _pktap_active { - log::info!("Using no-op process lookup - PKTAP provides process metadata"); - return Ok(Box::new(NoOpProcessLookup)); + { + use crate::network::platform::macos::MacOSProcessLookup; + + if _pktap_active { + log::info!("Using no-op process lookup - PKTAP provides process metadata"); + Ok(Box::new(NoOpProcessLookup)) + } else { + Ok(Box::new(MacOSProcessLookup::new()?)) + } } + #[cfg(target_os = "linux")] { + #[cfg(feature = "ebpf")] + { + // Try enhanced lookup first (with eBPF if available), fall back to basic + match linux_enhanced::EnhancedLinuxProcessLookup::new() { + Ok(enhanced) => { + log::info!("Using enhanced Linux process lookup (eBPF + procfs)"); + return Ok(Box::new(enhanced)); + } + Err(e) => { + log::warn!( + "Enhanced lookup failed, falling back to basic procfs: {}", + e + ); + } + } + } + // Use basic procfs lookup (either as fallback or when eBPF is not enabled) Ok(Box::new(LinuxProcessLookup::new()?)) } @@ -65,17 +95,19 @@ pub fn create_process_lookup_with_pktap_status( Ok(Box::new(WindowsProcessLookup::new()?)) } - #[cfg(target_os = "macos")] - { - Ok(Box::new(MacOSProcessLookup::new()?)) - } - #[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))] { Err(anyhow::anyhow!("Unsupported platform")) } } +/// Create a basic process lookup (procfs only on Linux) - for testing or fallback +#[cfg(target_os = "linux")] +#[allow(dead_code)] +pub fn create_basic_process_lookup() -> Result> { + Ok(Box::new(LinuxProcessLookup::new()?)) +} + /// Connection identifier for lookups #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct ConnectionKey { diff --git a/src/network/types.rs b/src/network/types.rs index 63c7908..7d45a50 100644 --- a/src/network/types.rs +++ b/src/network/types.rs @@ -185,6 +185,12 @@ pub struct TlsInfo { pub cipher_suite: Option, } +impl Default for TlsInfo { + fn default() -> Self { + Self::new() + } +} + impl TlsInfo { pub fn new() -> Self { Self { diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs new file mode 100644 index 0000000..e3de2df --- /dev/null +++ b/tests/integration_tests.rs @@ -0,0 +1,42 @@ +//! Integration tests for rustnet + +#[cfg(target_os = "linux")] +mod linux_tests { + use rustnet_monitor::network::platform::create_process_lookup_with_pktap_status; + + #[test] + fn test_process_lookup_creation() { + // Test that we can create a process lookup without panicking + let result = create_process_lookup_with_pktap_status(false); + assert!(result.is_ok(), "Should be able to create process lookup"); + } + + #[cfg(feature = "ebpf")] + #[test] + fn test_ebpf_enhanced_lookup() { + // This test verifies that the enhanced lookup can be created + // when eBPF feature is enabled + let result = create_process_lookup_with_pktap_status(false); + assert!( + result.is_ok(), + "Enhanced lookup should be created successfully" + ); + + // Just verify we got a lookup instance that can be refreshed + let lookup = result.unwrap(); + let refresh_result = lookup.refresh(); + assert!(refresh_result.is_ok(), "Refresh should work"); + } +} + +#[cfg(target_os = "macos")] +mod other_platforms { + use rustnet_monitor::network::platform::create_process_lookup_with_pktap_status; + + #[test] + fn test_other_platform_lookup() { + // Test that other platforms can create process lookups + let result = create_process_lookup_with_pktap_status(false); + assert!(result.is_ok(), "Should work on other platforms too"); + } +}