Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ The same backend publishes a socket snapshot every 5 seconds for the Host tab. T
**eBPF Mode (Default on Linux):**
- Uses kernel eBPF programs attached to socket syscalls
- Captures socket creation events with process context
- On Linux 5.11+, runs a one-shot task-file iterator after attaching the live probes to capture owners of sockets that predate RustNet, including other users' sockets in file-capability mode
- Provides lower overhead than procfs scanning
- Records the group leader's TGID, the acting TID, and credentials; the name, executable path, and PPID are enriched in user space via procfs
- **Limitations:**
Expand All @@ -273,7 +274,8 @@ The same backend publishes a socket snapshot every 5 seconds for the Host tab. T
- Note: CAP_NET_ADMIN is NOT required (uses read-only, non-promiscuous packet capture)

**Fallback Behavior:**
- If eBPF fails to load (permissions, kernel compatibility), automatically falls back to procfs mode
- If the task-file iterator is unavailable, keeps the live eBPF tracker and uses the procfs startup inventory
- If the live eBPF tracker fails to load, automatically falls back to procfs mode
- TUI Statistics panel shows active detection method

#### macOS
Expand Down
4 changes: 3 additions & 1 deletion ARCHITECTURE.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ RustNet 使用平台特定的 API 将网络连接与进程关联。每次归属
**eBPF 模式(Linux 默认):**
- 使用附加到 socket 系统调用的内核 eBPF 程序
- 捕获带进程上下文的 socket 创建事件
- 在 Linux 5.11 及更高版本上,先附加实时探针,再运行一次性的 task-file 迭代器,以捕获 RustNet 启动前已存在的 socket 所有者;使用文件 capabilities 运行时也包括其他用户的 socket
- 比 procfs 扫描开销更低
- 记录进程组组长的 TGID、当前线程的 TID 以及凭据;进程名、可执行路径和 PPID 在用户态通过 procfs 富化
- **局限性:**
Expand All @@ -268,7 +269,8 @@ RustNet 使用平台特定的 API 将网络连接与进程关联。每次归属
- 注意:不需要 CAP_NET_ADMIN(使用只读、非混杂包捕获)

**回退行为:**
- 如果 eBPF 加载失败(权限、内核兼容性),自动回退到 procfs 模式
- 如果 task-file 迭代器不可用,则保留实时 eBPF 追踪,并使用启动时的 procfs 清单
- 如果实时 eBPF 追踪器加载失败,则自动回退到 procfs 模式
- TUI 统计面板显示当前使用的检测方法

#### macOS
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Default Npcap Installations on Windows**: RustNet now finds Npcap in its
standard `System32\Npcap` directory, so WinPcap API-compatible mode is no
longer required. `--help` and `--version` also work without Npcap installed
- **Attribution of Pre-Existing Connections on Linux**: connections that were
already open before RustNet started keep their process name after privilege
reduction, including root services when RustNet runs with file capabilities
on Linux 5.11 and newer. A one-shot BPF task-file inventory and the
privileged procfs scan feed a validated fallback shown as the "startup
snapshot" match quality (#575)

### Removed
- **Ubuntu 25.10 (Questing) PPA**: the series reached end of life and
Expand Down
1 change: 1 addition & 0 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ RustNet は、各接続を所有するプロセス、通信量、状態、アプ
## 主な機能

- TCP、UDP、QUIC 接続とプロセスの対応付け。詳細には PID、実行ファイル、ユーザー/グループ名、照合の信頼度、全プラットフォーム共通の親プロセスチェーン(上限あり)を表示
- Linux 5.11 以降では、起動時の BPF task-file イテレーターにより、ファイル capabilities で実行した場合でも root や他ユーザーが所有する既存 socket を識別
- HTTP、TLS/SNI、DNS、SSH、QUIC、WireGuard、OpenVPN などの深層パケット解析
- TCP、QUIC ハンドシェイク、DNS 応答、ICMP エコーの往復時間(RTT)と、TCP の再送・順序入れ替わりをリアルタイム表示
- Host タブに TCP LISTEN ソケット、UDP BOUND エンドポイント、TCP 状態集計、観測 RTT、所有プロセス、インターフェース統計を表示
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ RustNet uses kernel eBPF programs by default on Linux for enhanced performance a
- Short-lived processes that exit before this enrichment runs keep the eBPF-recorded 16-character name

**Fallback Behavior:**
- On Linux 5.11 and newer, a one-shot BPF task-file iterator inventories sockets that were already open at startup, including sockets owned by root and other users when RustNet runs with file capabilities
- When eBPF fails to load or lacks sufficient permissions, RustNet automatically falls back to standard procfs-based process identification
- Standard mode resolves names the same way via procfs scanning, but with higher CPU overhead
- Older kernels and procfs-only builds resolve names through procfs scanning, which has higher CPU overhead and can only inspect socket owners visible to the RustNet user
- eBPF is enabled by default; no special build flags needed

To disable eBPF and use procfs-only mode, build with:
Expand Down
3 changes: 2 additions & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ RustNet 在 Linux 上默认使用内核 eBPF 程序进行进程识别,从而
- 在该富化流程运行前就已退出的短命进程,仍保留 eBPF 记录的 16 字符名称

