Skip to content

Latest commit

 

History

History
506 lines (362 loc) · 30.6 KB

ARCHITECTURE.zh-CN.md

File metadata and controls

506 lines (362 loc) · 30.6 KB

English | 简体中文

架构

文档版本: main 分支上的本指南描述开发中的代码,可能包含尚未发布的实现细节。使用 v1.7.0 时,请参阅 v1.7.0 架构指南。运行 rustnet --version 可查看已安装的版本。

本文档描述 RustNet 的技术架构和实现细节。

目录

Crate 结构

RustNet 是一个由五个 crate 组成的 Cargo 工作区。分析逻辑、捕获后端、进程归属和沙箱各自位于独立的可复用库 crate 中;二进制 crate 将它们组合成 TUI 应用。

Crate 类型 职责
rustnet-core 库 与平台和捕获无关的分析核心:数据包解析、协议/连接类型、深度包检测、链路层解析器、连接合并、DNS/GeoIP/OUI 查找、可复用的 ConnectionTracker,以及有界的保留进程活动计量。仅操作字节切片和已解析的结构,不依赖 libpcap、原始套接字或操作系统进程表。
rustnet-capture 库 基于 libpcap/Npcap 的数据包捕获后端:设备选择、BPF 过滤器、macOS PKTAP、TUN/TAP,以及原始帧 PacketReader。
rustnet-host 库 按连接进程归属及主机 TCP/UDP 套接字清单:Linux 上的 eBPF/procfs、macOS 上的 PKTAP/lsof、Windows 上的 ETW/IP Helper,以及 FreeBSD 上的 sockstat。负责 eBPF 构建工具链及内置的 vmlinux.h。
rustnet-sandbox 库 初始化完成后的沙箱与 root 权限降级,统一入口 apply_sandbox:Linux 上的 Landlock + 能力降级、macOS 上的 Seatbelt、Windows 上的令牌特权移除 + 作业对象,以及 Linux/macOS/FreeBSD 共享的 uid 降级。不依赖任何其他工作区 crate。
rustnet-monitor(二进制 rustnet) 二进制 面向用户的应用:CLI、TUI 和应用事件循环。以 ConnectionTracker 作为唯一数据来源(dogfooding)。

包名为 rustnet-monitor,因为 rustnet 这个 crate 名称在 crates.io 上已被占用;安装后的二进制文件名为 rustnet。

依赖关系图

flowchart TD
    BIN[rustnet-monitor<br/>二进制: rustnet]
    CAP[rustnet-capture]
    HOST[rustnet-host]
    SBX[rustnet-sandbox]
    CORE[rustnet-core]

    BIN --> CAP
    BIN --> HOST
    BIN --> SBX
    BIN --> CORE
    CAP --> CORE
    HOST --> CORE
Loading

依赖关系是无环的:rustnet-core 没有任何工作区内依赖,rustnet-capture 和 rustnet-host 都仅依赖它,而 rustnet-sandbox 不依赖任何工作区 crate。让 rustnet-core 保持为叶子节点,使其可以独立发布和复用 —— 无界面的前端(例如 Prometheus 导出器)可以仅组合 rustnet-capture + rustnet-core 而无需 TUI;examples/headless.rs 展示了包含 rustnet-host 与 rustnet-sandbox 的完整组合。

重导出门面

为了将拆分对二进制内部保持透明,src/network/mod.rs 重导出 rustnet_core::network::* 和 rustnet_capture(作为 capture),因此现有的 crate::network::* 路径、集成测试和基准测试无需改动即可编译。src/network/platform 模块如今只是接入 rustnet-host 进程与套接字查找的一层薄壳;各平台的接口统计提供者位于 rustnet-core(入口 interface_stats::create_stats_provider),沙箱与 root uid 降级位于 rustnet-sandbox,二进制直接使用这些 crate。

前端模块

