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.
This commit is contained in:
Marco Cadetg
2025-09-18 11:46:03 +02:00
committed by GitHub
parent 47d9748fba
commit 799d66cf86
20 changed files with 2543 additions and 32 deletions

View File

@@ -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

319
Cargo.lock generated
View File

@@ -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"

View File

@@ -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"]

231
EBPF_BUILD.md Normal file
View File

@@ -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.

View File

@@ -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

View File

@@ -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

71
build.rs Normal file
View File

@@ -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<Self> {
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
}

View File

@@ -1,4 +1,4 @@
use anyhow::{anyhow, Result};
use anyhow::{Result, anyhow};
use std::fs;
use std::path::PathBuf;

9
src/lib.rs Normal file
View File

@@ -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;

View File

@@ -84,6 +84,12 @@ pub struct PacketParser {
linktype: Option<i32>, // 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 {

View File

@@ -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<SocketTrackerSkel<'static>>,
_open_object: Box<std::mem::MaybeUninit<libbpf_rs::OpenObject>>,
}
impl EbpfLoader {
/// Attempt to load eBPF programs with graceful error handling
pub fn try_load() -> Result<Option<Self>> {
// 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<Self> {
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
}
}

View File

@@ -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<ConnInfo> 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<Option<ProcessInfo>> {
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<u32> {
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(())
}
}

View File

@@ -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,
}

View File

@@ -0,0 +1,246 @@
// Socket tracker eBPF program
// CO-RE (Compile Once - Run Everywhere) version using BTF
#include "vmlinux_min.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_endian.h>
#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";

View File

@@ -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__ */

View File

@@ -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<Option<Self>> {
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<ProcessInfo> {
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<ProcessInfo> {
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<ProcessInfo> {
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
}
}
}
}

View File

@@ -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<Option<Box<EbpfSocketTracker>>>,
procfs_lookup: LinuxProcessLookup,
unified_cache: RwLock<ProcessCache>,
stats: RwLock<LookupStats>,
cleanup_config: CleanupConfig,
last_cleanup: RwLock<Instant>,
}
pub struct ProcessCache {
lookup: HashMap<ConnectionKey, (u32, String)>,
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> {
Self::new_with_config(CleanupConfig::default())
}
pub fn new_with_config(cleanup_config: CleanupConfig) -> Result<Self> {
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<ProcessCache>,
stats: RwLock<LookupStats>,
}
// Stub tracker for non-eBPF builds
pub struct EbpfSocketTracker;
impl EbpfSocketTracker {
pub fn new() -> anyhow::Result<Option<Self>> {
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<ConnectionKey, (u32, String)>,
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> {
Self::new_with_config()
}
pub fn new_with_config() -> Result<Self> {
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::*;

View File

@@ -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<Box<dyn ProcessLookup>> {
#[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<Box<dyn ProcessLookup>> {
Ok(Box::new(LinuxProcessLookup::new()?))
}
/// Connection identifier for lookups
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct ConnectionKey {

View File

@@ -185,6 +185,12 @@ pub struct TlsInfo {
pub cipher_suite: Option<u16>,
}
impl Default for TlsInfo {
fn default() -> Self {
Self::new()
}
}
impl TlsInfo {
pub fn new() -> Self {
Self {

View File

@@ -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");
}
}