**回退行为:**
- 在 Linux 5.11 及更高版本上,一次性的 BPF task-file 迭代器会清点 RustNet 启动前已经打开的 socket;使用文件 capabilities 运行时,也能识别 root 和其他用户拥有的 socket
- 当 eBPF 加载失败或权限不足时,RustNet 会自动回退到基于 procfs 的标准进程识别方式
- 标准模式通过 procfs 扫描以同样的方式解析进程名,但 CPU 开销更高
- 旧版内核和纯 procfs 构建通过 procfs 扫描解析进程名,CPU 开销更高,并且只能检查当前 RustNet 用户可见的 socket 所有者
- eBPF 默认启用,无需任何特殊编译参数

如需关闭 eBPF、仅使用 procfs 模式,请这样构建:
Expand Down
10 changes: 6 additions & 4 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,11 @@ If an attacker exploits a vulnerability in DPI/packet parsing:

Trade-off of the root uid drop: the procfs fallback for process attribution can
then only inspect processes owned by the target user, and Kubernetes log
directories under `/var/log/pods` may become unreadable. The eBPF fast path
(the default) is unaffected. If you rely on procfs-only attribution (e.g. a
build without eBPF) and need to attribute other users' processes, use
`--no-uid-drop`.
directories under `/var/log/pods` may become unreadable. On Linux 5.11 and
newer, the default eBPF path takes a one-shot task-file socket inventory before
the drop, so pre-existing sockets owned by other users remain attributable. If
you rely on procfs-only attribution, such as a build without eBPF or an older
kernel, and need to attribute other users' processes, use `--no-uid-drop`.

### Graceful Degradation

Expand Down Expand Up @@ -276,6 +277,7 @@ When using eBPF for enhanced process detection (default on Linux):
- Requires additional kernel capabilities (`CAP_BPF`, `CAP_PERFMON`)
- eBPF programs are verified by kernel before loading
- Limited to read-only operations (no packet modification)
- On Linux 5.11+, a one-shot task-file iterator inventories pre-existing socket owners
- Automatically falls back to procfs if eBPF fails

## Threat Model
Expand Down
8 changes: 5 additions & 3 deletions SECURITY.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,10 @@ RustNet 处理不受信任的网络数据,因此纵深防御至关重要。本
```

root uid 降权的权衡:降权后,procfs 回退路径的进程归属只能检查目标用户拥有的进程,
`/var/log/pods` 下的 Kubernetes 日志目录也可能不可读。eBPF 快速路径(默认)不受影响。
如果依赖纯 procfs 归属(如未启用 eBPF 的构建)且需要归属其他用户的进程,请使用
`--no-uid-drop`。
`/var/log/pods` 下的 Kubernetes 日志目录也可能不可读。在 Linux 5.11 及更高版本上,
默认 eBPF 路径会在降权前通过一次性的 task-file 迭代器清点 socket,因此其他用户拥有的
既有 socket 仍可归属。如果依赖纯 procfs 归属(如未启用 eBPF 的构建或旧版内核)且需要
归属其他用户的进程,请使用 `--no-uid-drop`。

### 优雅降级

Expand Down Expand Up @@ -271,6 +272,7 @@ RustNet 完全在本地运行:
- 现代内核需要额外的 Linux capabilities(`CAP_BPF`、`CAP_PERFMON`)
- eBPF 程序在加载前由内核验证
- 仅限只读操作(不修改数据包)
- 在 Linux 5.11 及更高版本上,一次性的 task-file 迭代器会清点既有 socket 的所有者
- 如果 eBPF 失败,自动回退到 procfs

## 威胁模型<a id="threat-model"></a>
Expand Down
8 changes: 8 additions & 0 deletions crates/rustnet-core/src/network/types/identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ pub enum MatchQuality {
ProcfsExact,
/// procfs match that needed a relaxed key.
ProcfsRelaxed,
/// Matched the validated socket-table snapshot taken at startup. The
/// owner came from BPF or a privileged procfs scan, and both the socket
/// inode and process identity still match their startup values.
StartupSnapshot,
/// The backend reported an owner but could not report match provenance.
Unspecified,
}
Expand All @@ -48,6 +52,7 @@ impl MatchQuality {
Self::ListenerSocket => "listener socket",
Self::ProcfsExact => "procfs exact",
Self::ProcfsRelaxed => "procfs relaxed",
Self::StartupSnapshot => "startup snapshot",
Self::Unspecified => "unspecified",
}
}
Expand All @@ -65,6 +70,7 @@ impl MatchQuality {
Self::ListenerSocket => "listener-socket",
Self::ProcfsExact => "procfs-exact",
Self::ProcfsRelaxed => "procfs-relaxed",
Self::StartupSnapshot => "startup-snapshot",
Self::Unspecified => "unspecified",
}
}
Expand Down Expand Up @@ -379,6 +385,7 @@ mod tests {
(MatchQuality::ListenerSocket, "listener-socket"),
(MatchQuality::ProcfsExact, "procfs-exact"),
(MatchQuality::ProcfsRelaxed, "procfs-relaxed"),
(MatchQuality::StartupSnapshot, "startup-snapshot"),
(MatchQuality::Unspecified, "unspecified"),
];

Expand All @@ -391,6 +398,7 @@ mod tests {
assert!(MatchQuality::ExactTuple.is_exact());
assert!(MatchQuality::ProcfsExact.is_exact());
assert!(!MatchQuality::ProcfsRelaxed.is_exact());
assert!(!MatchQuality::StartupSnapshot.is_exact());
assert!(!MatchQuality::Unspecified.is_exact());
}

Expand Down
14 changes: 11 additions & 3 deletions crates/rustnet-host/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,13 @@ When a platform can't use its optimal method, `ProcessLookup::get_degradation_re
reports why (e.g. missing `CAP_BPF`, no root for PKTAP) via `DegradationReason`,
which front-ends can surface to the user.