src/main.rs 委托库中的 bootstrap::run 执行启动。src/bootstrap.rs 负责解析选项、验证输出路径、准备特权资源、应用沙箱、通过运行时许可启动工作线程并选择前端。src/tui.rs 负责交互事件循环。无界面模式的调度位于 src/headless/mod.rs,版本化快照投影位于 src/headless/schema.rs,有界最新值写入器位于 src/headless/output.rs。两个前端共用应用状态、过滤器、信号标志和由运行时持有的关闭报告。

配置的 JSON 日志、PCAP、PCAP 旁路记录和 PCAPNG 输出会先验证路径,再检查已打开文件的身份,之后才截断捕获输出。其文件描述符在 UID 降级及沙箱切换期间保持打开,不授予输出路径写入权限,也不会在之后重新打开路径。关闭时先停止并等待生产者,再重建最终快照;如果工作线程超过期限,无界面模式的终止记录会报告 stopping,而不会声称关闭已完成。

多线程架构

RustNet 采用多线程架构以实现高效的数据包处理:

flowchart LR
    PC[数据包捕获<br/>libpcap]
    CH([Crossbeam Channel])
    PP[数据包处理器<br/>线程 0..N]
    PE[进程信息补全<br/>平台 API]
    DM[(活跃连接)]
    HI[(历史连接池<br/>最多 5,000 条)]
    SP[快照提供器]
    UI[/RwLock&lt;Vec&lt;Connection&gt;&gt;<br/>供 UI 使用/]
    CT[清理线程]
    PA[进程活动采样器]
    PS[/进程活动快照/]

    PC -- 数据包 --> CH --> PP --> DM
    PE --> DM
    DM --> SP --> UI
    DM --> CT
    CT --> HI
    HI --> SP
    SP --> PA --> PS
Loading

核心组件

1. 数据包捕获线程

使用 libpcap 从网络接口捕获原始数据包。该线程独立运行,将数据包送入 Crossbeam channel 进行处理。

职责:

  • 打开网络接口进行数据包捕获(非混杂、只读模式)
  • 按需应用 BPF 过滤器
  • 捕获原始数据包
  • 如果启用了 --pcap-export,将数据包流式写入 PCAP 文件(直接磁盘写入,无内存缓冲)
  • 如果启用了 --pcapng-export,将解析后的数据包送入带注释 PCAPNG writer(有界 best-effort 队列)
  • 将数据包发送到处理队列

2. 数据包处理器

多个工作线程(默认最多 4 个,基于 CPU 核心数)解析数据包并执行深度包检测(DPI)分析。

职责:

  • 解析 Ethernet、IP、TCP、UDP、ICMP、ARP 头部
  • 提取连接五元组(协议、源 IP、源端口、目的 IP、目的端口)
  • 执行 DPI 检测应用协议:
    • 带主机信息的 HTTP
    • 带 SNI(Server Name Indication)的 HTTPS/TLS
    • DNS 查询和响应
    • 带版本检测的 SSH 连接
    • 带命令、响应代码、用户名、服务器软件和系统类型的 FTP 控制通道
    • 带 CONNECTION_CLOSE 帧检测的 QUIC 协议
    • 带报文类型、版本和客户端标识符的 MQTT
    • MySQL 经典握手/SSLRequest、Redis RESP 命令以及 PostgreSQL 启动消息/SSLRequest,使用有界的单包解析(自 v1.7.0 起可用)
    • BitTorrent 握手和 DHT 消息
    • WireGuard 和 OpenVPN 隧道流量
    • 用于 WebRTC 和 NAT 穿越的 STUN
    • 带版本、模式和 stratum 的 NTP
    • 用于本地名称解析的 mDNS 和 LLMNR
    • 带消息类型和主机名的 DHCP
    • 带 PDU 类型的 SNMP(v1、v2c、v3)
    • 用于 UPnP 设备发现的 SSDP
    • NetBIOS 名称服务和数据报服务
  • 追踪连接状态和生命周期
  • 在 DashMap 中更新连接元数据
  • 计算带宽指标

3. 进程信息补全

使用平台特定的 API 将网络连接与运行中的进程关联。该组件定期运行,为连接数据补充进程信息。

职责:

  • 将 socket inode 映射到进程 ID
  • 解析进程名和命令行
  • 用进程信息更新连接记录
  • 处理与权限相关的回退

各平台的详情见平台特定实现。

4. 快照提供器

按固定间隔(默认 500ms)创建连接数据的一致快照供 UI 使用。这确保 UI 拥有稳定的连接视图,避免竞争条件。

职责:

  • 按配置间隔从 DashMap 读取
  • 根据用户条件应用过滤(localhost 等)
  • 按用户选择的列排序连接
  • 为 UI 渲染创建不可变快照
  • 为 UI 线程提供 RwLock 保护的 Vec

5. DNS 归因缓存

rustnet-core 中的 network::dns_attribution::DnsAttributionCache 是一个由线上观测到的 DNS 响应填充的 IpAddr -> 最近域名 映射,由 ConnectionTracker 持有(通过 TrackerConfig::dns_attribution 开关,默认开启)。当连接没有可用于识别的 SNI / Host 头(握手后的加密 QUIC、纯 TCP、分片的 ClientHello)时,缓存会提供一个从用户刚执行的 DNS 解析推断出的主机名。

该设计是事件驱动的:创建时无法归因的连接(尚无新鲜 DNS)会登记到一个以远端 IP 为键的侧索引中,随后到达的匹配 DNS 响应会唤醒这些等待者。没有逐包轮询,也没有定时器。

数据流:

1. DNS 响应 ──record──► IP→域名 缓存            IP→[ConnectionKey]
                             ▲    │              待归因等待者
                             │    │                  ▲    │
2. 新连接 ──attribute────────┘    │                  │    │ 3. DNS 到达时
      │  命中:写入 attributed_hostname ◄────────────┼──────  排空等待者
      └─ 未命中:登记等待 ───────────────────────────┘

4. 清理线程(每次清扫):cleanup_tick(now)
    ├─ 清除超过保留期(10 分钟)的缓存条目
    └─ 清除超过 10 秒新鲜窗口的待归因登记

5. 连接移除:forget_pending(remote_ip, key)

缓存属性:

  • 新鲜窗口:10 秒,对称地应用于缓存的 IP -> 域名 条目(仅当针对远端 IP 的 DNS 响应在此窗口内被观测到时,连接才会被归因)和待归因登记(新的 DNS 解析只会归因在此窗口内建立的连接)。与 Little Snitch 的 MAX_QUERY_AGE 一致。
  • 保留期(缓存条目):10 分钟。更旧的条目会被清除,且永远不用于归因。
  • 上限:缓存最多 8192 个 IP,每个 IP 最多 4 个最近域名,每个 IP 最多 256 个待归因等待者。
  • 每个连接首写生效:一旦打上标签,就不会被指向同一 IP 的后续解析重新标记。

热路径开销:已建立连接的逐包 DNS 查找为零。每个连接只在创建时尝试归因一次,之后每个匹配的 DNS 响应至多再尝试一次。载荷中已携带主机名的连接(通过统一的 ApplicationProtocol::hostname() 访问器检查,即 HTTPS 和 QUIC 的 TLS SNI 以及 HTTP Host: 头)和归因无意义的协议(DNS、mDNS、LLMNR、DHCP、SSDP、NetBIOS、ARP)在触碰缓存之前就会短路返回。

竞争安全:attribute() 先把连接登记到待归因索引,然后重查缓存。如果同一 IP 的并发 record_and_drain_pending() 恰好落在查找和登记之间,重查会为连接打上标签;此时已过时的登记是无害的,会在连接被清理时由 forget_pending 移除,或在新鲜窗口过后由待归因清理移除。

与 network::dns::DnsResolver 的区别:后者通过系统解析器执行反向 DNS(IP 到 PTR);归因缓存则是从观测到的数据包中收集的正向 DNS(域名到 IP)。两者互补。自 v1.7.0 起,UI 在 Remote 中显示反向 DNS,在 App 中显示 DNS 归因;SNI / Host 名称优先于推断名称。