Both Linux BPF objects use CO-RE for safe socket field access and therefore
All Linux BPF objects use CO-RE for safe kernel field access and therefore
require usable target BTF. A compatible target-BTF kernel tries fentry/fexit
first and legacy kprobes second. A kernel without usable target BTF falls
directly to procfs rather than relying on fixed structure offsets.
first and legacy kprobes second. On Linux 5.11 and newer, a separate one-shot
task-file iterator captures socket owners that predate the live probes. Its
failure leaves the selected live tracker running and falls back to the procfs
startup inventory. A kernel without usable target BTF falls directly to procfs
rather than relying on fixed structure offsets.

## Linux eBPF integration matrix

Expand All @@ -99,6 +102,11 @@ sudo -E cargo test -p rustnet-host --features ebpf -- \
--ignored --exact \
linux::ebpf::tracker_libbpf::integration_tests::legacy_kprobe_socket_attribution_matrix \
--nocapture

sudo -E cargo test -p rustnet-host --features ebpf -- \
--ignored --exact \
linux::ebpf::task_file::tests::kernel_iterator_reports_current_process_socket \
--nocapture
```

## Scope
Expand Down
8 changes: 4 additions & 4 deletions crates/rustnet-host/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@ fn compile_ebpf_programs() {
let vmlinux_include_path =
get_vmlinux_header(vmlinux_arch).expect("Failed to locate bundled vmlinux.h");

for backend in ["fentry", "kprobe"] {
let src = format!("src/linux/ebpf/programs/socket_tracker_{backend}.bpf.c");
let out = out_dir.join(format!("socket_tracker_{backend}.skel.rs"));
for program in ["fentry", "kprobe", "task_file"] {
let src = format!("src/linux/ebpf/programs/socket_tracker_{program}.bpf.c");
let out = out_dir.join(format!("socket_tracker_{program}.skel.rs"));

println!("cargo:warning=Building eBPF {backend} backend using libbpf-cargo");
println!("cargo:warning=Building eBPF {program} program using libbpf-cargo");

SkeletonBuilder::new()
.source(&src)
Expand Down
2 changes: 2 additions & 0 deletions crates/rustnet-host/src/linux/ebpf/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@

mod loader;
mod maps_libbpf;
mod task_file;
mod tracker_libbpf;

pub(super) use task_file::snapshot_task_file_owners;
pub(super) use tracker_libbpf::LibbpfSocketTracker as EbpfSocketTracker;

use crate::MatchQuality;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#include "vmlinux.h"

#include <bpf/bpf_core_read.h>
#include <bpf/bpf_helpers.h>

#define TASK_COMM_LEN 16
#define TASK_FILE_OWNER_SIZE 32

/* Binary record consumed by task_file.rs. */
struct task_file_owner
{
__u64 inode;
__u32 tgid;
__u32 uid;
char comm[TASK_COMM_LEN];
};

_Static_assert(sizeof(struct task_file_owner) == TASK_FILE_OWNER_SIZE,
"task-file owner ABI size changed");

/*
* One-shot startup inventory for sockets that existed before the live
* fentry/kprobe programs were attached. The kernel's task_file iterator skips
* threads that share their group leader's file table, so normal thread groups
* do not emit duplicate owners.
*/
SEC("iter/task_file")
int snapshot_task_file_owners(struct bpf_iter__task_file *ctx)
{
struct task_struct *task = ctx->task;
struct file *file = ctx->file;

if (!task || !file || !bpf_sock_from_file(file))
return 0;

struct task_file_owner owner = {};
owner.inode = BPF_CORE_READ(file, f_inode, i_ino);
owner.tgid = BPF_CORE_READ(task, tgid);
owner.uid = BPF_CORE_READ(task, cred, euid.val);

struct task_struct *leader = BPF_CORE_READ(task, group_leader);
if (!leader || owner.inode == 0 || owner.tgid == 0)
return 0;

if (BPF_CORE_READ_STR_INTO(&owner.comm, leader, comm) <= 0 ||
owner.comm[0] == '\0')
return 0;

bpf_seq_write(ctx->meta->seq, &owner, sizeof(owner));
return 0;
}

char LICENSE[] SEC("license") = "Dual BSD/GPL";
Loading