CNAME 链不需要单独的映射:DNS DPI 解析器记录的是原始问题名称,应答中的 A/AAAA 记录直接映射到它。在线路层捕获看到的信号比 eBPF 套接字层方案少(除非同时捕获 lo,否则看不到应用到 stub 的流量;看不到 D-Bus 解析;看不到 DoH/DoT 明文);这是已知限制。

被动 DNS 分析

network::dns_analytics::DnsAnalyticsTracker 是由 ConnectionTracker 持有的独立有界汇总器。对于捕获到的单播 UDP DNS,它使用以本机为方向的连接键和 16 位事务 ID,将每个出站查询与入站响应配对。重传会刷新待处理项,无法匹配的响应会被忽略,查询在 10 秒内没有响应时会最终记为超时。

生成快照时会清理超过一小时的已完成事务和过期的近期问题。数据包处理会检查待处理查询超时并执行容量限制,不会扫描保留的历史记录。Host 快照按所选的 60 秒、五分钟或一小时窗口过滤;默认快照方法和 Overview 的健康状态仍使用 60 秒。追踪器最多保存 4,096 个待处理事务和 8,192 个已完成事务,容量压力会在 Host 的 DNS 视图中显示 sampled 标记。快照包含响应码与 NODATA 总数、延迟分位数与区间、规范化查询名称汇总,以及供 Overview 使用的 DNS 健康分类。ConnectionTracker::clear() 会同时重置待处理和已完成的 DNS 状态。

健康分类要求足够的观测证据。NXDOMAIN 算作解析器正常响应,SERVFAIL、REFUSED、其他响应码和超时算作运行失败。failing 或 no-replies 状态至少需要三次失败观测。degraded 状态需要至少五次已完成观测且运行失败率达到 20%,或者至少五个延迟样本且 p95 达到 500 ms。未观测到明文 DNS 时保持 unknown,而不会判定为健康,因为加密 DNS、缓存、捕获丢包和接口选择都可能隐藏活动。

**开发版:**回环 DNS 根据查询/响应角色确定方向,使本地 IPv4 和 IPv6 存根响应能够匹配客户端流。除事务 ID 外,还校验可用的查询名称和类型。重传保留首次请求时间,用于分析延迟和待处理时间;10 秒无活动超时从最近一次重传重新计时。

快照还按观测到的解析器端点汇总,并最多保留 64 条错误或慢响应记录,保留 15 分钟。各解析器使用相同的健康阈值,任何解析器异常都会使原本正常的总状态降为 degraded。独立的问题历史按观测时间排序,在实时样本过期后仍可查看,并随追踪器一同清空。这些数据来自被动 UDP 观测,不依赖系统解析器配置,也不执行主动探测。

滚动视图与自启动以来视图共用 DNS 汇总器,统一处理结果、解析器健康状态、查询名称和快照。滚动快照从保留事务重建汇总,使用精确延迟样本;累计汇总对每个最终完成的事务只更新一次,使用有界延迟直方图,不受原始明细过期或淘汰影响。已跟踪事务的总计完整保留,明细映射限制为前 128 个解析器端点和 1,024 个查询名称/类型组合。固定对数直方图提供明确标记的近似分位数,最大延迟、延迟区间计数和慢响应阈值分类保持精确。明细限制与查询关联丢失分别标识。清空会重置两个汇总器,并将累计结果标记为 since-clear。

Host 的 DNS、网络事件和接口错误共用 TimeWindow 选择器。事件与接口计数使用有界的一小时 1 秒桶,并独立保留会话累计值。接口轮询先建立基线,再记录增量;计数下降视为新的计数周期,接口消失则删除历史。清空会重置观测计数并重新建立接口基线,不修改 OS 计数。

6. 清理线程

使用智能的、协议感知的超时机制移除不活跃的连接。这防止内存泄漏并保持连接列表的相关性。当启用 --pcap-export 时,连接关闭时还会将连接元数据(PID、进程名、时间戳)流式写入 JSONL sidecar 文件。

超时策略:

TCP 连接

  • HTTP/HTTPS(通过 DPI 检测):10 分钟 —— 支持 HTTP keep-alive
  • SSH(通过 DPI 检测):30 分钟 —— 适应长交互会话
  • 普通已建立连接:5 分钟
  • TIME_WAIT:30 秒(标准 TCP 超时)
  • CLOSED:15 秒(终止状态归档宽限期)
  • SYN_SENT、FIN_WAIT 等:30-60 秒

UDP 连接

  • HTTP/3(带 HTTP 的 QUIC):10 分钟 —— 连接复用
  • HTTPS/3(带 HTTPS 的 QUIC):10 分钟 —— 连接复用
  • 基于 UDP 的 SSH:30 分钟 —— 长会话
  • DNS:30 秒 —— 短查询
  • 普通 UDP:60 秒 —— 标准超时

QUIC 连接(检测到的状态)

  • 已连接:默认 3 分钟,或当存在对端的 max_idle_timeout 传输参数时使用该值
  • 带 CONNECTION_CLOSE 帧:15 秒
  • Initial/Handshaking:60 秒(允许连接建立)
  • Draining/Closed:15 秒(终止状态归档宽限期)

终止状态的连接会保留首次进入终止状态的时间戳。重复的关闭流程数据包会更新最终计数器,但不会推迟归档。当新的 TCP SYN 复用正在关闭的连接元组时,清理与数据包摄取会执行一次生命周期安全的转换,将旧代次归档为不可变记录,并创建新的活跃代次。一条有界的 30 秒 tombstone 记录可防止延迟的 TCP 关闭流程数据包生成虚假的已建立连接行。

视觉陈旧度指示器:

连接的清理计时达到超时时间的一半,且显示的双向流量速率都衰减为零后, 行左侧会出现条纹,并显示移除倒计时。真彩色主题下,标识信息列会逐渐接近 柔和色,状态等信号列则保留原有颜色。随着清理时间临近,条纹与倒计时由黄 色渐变为红色。带背景色的选中行保留标识信息列的颜色,以便阅读。启用历史 记录时,未选中的已归档连接显示为灰色。

7. 速率刷新线程

每 500ms 更新带宽计算,采用温和衰减。这提供平滑的带宽可视化,避免突变。

职责:

  • 计算下载和上传的字节/秒
  • 对旧测量值应用指数衰减
  • 更新可视化带宽指示器
  • 维护包速率的滚动窗口

8. DashMap

并发哈希表(DashMap<ConnectionKey, Connection>)用于存储连接状态。它使用分片锁支持并发访问;生命周期转换还使用独立的同步锁。

关键特性:

  • 细粒度锁定(per-shard)
  • 对连接条目进行分片访问
  • 安全的并发读写
  • 高并发负载下的高性能

平台特定实现

进程查找

RustNet 使用平台特定的 API 将网络连接与进程关联。每次归属还会附带有层级上限的父进程链(最多四级祖先,包含 PID、名称、可执行文件路径和启动时间),在 Details 标签页中以进程树展示,并导出到 JSONL。

同一后端每 5 秒为 Host 标签页发布一次套接字快照。该快照独立于数据包捕获,因此可包含尚未产生数据包的 TCP LISTEN 套接字和 UDP BOUND 端点。TCP 状态汇总来自此原生快照,RTT 仍是基于已捕获数据包的观测指标。各平台的实现如下:

Linux

标准模式(procfs):

  • 解析 /proc/net/tcp 和 /proc/net/udp 获取 socket inode
  • 同时读取 tcp6、udp 和 udp6,并在 Host 快照中保留所有原生 TCP 状态
  • 遍历 /proc/<pid>/fd/ 查找 socket 文件描述符
  • 将 inode 映射到进程 ID,并从 /proc/<pid>/comm 解析进程名,同时借助可执行文件名恢复被 comm 截断的名称
  • 从 /proc/<pid>/ 解析可执行路径、PPID 以及 UID/GID

eBPF 模式(Linux 默认):

  • 使用附加到 socket 系统调用的内核 eBPF 程序
  • 捕获带进程上下文的 socket 创建事件
  • 在 Linux 5.11 及更高版本上,先附加实时探针,再运行一次性的 task-file 迭代器,以捕获 RustNet 启动前已存在的 socket 所有者;使用文件 capabilities 运行时也包括其他用户的 socket
  • 比 procfs 扫描开销更低
  • 记录进程组组长的 TGID、当前线程的 TID、凭据和进程名
  • 自 v1.7.0 起,还在观察 socket 时保存可执行文件和直接父进程(PID、名称、可执行文件和启动时间),在退出或 exec 后继续保留。只有进程启动时间仍匹配时,才通过 procfs 扩展父进程链。
  • 局限性:
    • 进程名来源于内核 16 字节的 comm 字段,可使用保留的可执行文件名恢复截断。可执行路径最多保留 255 字节,最多遍历 32 个目录或挂载点;不完整路径保持未知。脚本显示运行中的解释器。直接父进程之外的祖先信息仍尽力提供。
  • Linux capabilities 需求:
    • 现代 Linux(5.8+):CAP_NET_RAW(包捕获)、CAP_BPF、CAP_PERFMON(eBPF)
      • 旧版 Linux(pre-5.8):eBPF 需要宽泛的 CAP_SYS_ADMIN;RustNet 安装包不会自动授予它,并会回退到 procfs
    • 注意:不需要 CAP_NET_ADMIN(使用只读、非混杂包捕获)

回退行为:

  • 如果 task-file 迭代器不可用,则保留实时 eBPF 追踪,并使用启动时的 procfs 清单
  • 如果实时 eBPF 追踪器加载失败,则自动回退到 procfs 模式
  • TUI 统计面板显示当前使用的检测方法

macOS

PKTAP 模式(使用 sudo 时):

  • 使用 PKTAP(Packet Tap)内核接口
  • 直接从包元数据提取 PID 和进程名称
  • 使用 libproc 补充 PPID、可执行文件路径和有效 UID/GID
  • 需要 root 特权(特权内核接口)
  • 比 lsof 更快更准确

lsof 模式(无 sudo 或回退时):

  • 使用 lsof -i -n -P -l 列出网络连接及数字 UID
  • 即使 PKTAP 提供数据包进程元数据,Host 标签页仍持续使用 lsof 套接字清单
  • 解析输出以将 socket 与进程关联,然后使用 libproc 获取 PPID 和可执行文件路径
  • CPU 开销更高,但无需 root
  • 当 PKTAP 不可用时自动使用

检测:

  • TUI 统计面板根据当前方法显示 "pktap" 或 "lsof"
  • 自动选择最佳可用方法

FreeBSD

  • 使用 sockstat -s 将 TCP 和 UDP socket 关联到进程并保留原生 TCP 状态
  • 使用原生 KERN_PROC_PID 和 KERN_PROC_PATHNAME sysctl 查询补充 PPID、有效 UID/GID 和可执行文件路径
  • 在每次 socket 表刷新周期内按 PID 缓存进程详情

Windows

ETW + IP Helper API:

  • 为内核网络与进程 provider 启动实时用户 trace session
  • 使用包含进程 ID、方向和连接元组的 TCP/UDP 事件
  • 缓存连接元组所有权和进程生命周期元数据,使短生命周期进程退出后仍可归属
  • 使用 GetExtendedTcpTable 和 GetExtendedUdpTable 进行校准、补全缓存未命中项,并在 ETW 无法启动时完全回退
  • Host 套接字清单复用相同的 IP Helper owner 表,其中包括 TCP LISTEN 和所有已分配的 UDP 端点
  • 支持 IPv4 和 IPv6 上的 TCP 与 UDP
  • 使用生命周期事件或 OpenProcess 和 QueryFullProcessImageNameW 解析进程名

需求:

  • 启动 ETW session 可能需要 Administrator 或 **Performance Log Users(性能日志用户)**组成员身份,具体仍取决于 provider 安全策略
  • ETW 失败不会中断捕获;检测模式会变为 IP Helper。该模式基于快照,可能遗漏短生命周期进程
  • 数据包捕获需要 Npcap,其捕获权限与 ETW 权限相互独立

网络接口

抓包后端通过 libpcap/Npcap 列出设备。未指定 --interface 时, 它会选择一个合适的活动设备。数据包解析器另行收集主机已分配的地址, 用于判断流量方向:

  • Linux、macOS 和 FreeBSD:pnet_datalink 枚举已分配的 IPv4 和 IPv6 地址。
  • Windows:IP Helper 的 GetAdaptersAddresses() 枚举已分配的 IPv4 和 IPv6 地址。

数据包端点方向判定会维护当前分配给主机的地址快照。数据包处理线程每 30 秒刷新一次; 当两个单播端点都无法识别为本地地址时,还会以限速方式立即刷新,并重新解析该数据包一次。 因此,DHCP 地址变化、VPN 连接、网络漫游以及 IPv6 隐私地址轮换后,流量方向仍能正确判定。 在 Windows 上,GetAdaptersAddresses() 不依赖抓包驱动, 即可获取临时和稳定的 IPv6 地址。

进程活动计量

ProcessActivityTracker 每 500ms 从现有快照提供器接收活跃连接和已保留的历史连接。它按进程身份计算当前速率、60 秒滚动窗口、峰值、保留总计、带宽占比、连接数,以及有界的目的地摘要。

接口统计采集器会为每个接口保留紧凑的 60 秒计数窗口。活动覆盖率会在相同持续时间内比较已捕获的进程字节数与接口字节数,而不是将两个独立采样的瞬时速率相除。

清理线程已将关闭的连接行移入 ConnectionTracker 的历史连接池,该池最多保留 5,000 条连接。活动功能直接复用此连接池作为事实来源,不会保留第二份完整连接副本,也不会运行单独的采样线程。活动功能只额外保留紧凑的速率历史、峰值和最新进程快照。每次采样时,超出限制的历史数据会折叠到有界的进程和目的地汇总桶中。

活动标签页与连接界面使用相同的活跃连接和历史连接来源。因此短寿命辅助进程退出后仍然可见,直到对应历史连接被淘汰或用户清除连接。

性能考量

多线程处理

数据包处理分布在多个线程上(默认最多 4 个,基于 CPU 核心数)。这实现了:

  • 并行数据包解析和 DPI 分析
  • 更好地利用多核系统
  • 降低高包速率下的延迟

并发数据结构

DashMap 通过分片锁提供并发连接映射访问,避免单个全局映射锁; 访问同一分片的操作仍可能发生锁竞争。

批处理

抓包线程将有界的数据包批次送入处理队列。单个处理线程会立即处理每个数据包; 多个处理线程可以在有序更新连接跟踪器之前合并队列中已有的批次。

选择性 DPI

可以使用 --no-dpi 禁用深度包检测以降低开销:

  • 在繁忙网络上可能降低 CPU 使用率;实际效果需按工作负载测量
  • 仍然追踪基本连接信息
  • 适用于性能受限的环境

可配置间隔

根据需求调整刷新率:

  • UI 刷新:默认 500ms(可通过 --refresh-interval 调整)
  • 进程补全:每 2 秒
  • 清理检查:每 5 秒
  • 速率计算:每 500ms

内存管理

连接清理防止内存无界增长:

  • 协议感知的超时移除陈旧连接
  • 移除前的视觉陈旧度警告

快照隔离防止 UI 阻塞:

  • UI 从不可变快照读取
  • 后台线程并发更新 DashMap
  • UI 渲染不会持有数据包处理所用的连接映射锁

依赖项

RustNet 使用以下关键依赖构建:

核心依赖

  • ratatui —— 终端用户界面框架,带完整控件支持
  • crossterm —— 跨平台终端操作
  • pcap —— libpcap/Npcap 的包捕获库绑定
  • pnet_datalink —— 网络接口枚举和低层网络

并发与线程

  • dashmap —— 细粒度锁定的并发哈希表
  • crossbeam —— 多线程工具和无锁 channel

网络与协议

  • dns-lookup —— DNS 解析能力
  • maxminddb —— GeoIP 数据库查询(GeoLite2)

序列化

  • serde / serde_json —— 事件日志和 PCAP sidecar 的 JSON 序列化

命令行与日志

  • clap —— 带 derive 特性的命令行参数解析
  • simplelog —— 灵活的日志框架
  • log —— 日志门面
  • anyhow —— 错误处理和上下文

平台特定

  • procfs(Linux)— 从 /proc 文件系统获取进程信息(运行时回退)
  • libbpf-rs(Linux)— eBPF 程序加载和管理
  • landlock(Linux)— 文件系统和网络沙箱
  • caps(Linux)— Linux capabilities 管理
  • windows(Windows)— ETW 和 IP Helper API 的 Windows API 绑定

工具库

  • arboard —— 复制地址的剪贴板访问
  • chrono —— 日期和时间处理
  • ring —— 加密操作(用于 TLS/SNI 解析)
  • aes —— AES 加密支持(用于协议检测)
  • flate2 —— Gzip 解压(用于压缩的嵌入式数据)
  • libc —— 低层 C 绑定

嵌入式数据文件

RustNet 在编译时嵌入静态查找数据库,避免运行时文件依赖。两者遵循相同的模式:嵌入文件,启动时解析为 HashMap,暴露 lookup() 方法。

服务查找(crates/rustnet-core/assets/services)

端口到服务名的映射(例如 80/tcp -> http)。由 crates/rustnet-core/src/network/services.rs 中的 ServiceLookup 使用 include_str! 加载。

OUI 厂商数据库(crates/rustnet-core/assets/oui.gz)

IEEE MA-L OUI 前缀到厂商的映射,用于 MAC 地址厂商解析(例如 00:1B:63 -> Apple)。Gzip 压缩以减小二进制体积(压缩后约 400KB,原始约 1.2MB)。由 crates/rustnet-core/src/network/oui.rs 中的 OuiLookup 使用 include_bytes! + flate2 在启动时解压。用于 ARP 连接以及详情页的本地/远程 MAC 行,后者由一个邻居缓存(crates/rustnet-core/src/network/neighbors.rs)支撑,该缓存从观察到的 ARP(IPv4)和 NDP(IPv6)流量中被动学习 IP 到 MAC 的映射。ARP 不会跨越路由器,而 NDP 报文仅在跳数限制为 255 时才被信任——这证明其未经过路由(RFC 4861),且分片的 NDP 报文会被完全忽略(RFC 6980),因此缓存只会标注链路内地址(局域网设备和网关),绝不会标注公网远端。

GitHub Action(.github/workflows/update-oui.yml)每月从 IEEE 公开数据库 更新此文件,如有变更则自动打开 PR。

安全

关于 Landlock 沙箱、权限需求和威胁模型的安全文档,参见 SECURITY.zh-CN.md。

数据包捕获与进程归属

RustNet 将数据包捕获与操作系统的进程查询结合,在终端显示连接元数据。 进程归属采用尽力而为的方式:权限、抓包接口或平台后端的限制, 可能使归属信息缺失或延迟。连接视图不会解析所有协议,也不会保留完整数据包负载。

使用 --pcap-export 将捕获的数据包保存为标准 PCAP 文件。 如果路径是 capture.pcap,RustNet 还会写入 capture.pcap.connections.jsonl,在连接关闭时记录其元数据。 Sidecar 是独立文件,不嵌入 PCAP。

使用 --pcapng-export 写出包含抓包时已知进程信息的数据包注释。 这些注释采用尽力而为的方式;若当时尚未识别进程,注释可能缺失。 两种格式都可在 Wireshark 中打开以分析数据包。

sudo rustnet -i eth0 --pcap-export capture.pcap
sudo rustnet -i eth0 --pcapng-export annotated.pcapng

# 可选:抓包结束后将 PCAP 与其 sidecar 合并。
python scripts/pcap_enrich.py capture.pcap -o enriched.pcapng

输出细节和限制见PCAP 导出。