From 19dc5d82a073a8c575d8fd8298abb8607bd2bc51 Mon Sep 17 00:00:00 2001 From: wehub-resource-sync Date: Mon, 13 Jul 2026 12:29:44 +0800 Subject: [PATCH] chore: import upstream snapshot with attribution --- .dockerignore | 34 + .gitattributes | 2 + .github/PPA_SETUP.md | 50 + .github/actions/build-rustnet/action.yml | 38 + .github/actions/build-static/action.yml | 30 + .github/actions/setup-linux-deps/action.yml | 11 + .github/dependabot.yml | 46 + .github/pull_request_template.md | 56 + .github/workflows/aur-update.yml | 221 + .github/workflows/build-android-static.yml | 222 + .github/workflows/build-musl-arm64.yml | 73 + .github/workflows/build-platforms.yml | 353 + .github/workflows/copr-update-spec.yml | 72 + .github/workflows/docker.yml | 72 + .github/workflows/obs-release.yml | 146 + .github/workflows/ppa-release.yml | 254 + .github/workflows/publish.yml | 64 + .github/workflows/release.yml | 817 + .github/workflows/rust.yml | 78 + .github/workflows/security.yml | 25 + .github/workflows/test-platform-builds.yml | 46 + .github/workflows/update-oui.yml | 50 + .gitignore | 24 + ARCHITECTURE.md | 490 + ARCHITECTURE.zh-CN.md | 490 + CHANGELOG.md | 601 + CONTRIBUTING.md | 120 + CONTRIBUTING.zh-CN.md | 119 + CONTRIBUTORS.md | 21 + Cargo.lock | 4142 ++ Cargo.toml | 245 + Cross.toml | 21 + Dockerfile | 111 + Dockerfile.static | 61 + INSTALL.md | 1160 + INSTALL.zh-CN.md | 1123 + LICENSE | 201 + PROFILING.md | 196 + PROFILING.zh-CN.md | 196 + README.md | 340 + README.wehub.md | 7 + README.zh-CN.md | 337 + RELEASE.md | 218 + ROADMAP.md | 270 + SECURITY.md | 333 + SECURITY.zh-CN.md | 328 + USAGE.md | 1189 + USAGE.zh-CN.md | 1147 + assets/rustnet.gif | Bin 0 -> 2735489 bytes assets/rustnet.svg | 24 + assets/screenshots/details.png | Bin 0 -> 259933 bytes assets/screenshots/filter.png | Bin 0 -> 1268023 bytes assets/screenshots/graph.png | Bin 0 -> 319409 bytes assets/screenshots/grouping.png | Bin 0 -> 442079 bytes assets/screenshots/interfaces.png | Bin 0 -> 155691 bytes assets/screenshots/overview.png | Bin 0 -> 1249463 bytes benches/connection_merge.rs | 113 + benches/packet_parsing.rs | 93 + benches/rate_tracker.rs | 221 + benches/snapshot.rs | 93 + benches/tracker_ingest.rs | 101 + build.rs | 180 + crates/rustnet-capture/Cargo.toml | 27 + crates/rustnet-capture/README.md | 32 + crates/rustnet-capture/src/lib.rs | 607 + crates/rustnet-core/Cargo.toml | 36 + crates/rustnet-core/README.md | 38 + crates/rustnet-core/assets/oui.gz | Bin 0 -> 417003 bytes crates/rustnet-core/assets/services | 11607 ++++ crates/rustnet-core/src/lib.rs | 39 + crates/rustnet-core/src/network/bogon.rs | 230 + crates/rustnet-core/src/network/dns.rs | 354 + .../src/network/dpi/bittorrent.rs | 673 + .../src/network/dpi/cipher_suites.rs | 227 + crates/rustnet-core/src/network/dpi/dhcp.rs | 219 + crates/rustnet-core/src/network/dpi/dns.rs | 661 + crates/rustnet-core/src/network/dpi/ftp.rs | 367 + crates/rustnet-core/src/network/dpi/http.rs | 218 + crates/rustnet-core/src/network/dpi/https.rs | 483 + crates/rustnet-core/src/network/dpi/llmnr.rs | 146 + crates/rustnet-core/src/network/dpi/mdns.rs | 195 + crates/rustnet-core/src/network/dpi/mod.rs | 266 + crates/rustnet-core/src/network/dpi/mqtt.rs | 544 + .../rustnet-core/src/network/dpi/netbios.rs | 337 + crates/rustnet-core/src/network/dpi/ntp.rs | 108 + crates/rustnet-core/src/network/dpi/quic.rs | 2940 + crates/rustnet-core/src/network/dpi/snmp.rs | 424 + crates/rustnet-core/src/network/dpi/ssdp.rs | 169 + crates/rustnet-core/src/network/dpi/ssh.rs | 540 + crates/rustnet-core/src/network/dpi/stun.rs | 314 + crates/rustnet-core/src/network/geoip.rs | 469 + .../src/network/interface_stats.rs | 162 + .../src/network/link_layer/ethernet.rs | 136 + .../src/network/link_layer/linux_sll.rs | 194 + .../src/network/link_layer/mod.rs | 173 + .../src/network/link_layer/pktap.rs | 288 + .../src/network/link_layer/raw_ip.rs | 117 + .../src/network/link_layer/tun_tap.rs | 221 + crates/rustnet-core/src/network/merge.rs | 1100 + crates/rustnet-core/src/network/mod.rs | 22 + crates/rustnet-core/src/network/oui.rs | 168 + crates/rustnet-core/src/network/parser.rs | 1044 + .../rustnet-core/src/network/protocol/icmp.rs | 103 + .../rustnet-core/src/network/protocol/igmp.rs | 276 + .../rustnet-core/src/network/protocol/mod.rs | 45 + .../rustnet-core/src/network/protocol/tcp.rs | 171 + .../rustnet-core/src/network/protocol/udp.rs | 58 + crates/rustnet-core/src/network/services.rs | 148 + crates/rustnet-core/src/network/tracker.rs | 632 + crates/rustnet-core/src/network/types.rs | 3777 ++ crates/rustnet-host/Cargo.toml | 47 + crates/rustnet-host/README.md | 45 + crates/rustnet-host/build.rs | 83 + .../resources/ebpf/vmlinux/aarch64/vmlinux.h | 48672 ++++++++++++++ .../resources/ebpf/vmlinux/arm/vmlinux.h | 47898 ++++++++++++++ .../resources/ebpf/vmlinux/x86/vmlinux.h | 53143 ++++++++++++++++ crates/rustnet-host/src/freebsd/mod.rs | 15 + crates/rustnet-host/src/freebsd/process.rs | 334 + crates/rustnet-host/src/lib.rs | 280 + crates/rustnet-host/src/linux/ebpf/loader.rs | 472 + .../src/linux/ebpf/maps_libbpf.rs | 434 + crates/rustnet-host/src/linux/ebpf/mod.rs | 19 + .../linux/ebpf/programs/socket_tracker.bpf.c | 364 + .../src/linux/ebpf/tracker_libbpf.rs | 275 + crates/rustnet-host/src/linux/enhanced.rs | 720 + crates/rustnet-host/src/linux/mod.rs | 38 + crates/rustnet-host/src/linux/process.rs | 280 + crates/rustnet-host/src/macos/mod.rs | 66 + crates/rustnet-host/src/macos/process.rs | 397 + crates/rustnet-host/src/windows/mod.rs | 15 + crates/rustnet-host/src/windows/process.rs | 551 + debian/README.md | 67 + debian/cargo.config | 5 + debian/changelog | 22 + debian/control | 42 + debian/copyright | 28 + debian/install | 3 + debian/postinst | 72 + debian/rules | 80 + debian/rustnet.lintian-overrides | 6 + debian/source/format | 1 + debian/source/include-binaries | 1 + demo.tape | 106 + deny.toml | 54 + examples/k8s_resolver_check.rs | 54 + .../packaging/linux/graphics/rustnet.png | Bin 0 -> 201321 bytes resources/packaging/linux/rustnet.desktop | 10 + resources/packaging/macos/Info.plist | 34 + resources/packaging/macos/graphics/dmg_bg.png | Bin 0 -> 29360 bytes .../packaging/macos/graphics/rustnet.icns | Bin 0 -> 743395 bytes .../packaging/macos/graphics/rustnet.png | Bin 0 -> 743395 bytes resources/packaging/macos/wrapper.sh | 18 + .../packaging/windows/graphics/rustnet.ico | Bin 0 -> 164528 bytes resources/packaging/windows/wix/main.wxs | 122 + rpm/rustnet.spec | 176 + screenshots.tape | 101 + scripts/clear_old_logs.sh | 61 + scripts/debug-attribution.sh | 84 + scripts/pcap_enrich.py | 424 + scripts/pre-release-check.sh | 179 + scripts/record-rustnet-demo.sh | 177 + scripts/test-deb-build.sh | 66 + src/app.rs | 3070 + src/cli.rs | 227 + src/config.rs | 190 + src/export/mod.rs | 1 + src/export/pcapng.rs | 258 + src/filter.rs | 879 + src/lib.rs | 104 + src/main.rs | 1303 + src/network/kubernetes.rs | 1000 + src/network/mod.rs | 33 + .../platform/freebsd/interface_stats.rs | 98 + src/network/platform/freebsd/mod.rs | 7 + src/network/platform/freebsd/privdrop.rs | 186 + src/network/platform/linux/interface_stats.rs | 150 + src/network/platform/linux/mod.rs | 10 + .../platform/linux/sandbox/capabilities.rs | 139 + .../platform/linux/sandbox/landlock.rs | 365 + src/network/platform/linux/sandbox/mod.rs | 477 + .../platform/linux/sandbox/privdrop.rs | 192 + src/network/platform/macos/interface_stats.rs | 131 + src/network/platform/macos/mod.rs | 11 + src/network/platform/macos/privdrop.rs | 179 + src/network/platform/macos/sandbox/mod.rs | 140 + .../platform/macos/sandbox/seatbelt.rs | 589 + src/network/platform/mod.rs | 56 + .../platform/windows/interface_stats.rs | 167 + src/network/platform/windows/mod.rs | 7 + src/network/platform/windows/sandbox/mod.rs | 144 + .../platform/windows/sandbox/restricted.rs | 221 + src/network/privileges.rs | 400 + src/ui/actions.rs | 159 + src/ui/clipboard.rs | 62 + src/ui/component.rs | 97 + src/ui/connection_table.rs | 704 + src/ui/effects.rs | 33 + src/ui/format.rs | 59 + src/ui/mod.rs | 1146 + ...snapshot_tests__details_tab_tcp_https.snap | 44 + ...tests__filter_input_mode_active_empty.snap | 6 + ...s__filter_input_mode_active_with_text.snap | 6 + ...napshot_tests__filter_input_persisted.snap | 6 + ...apshot_tests__graph_tab_empty_history.snap | 45 + ...monitor__ui__snapshot_tests__help_tab.snap | 54 + ...r__ui__snapshot_tests__interfaces_tab.snap | 35 + ...r__ui__snapshot_tests__loading_screen.snap | 22 + ...napshot_tests__loading_screen_via_app.snap | 22 + ..._tests__status_bar_clear_confirmation.snap | 6 + ...napshot_tests__status_bar_details_tab.snap | 6 + ...__snapshot_tests__status_bar_filtered.snap | 6 + ...__snapshot_tests__status_bar_help_tab.snap | 6 + ...ot_tests__status_bar_overview_default.snap | 6 + ...t_tests__status_bar_quit_confirmation.snap | 6 + ...apshot_tests__tabs_bar_details_active.snap | 7 + ..._snapshot_tests__tabs_bar_help_active.snap | 7 + ...pshot_tests__tabs_bar_overview_active.snap | 7 + src/ui/sorting.rs | 80 + src/ui/state.rs | 1050 + src/ui/tabs/details.rs | 1815 + src/ui/tabs/graph.rs | 712 + src/ui/tabs/help.rs | 321 + src/ui/tabs/interfaces.rs | 203 + src/ui/tabs/mod.rs | 9 + src/ui/tabs/overview.rs | 1498 + src/ui/terminal.rs | 77 + src/ui/theme.rs | 366 + src/ui/widgets/braille_graph.rs | 404 + src/ui/widgets/filter_input.rs | 46 + src/ui/widgets/loading.rs | 94 + src/ui/widgets/mod.rs | 10 + src/ui/widgets/scrollbar.rs | 94 + src/ui/widgets/status_bar.rs | 76 + src/ui/widgets/tabs_bar.rs | 98 + tests/integration_tests.rs | 42 + tests/kubernetes_resolver_live.rs | 55 + 236 files changed, 224662 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitattributes create mode 100644 .github/PPA_SETUP.md create mode 100644 .github/actions/build-rustnet/action.yml create mode 100644 .github/actions/build-static/action.yml create mode 100644 .github/actions/setup-linux-deps/action.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/aur-update.yml create mode 100644 .github/workflows/build-android-static.yml create mode 100644 .github/workflows/build-musl-arm64.yml create mode 100644 .github/workflows/build-platforms.yml create mode 100644 .github/workflows/copr-update-spec.yml create mode 100644 .github/workflows/docker.yml create mode 100644 .github/workflows/obs-release.yml create mode 100644 .github/workflows/ppa-release.yml create mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/rust.yml create mode 100644 .github/workflows/security.yml create mode 100644 .github/workflows/test-platform-builds.yml create mode 100644 .github/workflows/update-oui.yml create mode 100644 .gitignore create mode 100644 ARCHITECTURE.md create mode 100644 ARCHITECTURE.zh-CN.md create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 CONTRIBUTING.zh-CN.md create mode 100644 CONTRIBUTORS.md create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Cross.toml create mode 100644 Dockerfile create mode 100644 Dockerfile.static create mode 100644 INSTALL.md create mode 100644 INSTALL.zh-CN.md create mode 100644 LICENSE create mode 100644 PROFILING.md create mode 100644 PROFILING.zh-CN.md create mode 100644 README.md create mode 100644 README.wehub.md create mode 100644 README.zh-CN.md create mode 100644 RELEASE.md create mode 100644 ROADMAP.md create mode 100644 SECURITY.md create mode 100644 SECURITY.zh-CN.md create mode 100644 USAGE.md create mode 100644 USAGE.zh-CN.md create mode 100644 assets/rustnet.gif create mode 100644 assets/rustnet.svg create mode 100644 assets/screenshots/details.png create mode 100644 assets/screenshots/filter.png create mode 100644 assets/screenshots/graph.png create mode 100644 assets/screenshots/grouping.png create mode 100644 assets/screenshots/interfaces.png create mode 100644 assets/screenshots/overview.png create mode 100644 benches/connection_merge.rs create mode 100644 benches/packet_parsing.rs create mode 100644 benches/rate_tracker.rs create mode 100644 benches/snapshot.rs create mode 100644 benches/tracker_ingest.rs create mode 100644 build.rs create mode 100644 crates/rustnet-capture/Cargo.toml create mode 100644 crates/rustnet-capture/README.md create mode 100644 crates/rustnet-capture/src/lib.rs create mode 100644 crates/rustnet-core/Cargo.toml create mode 100644 crates/rustnet-core/README.md create mode 100644 crates/rustnet-core/assets/oui.gz create mode 100644 crates/rustnet-core/assets/services create mode 100644 crates/rustnet-core/src/lib.rs create mode 100644 crates/rustnet-core/src/network/bogon.rs create mode 100644 crates/rustnet-core/src/network/dns.rs create mode 100644 crates/rustnet-core/src/network/dpi/bittorrent.rs create mode 100644 crates/rustnet-core/src/network/dpi/cipher_suites.rs create mode 100644 crates/rustnet-core/src/network/dpi/dhcp.rs create mode 100644 crates/rustnet-core/src/network/dpi/dns.rs create mode 100644 crates/rustnet-core/src/network/dpi/ftp.rs create mode 100644 crates/rustnet-core/src/network/dpi/http.rs create mode 100644 crates/rustnet-core/src/network/dpi/https.rs create mode 100644 crates/rustnet-core/src/network/dpi/llmnr.rs create mode 100644 crates/rustnet-core/src/network/dpi/mdns.rs create mode 100644 crates/rustnet-core/src/network/dpi/mod.rs create mode 100644 crates/rustnet-core/src/network/dpi/mqtt.rs create mode 100644 crates/rustnet-core/src/network/dpi/netbios.rs create mode 100644 crates/rustnet-core/src/network/dpi/ntp.rs create mode 100644 crates/rustnet-core/src/network/dpi/quic.rs create mode 100644 crates/rustnet-core/src/network/dpi/snmp.rs create mode 100644 crates/rustnet-core/src/network/dpi/ssdp.rs create mode 100644 crates/rustnet-core/src/network/dpi/ssh.rs create mode 100644 crates/rustnet-core/src/network/dpi/stun.rs create mode 100644 crates/rustnet-core/src/network/geoip.rs create mode 100644 crates/rustnet-core/src/network/interface_stats.rs create mode 100644 crates/rustnet-core/src/network/link_layer/ethernet.rs create mode 100644 crates/rustnet-core/src/network/link_layer/linux_sll.rs create mode 100644 crates/rustnet-core/src/network/link_layer/mod.rs create mode 100644 crates/rustnet-core/src/network/link_layer/pktap.rs create mode 100644 crates/rustnet-core/src/network/link_layer/raw_ip.rs create mode 100644 crates/rustnet-core/src/network/link_layer/tun_tap.rs create mode 100644 crates/rustnet-core/src/network/merge.rs create mode 100644 crates/rustnet-core/src/network/mod.rs create mode 100644 crates/rustnet-core/src/network/oui.rs create mode 100644 crates/rustnet-core/src/network/parser.rs create mode 100644 crates/rustnet-core/src/network/protocol/icmp.rs create mode 100644 crates/rustnet-core/src/network/protocol/igmp.rs create mode 100644 crates/rustnet-core/src/network/protocol/mod.rs create mode 100644 crates/rustnet-core/src/network/protocol/tcp.rs create mode 100644 crates/rustnet-core/src/network/protocol/udp.rs create mode 100644 crates/rustnet-core/src/network/services.rs create mode 100644 crates/rustnet-core/src/network/tracker.rs create mode 100644 crates/rustnet-core/src/network/types.rs create mode 100644 crates/rustnet-host/Cargo.toml create mode 100644 crates/rustnet-host/README.md create mode 100644 crates/rustnet-host/build.rs create mode 100644 crates/rustnet-host/resources/ebpf/vmlinux/aarch64/vmlinux.h create mode 100644 crates/rustnet-host/resources/ebpf/vmlinux/arm/vmlinux.h create mode 100644 crates/rustnet-host/resources/ebpf/vmlinux/x86/vmlinux.h create mode 100644 crates/rustnet-host/src/freebsd/mod.rs create mode 100644 crates/rustnet-host/src/freebsd/process.rs create mode 100644 crates/rustnet-host/src/lib.rs create mode 100644 crates/rustnet-host/src/linux/ebpf/loader.rs create mode 100644 crates/rustnet-host/src/linux/ebpf/maps_libbpf.rs create mode 100644 crates/rustnet-host/src/linux/ebpf/mod.rs create mode 100644 crates/rustnet-host/src/linux/ebpf/programs/socket_tracker.bpf.c create mode 100644 crates/rustnet-host/src/linux/ebpf/tracker_libbpf.rs create mode 100644 crates/rustnet-host/src/linux/enhanced.rs create mode 100644 crates/rustnet-host/src/linux/mod.rs create mode 100644 crates/rustnet-host/src/linux/process.rs create mode 100644 crates/rustnet-host/src/macos/mod.rs create mode 100644 crates/rustnet-host/src/macos/process.rs create mode 100644 crates/rustnet-host/src/windows/mod.rs create mode 100644 crates/rustnet-host/src/windows/process.rs create mode 100644 debian/README.md create mode 100644 debian/cargo.config create mode 100644 debian/changelog create mode 100644 debian/control create mode 100644 debian/copyright create mode 100644 debian/install create mode 100755 debian/postinst create mode 100755 debian/rules create mode 100644 debian/rustnet.lintian-overrides create mode 100644 debian/source/format create mode 100644 debian/source/include-binaries create mode 100644 demo.tape create mode 100644 deny.toml create mode 100644 examples/k8s_resolver_check.rs create mode 100644 resources/packaging/linux/graphics/rustnet.png create mode 100644 resources/packaging/linux/rustnet.desktop create mode 100644 resources/packaging/macos/Info.plist create mode 100644 resources/packaging/macos/graphics/dmg_bg.png create mode 100644 resources/packaging/macos/graphics/rustnet.icns create mode 100644 resources/packaging/macos/graphics/rustnet.png create mode 100644 resources/packaging/macos/wrapper.sh create mode 100644 resources/packaging/windows/graphics/rustnet.ico create mode 100644 resources/packaging/windows/wix/main.wxs create mode 100644 rpm/rustnet.spec create mode 100644 screenshots.tape create mode 100755 scripts/clear_old_logs.sh create mode 100755 scripts/debug-attribution.sh create mode 100755 scripts/pcap_enrich.py create mode 100755 scripts/pre-release-check.sh create mode 100755 scripts/record-rustnet-demo.sh create mode 100755 scripts/test-deb-build.sh create mode 100644 src/app.rs create mode 100644 src/cli.rs create mode 100644 src/config.rs create mode 100644 src/export/mod.rs create mode 100644 src/export/pcapng.rs create mode 100644 src/filter.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/network/kubernetes.rs create mode 100644 src/network/mod.rs create mode 100644 src/network/platform/freebsd/interface_stats.rs create mode 100644 src/network/platform/freebsd/mod.rs create mode 100644 src/network/platform/freebsd/privdrop.rs create mode 100644 src/network/platform/linux/interface_stats.rs create mode 100644 src/network/platform/linux/mod.rs create mode 100644 src/network/platform/linux/sandbox/capabilities.rs create mode 100644 src/network/platform/linux/sandbox/landlock.rs create mode 100644 src/network/platform/linux/sandbox/mod.rs create mode 100644 src/network/platform/linux/sandbox/privdrop.rs create mode 100644 src/network/platform/macos/interface_stats.rs create mode 100644 src/network/platform/macos/mod.rs create mode 100644 src/network/platform/macos/privdrop.rs create mode 100644 src/network/platform/macos/sandbox/mod.rs create mode 100644 src/network/platform/macos/sandbox/seatbelt.rs create mode 100644 src/network/platform/mod.rs create mode 100644 src/network/platform/windows/interface_stats.rs create mode 100644 src/network/platform/windows/mod.rs create mode 100644 src/network/platform/windows/sandbox/mod.rs create mode 100644 src/network/platform/windows/sandbox/restricted.rs create mode 100644 src/network/privileges.rs create mode 100644 src/ui/actions.rs create mode 100644 src/ui/clipboard.rs create mode 100644 src/ui/component.rs create mode 100644 src/ui/connection_table.rs create mode 100644 src/ui/effects.rs create mode 100644 src/ui/format.rs create mode 100644 src/ui/mod.rs create mode 100644 src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__details_tab_tcp_https.snap create mode 100644 src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__filter_input_mode_active_empty.snap create mode 100644 src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__filter_input_mode_active_with_text.snap create mode 100644 src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__filter_input_persisted.snap create mode 100644 src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__graph_tab_empty_history.snap create mode 100644 src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__help_tab.snap create mode 100644 src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__interfaces_tab.snap create mode 100644 src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__loading_screen.snap create mode 100644 src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__loading_screen_via_app.snap create mode 100644 src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__status_bar_clear_confirmation.snap create mode 100644 src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__status_bar_details_tab.snap create mode 100644 src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__status_bar_filtered.snap create mode 100644 src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__status_bar_help_tab.snap create mode 100644 src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__status_bar_overview_default.snap create mode 100644 src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__status_bar_quit_confirmation.snap create mode 100644 src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__tabs_bar_details_active.snap create mode 100644 src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__tabs_bar_help_active.snap create mode 100644 src/ui/snapshots/rustnet_monitor__ui__snapshot_tests__tabs_bar_overview_active.snap create mode 100644 src/ui/sorting.rs create mode 100644 src/ui/state.rs create mode 100644 src/ui/tabs/details.rs create mode 100644 src/ui/tabs/graph.rs create mode 100644 src/ui/tabs/help.rs create mode 100644 src/ui/tabs/interfaces.rs create mode 100644 src/ui/tabs/mod.rs create mode 100644 src/ui/tabs/overview.rs create mode 100644 src/ui/terminal.rs create mode 100644 src/ui/theme.rs create mode 100644 src/ui/widgets/braille_graph.rs create mode 100644 src/ui/widgets/filter_input.rs create mode 100644 src/ui/widgets/loading.rs create mode 100644 src/ui/widgets/mod.rs create mode 100644 src/ui/widgets/scrollbar.rs create mode 100644 src/ui/widgets/status_bar.rs create mode 100644 src/ui/widgets/tabs_bar.rs create mode 100644 tests/integration_tests.rs create mode 100644 tests/kubernetes_resolver_live.rs diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..bc52f17 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,34 @@ +# Build artifacts +/target/ + +# Git +.git/ +.gitignore +.dockerignore + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# Logs +logs/ +*.log + +# Documentation and non-essential files +README.md +CHANGELOG.md +ROADMAP.md +LICENSE +RELEASE.md +rustnet-*.tar.gz + +# Assets we don't need in container +assets/rustnet.gif + +# Scripts (not needed in container) +scripts/ + +# CI/CD (already in repo context) +.github/ diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..79f08d9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Ignore the vmlinux.h header for GitHub language stats +vmlinux.h linguist-generated diff --git a/.github/PPA_SETUP.md b/.github/PPA_SETUP.md new file mode 100644 index 0000000..c230203 --- /dev/null +++ b/.github/PPA_SETUP.md @@ -0,0 +1,50 @@ +# GitHub Actions PPA Setup + +## Add GitHub Secrets + +Go to: **Settings** → **Secrets and variables** → **Actions** → **New repository secret** + +### 1. GPG_PRIVATE_KEY + +```bash +# Display your CI private key +cat ci-signing-key.asc +``` + +Copy the entire output (including `-----BEGIN PGP PRIVATE KEY BLOCK-----` and `-----END...`) + +- Name: `GPG_PRIVATE_KEY` +- Value: [paste the entire key] + +### 2. GPG_KEY_ID + +```bash +# Get your key ID +gpg --list-keys cadetg@gmail.com +``` + +Copy the long hex string (e.g., `ABC123...`) + +- Name: `GPG_KEY_ID` +- Value: [paste just the key ID] + +## Test the Workflow + +```bash +# Create and push a test tag +git tag v1.0.0-test +git push origin v1.0.0-test +``` + +Check: **Actions** tab in GitHub → **Release to Ubuntu PPA** + +## Remove Test Tag (if needed) + +```bash +git tag -d v1.0.0-test +git push origin :refs/tags/v1.0.0-test +``` + +## Done! + +From now on, just push version tags and GitHub will handle the PPA release automatically! 🚀 diff --git a/.github/actions/build-rustnet/action.yml b/.github/actions/build-rustnet/action.yml new file mode 100644 index 0000000..2b2ddad --- /dev/null +++ b/.github/actions/build-rustnet/action.yml @@ -0,0 +1,38 @@ +name: 'Build RustNet' +description: 'Build RustNet binary for a specific target' + +inputs: + target: + description: 'Rust target triple (e.g., x86_64-unknown-linux-gnu)' + required: true + cargo-command: + description: 'Cargo command to use (cargo or cross)' + required: false + default: 'cargo' + default-features: + description: 'Enable default features including eBPF (true/false)' + required: false + default: 'true' + strip-symbols: + description: 'Strip debug symbols from binary (true/false)' + required: false + default: 'true' + +runs: + using: 'composite' + steps: + - name: Build binary + shell: bash + env: + RUSTFLAGS: ${{ inputs.strip-symbols == 'true' && '-C strip=symbols' || '' }} + run: | + mkdir -p assets # Ensure asset dir exists for build.rs + + BUILD_CMD="${{ inputs.cargo-command }} build --verbose --release --target ${{ inputs.target }}" + + if [[ "${{ inputs.default-features }}" != "true" ]]; then + BUILD_CMD="$BUILD_CMD --no-default-features" + fi + + echo "Running: $BUILD_CMD" + $BUILD_CMD diff --git a/.github/actions/build-static/action.yml b/.github/actions/build-static/action.yml new file mode 100644 index 0000000..ad9b54c --- /dev/null +++ b/.github/actions/build-static/action.yml @@ -0,0 +1,30 @@ +name: 'Build Static Binary' +description: 'Build a statically-linked Linux binary using musl' + +runs: + using: 'composite' + steps: + - name: Install dependencies + shell: sh + run: | + apk add --no-cache \ + musl-dev libpcap-dev pkgconfig build-base perl \ + elfutils-dev zlib-dev zlib-static zstd-dev zstd-static \ + clang llvm linux-headers git + rustup component add rustfmt + + - name: Build static binary + shell: sh + env: + # -C strip=symbols: Strip debug symbols for smaller binary + # -C link-arg=-l:libzstd.a: Fix elfutils 0.189+ zstd dependency (libbpf/bpftool#152) + RUSTFLAGS: "-C strip=symbols -C link-arg=-l:libzstd.a" + run: cargo build --release + + - name: Verify static linking + shell: sh + run: | + file target/release/rustnet + # Use file command to verify (ldd behaves differently inside Alpine) + file target/release/rustnet | grep -q "static.* linked" || \ + (echo "ERROR: Binary is not statically linked" && exit 1) diff --git a/.github/actions/setup-linux-deps/action.yml b/.github/actions/setup-linux-deps/action.yml new file mode 100644 index 0000000..b9543c8 --- /dev/null +++ b/.github/actions/setup-linux-deps/action.yml @@ -0,0 +1,11 @@ +name: 'Setup Linux Build Dependencies' +description: 'Install dependencies required for building RustNet on Linux' + +runs: + using: 'composite' + steps: + - name: Install dependencies + shell: bash + run: | + sudo apt-get update -y + sudo apt-get install -y libpcap-dev libelf-dev zlib1g-dev clang llvm pkg-config diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..6ad2fb8 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,46 @@ +version: 2 +updates: + # Maintain Cargo dependencies + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "daily" + cooldown: + default-days: 3 + semver-major-days: 7 + semver-minor-days: 4 + semver-patch-days: 3 + groups: + rust-dependencies: + patterns: + - "*" + update-types: + - "major" + - "minor" + - "patch" + + # Maintain GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + groups: + actions: + patterns: + - "*" + update-types: + - "major" + - "minor" + - "patch" + + # Maintain Docker base images (Dockerfile FROM pins) + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "daily" + cooldown: + default-days: 7 + groups: + docker: + patterns: + - "*" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..2b1409b --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,56 @@ + + +## Summary + + + +## Linked issue + + + +Closes # + +## Verification + +Per [CONTRIBUTING.md > Code Quality Requirements](https://github.com/domcyrus/rustnet/blob/main/CONTRIBUTING.md#code-quality-requirements), +I ran the following locally and they all pass: + +- [ ] `cargo fmt --all -- --check` +- [ ] `cargo clippy --all-targets --all-features -- -D warnings` +- [ ] `cargo test --all-features` +- [ ] `cargo build --release` + + + +## Scope + +- [ ] One feature or fix per PR (no unrelated cleanups bundled in) +- [ ] No new dependencies, or rationale provided in the summary above +- [ ] No `#[allow(clippy::...)]` suppressions, or rationale provided + (see [CONTRIBUTING.md](https://github.com/domcyrus/rustnet/blob/main/CONTRIBUTING.md#code-quality-requirements)) + +## AI-assisted contributions + +If you used an AI assistant (Copilot, Claude, ChatGPT, Cursor, etc.) to +write any of this code, that is fine, but please confirm: + +- [ ] I have read every line I am submitting +- [ ] I ran the verification commands above myself +- [ ] The PR description reflects what the code actually does + +## Notes for the reviewer + + diff --git a/.github/workflows/aur-update.yml b/.github/workflows/aur-update.yml new file mode 100644 index 0000000..6132752 --- /dev/null +++ b/.github/workflows/aur-update.yml @@ -0,0 +1,221 @@ +name: Update AUR Package + +on: + release: + types: [published] + workflow_dispatch: + +permissions: + contents: read + +jobs: + update-aur: + name: update-aur + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Extract version from tag + id: version + run: | + # Determine the tag based on trigger type + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + # Get the latest tag when manually triggered + git fetch --tags + TAG=$(git tag --sort=-version:refname | head -n1) + else + # For release event, use the release tag + TAG="${{ github.event.release.tag_name }}" + fi + + # Extract version by removing 'v' prefix + VERSION="${TAG#v}" + + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "tag=$TAG" >> $GITHUB_OUTPUT + echo "Extracted version: $VERSION" + echo "Tag: $TAG" + + - name: Download release assets and calculate checksums + id: checksums + run: | + VERSION="${{ steps.version.outputs.version }}" + TAG="${{ steps.version.outputs.tag }}" + + # Define asset names + ASSET_X64="rustnet-${TAG}-x86_64-unknown-linux-gnu.tar.gz" + ASSET_ARM64="rustnet-${TAG}-aarch64-unknown-linux-gnu.tar.gz" + + echo "Downloading release assets..." + + # Download assets + gh release download "$TAG" -p "$ASSET_X64" -p "$ASSET_ARM64" + + # Calculate SHA256 checksums + SHA256_X64=$(sha256sum "$ASSET_X64" | awk '{print $1}') + SHA256_ARM64=$(sha256sum "$ASSET_ARM64" | awk '{print $1}') + + echo "x64_checksum=$SHA256_X64" >> $GITHUB_OUTPUT + echo "arm64_checksum=$SHA256_ARM64" >> $GITHUB_OUTPUT + + echo "Checksums calculated:" + echo " x86_64: $SHA256_X64" + echo " aarch64: $SHA256_ARM64" + env: + GH_TOKEN: ${{ github.token }} + + - name: Verify checksums match release assets + run: | + TAG="${{ steps.version.outputs.tag }}" + EXPECTED_X64="${{ steps.checksums.outputs.x64_checksum }}" + EXPECTED_ARM64="${{ steps.checksums.outputs.arm64_checksum }}" + FAILED=0 + + echo "Re-downloading assets to verify checksums are stable..." + + ASSET_X64="rustnet-${TAG}-x86_64-unknown-linux-gnu.tar.gz" + ASSET_ARM64="rustnet-${TAG}-aarch64-unknown-linux-gnu.tar.gz" + + gh release download "$TAG" -p "$ASSET_X64" -p "$ASSET_ARM64" -D /tmp/verify + + ACTUAL_X64=$(sha256sum "/tmp/verify/$ASSET_X64" | awk '{print $1}') + ACTUAL_ARM64=$(sha256sum "/tmp/verify/$ASSET_ARM64" | awk '{print $1}') + rm -rf /tmp/verify + + if [ "$ACTUAL_X64" != "$EXPECTED_X64" ]; then + echo "::error::x86_64 checksum mismatch: expected $EXPECTED_X64, got $ACTUAL_X64" + FAILED=1 + else + echo " ✓ x86_64 checksum verified" + fi + + if [ "$ACTUAL_ARM64" != "$EXPECTED_ARM64" ]; then + echo "::error::aarch64 checksum mismatch: expected $EXPECTED_ARM64, got $ACTUAL_ARM64" + FAILED=1 + else + echo " ✓ aarch64 checksum verified" + fi + + if [ "$FAILED" -ne 0 ]; then + echo "::error::Checksum verification failed — aborting to prevent pushing stale checksums to AUR" + exit 1 + fi + echo "All checksums verified successfully" + env: + GH_TOKEN: ${{ github.token }} + + - name: Setup SSH for AUR + env: + AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }} + run: | + mkdir -p ~/.ssh + # Pass the key via env, not inline interpolation, so its bytes are never + # spliced into the rendered command line (matches the GPG handling in + # ppa-release.yml). printf avoids echo's escape interpretation. + printf '%s\n' "$AUR_SSH_PRIVATE_KEY" > ~/.ssh/aur + chmod 600 ~/.ssh/aur + ssh-keyscan aur.archlinux.org >> ~/.ssh/known_hosts + + # Configure SSH to use the AUR key + cat > ~/.ssh/config < .SRCINFO + ' || { + # If that fails (no write permissions), install tools as root then run as user + docker run --rm -v "$PWD/aur-rustnet-bin:/pkg" archlinux:latest bash -c " + pacman -Sy --noconfirm binutils fakeroot sudo && + useradd -m -u $(id -u) builder && + cd /pkg && + su builder -c 'makepkg --printsrcinfo > .SRCINFO' + " + } + + echo ".SRCINFO generated successfully" + + - name: Commit and push to AUR + run: | + cd aur-rustnet-bin + VERSION="${{ steps.version.outputs.version }}" + + # Check if there are changes to commit + if git diff --quiet PKGBUILD .SRCINFO; then + echo "No changes to PKGBUILD or .SRCINFO (already at version $VERSION)" + exit 0 + fi + + # Show changes + echo "Changes to be committed:" + git diff PKGBUILD .SRCINFO + + # Commit and push + git add PKGBUILD .SRCINFO + git commit -m "Update to version $VERSION" + git push origin master + + echo "Pushed changes to AUR repository" + + - name: Summary + run: | + VERSION="${{ steps.version.outputs.version }}" + echo "## 🎉 AUR Package Updated" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **Package**: rustnet-bin" >> $GITHUB_STEP_SUMMARY + echo "- **Version**: $VERSION" >> $GITHUB_STEP_SUMMARY + echo "- **Architectures**: x86_64, aarch64" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Checksums" >> $GITHUB_STEP_SUMMARY + echo "- **x86_64**: \`${{ steps.checksums.outputs.x64_checksum }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **aarch64**: \`${{ steps.checksums.outputs.arm64_checksum }}\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "[View AUR Package →](https://aur.archlinux.org/packages/rustnet-bin)" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/build-android-static.yml b/.github/workflows/build-android-static.yml new file mode 100644 index 0000000..b8ad815 --- /dev/null +++ b/.github/workflows/build-android-static.yml @@ -0,0 +1,222 @@ +name: Build Android Musl Static + +# One-off workflow to build and upload android musl static binaries +# for multiple architectures to an existing release. +# +# Supported architectures: +# aarch64 - native on ARM runner (existing) +# x86_64 - native on x86_64 runner +# armv7 - cross-compiled on x86_64 using musl.cc armv7l-linux-musleabihf toolchain +# x86 - cross-compiled on x86_64 using musl.cc i686-linux-musl toolchain + +on: + workflow_dispatch: + inputs: + version: + description: 'Release tag (e.g., v1.0.0)' + required: true + ref: + description: 'Git ref to build from (default: tag)' + required: false + default: '' + +permissions: + contents: write + +jobs: + build-android: + name: build-${{ matrix.arch }}-android-static + runs-on: ${{ matrix.runner }} + container: + image: rust:alpine + strategy: + fail-fast: false + matrix: + include: + - arch: aarch64 + runner: ubuntu-24.04-arm + artifact: aarch64-linux-android-musl + outline-atomics: true + - arch: x86_64 + runner: ubuntu-latest + artifact: x86_64-linux-android-musl + outline-atomics: false + - arch: armv7 + runner: ubuntu-latest + artifact: armv7-linux-android-musl + outline-atomics: true + - arch: x86 + runner: ubuntu-latest + artifact: i686-linux-android-musl + outline-atomics: false + + steps: + # aarch64: JS actions don't work in Alpine containers on ARM runners + - name: Checkout repository (aarch64 workaround) + if: matrix.arch == 'aarch64' + run: | + apk add --no-cache git + git config --global --add safe.directory /__w/rustnet/rustnet + git clone --depth 1 https://github.com/${{ github.repository }}.git . + git fetch --depth 1 origin tag "${{ inputs.version }}" + git checkout "${{ inputs.version }}" + + - name: Checkout repository + if: matrix.arch != 'aarch64' + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ inputs.version }} + + - name: Install common dependencies + run: | + apk add --no-cache \ + musl-dev pkgconfig build-base perl \ + zlib-dev zlib-static \ + clang llvm linux-headers github-cli curl + rustup component add rustfmt + + # aarch64/x86_64: libpcap-dev provides a native static libpcap. + # zstd-static is required because Alpine's libpcap-dev links against zstd. + - name: Install libpcap (native) + if: matrix.arch == 'aarch64' || matrix.arch == 'x86_64' + run: apk add --no-cache libpcap-dev elfutils-dev zstd-dev zstd-static + + # armv7: use zig cc as the cross-compiler (no external toolchain download needed). + # zig bundles musl libc for all targets, so no -l:libzstd.a needed. + - name: Install cross-compiler and libpcap (armv7) + if: matrix.arch == 'armv7' + run: | + rustup target add armv7-unknown-linux-musleabihf + apk add zig flex bison bash + + # zig cc wrapper: cc-rs detects it as clang and passes --target=armv7-* + # which zig rejects (zig uses 'arm' not 'armv7'). Filter those args out. + printf '%s\n' \ + '#!/bin/bash' \ + 'args=()' \ + 'for arg in "$@"; do' \ + ' case "$arg" in --target=*) ;; *) args+=("$arg") ;; esac' \ + 'done' \ + 'exec zig cc -target arm-linux-musleabihf "${args[@]}"' \ + > /usr/local/bin/arm-linux-musleabihf-gcc + chmod +x /usr/local/bin/arm-linux-musleabihf-gcc + + # libpcap is pinned by version + sha256 (Dependabot does not track + # tarballs fetched in run steps, so bump the URL and the hash together). + curl -fsSL --retry 3 --retry-delay 5 \ + -o libpcap.tar.gz https://www.tcpdump.org/release/libpcap-1.10.5.tar.gz + echo "37ced90a19a302a7f32e458224a00c365c117905c2cd35ac544b6880a81488f0 libpcap.tar.gz" | sha256sum -c - + tar xzf libpcap.tar.gz + cd libpcap-1.10.5 + CC=arm-linux-musleabihf-gcc \ + ./configure --host=arm-linux-musleabihf \ + --disable-shared --enable-static \ + --disable-usb --disable-dbus --disable-bluetooth --disable-remote \ + --prefix=/arm-sysroot + make -j$(nproc) && make install + cd .. + + # x86: use zig cc as the cross-compiler (no external toolchain download needed). + # zig bundles musl libc for all targets, so no -l:libzstd.a needed. + - name: Install cross-compiler and libpcap (x86/i686) + if: matrix.arch == 'x86' + run: | + rustup target add i686-unknown-linux-musl + apk add zig flex bison bash + + # zig cc wrapper: cc-rs passes --target=i686-* which zig doesn't accept; + # filter those args and use -target x86-linux-musl instead. + printf '%s\n' \ + '#!/bin/bash' \ + 'args=()' \ + 'for arg in "$@"; do' \ + ' case "$arg" in --target=*) ;; *) args+=("$arg") ;; esac' \ + 'done' \ + 'exec zig cc -target x86-linux-musl "${args[@]}"' \ + > /usr/local/bin/i686-linux-musl-gcc + chmod +x /usr/local/bin/i686-linux-musl-gcc + + # libpcap is pinned by version + sha256 (Dependabot does not track + # tarballs fetched in run steps, so bump the URL and the hash together). + curl -fsSL --retry 3 --retry-delay 5 \ + -o libpcap.tar.gz https://www.tcpdump.org/release/libpcap-1.10.5.tar.gz + echo "37ced90a19a302a7f32e458224a00c365c117905c2cd35ac544b6880a81488f0 libpcap.tar.gz" | sha256sum -c - + tar xzf libpcap.tar.gz + cd libpcap-1.10.5 + CC=i686-linux-musl-gcc \ + ./configure --host=i686-linux-musl \ + --disable-shared --enable-static \ + --disable-usb --disable-dbus --disable-bluetooth --disable-remote \ + --prefix=/x86-sysroot + make -j$(nproc) && make install + cd .. + + - name: Build static binary (aarch64) + if: matrix.arch == 'aarch64' + env: + CFLAGS: "-mno-outline-atomics" + CXXFLAGS: "-mno-outline-atomics" + RUSTFLAGS: "-C strip=symbols -C link-arg=-l:libzstd.a" + run: cargo build --release --no-default-features + + - name: Build static binary (x86_64) + if: matrix.arch == 'x86_64' + env: + RUSTFLAGS: "-C strip=symbols -C link-arg=-l:libzstd.a" + run: cargo build --release --no-default-features + + - name: Build static binary (armv7) + if: matrix.arch == 'armv7' + env: + # link-self-contained=no: let zig provide musl CRT (avoid duplicate _start + # when both Rust's self-contained crt1.o and zig's crt1.o are linked) + RUSTFLAGS: "-C strip=symbols -C link-self-contained=no" + CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER: arm-linux-musleabihf-gcc + PKG_CONFIG_PATH: /arm-sysroot/lib/pkgconfig + PKG_CONFIG_SYSROOT_DIR: /arm-sysroot + PKG_CONFIG_ALLOW_CROSS: "1" + run: cargo build --release --target armv7-unknown-linux-musleabihf --no-default-features + + - name: Build static binary (x86/i686) + if: matrix.arch == 'x86' + env: + RUSTFLAGS: "-C strip=symbols -C link-self-contained=no" + CARGO_TARGET_I686_UNKNOWN_LINUX_MUSL_LINKER: i686-linux-musl-gcc + PKG_CONFIG_PATH: /x86-sysroot/lib/pkgconfig + PKG_CONFIG_SYSROOT_DIR: /x86-sysroot + PKG_CONFIG_ALLOW_CROSS: "1" + run: cargo build --release --target i686-unknown-linux-musl --no-default-features + + - name: Verify static linking + run: | + BIN="${{ (matrix.arch == 'armv7' && 'target/armv7-unknown-linux-musleabihf/release/rustnet') || (matrix.arch == 'x86' && 'target/i686-unknown-linux-musl/release/rustnet') || 'target/release/rustnet' }}" + file "$BIN" + file "$BIN" | grep -q "static.* linked" || \ + (echo "ERROR: Binary is not statically linked" && exit 1) + + - name: Create release archive + run: | + VERSION="${{ inputs.version }}" + ARTIFACT="${{ matrix.artifact }}" + + case "${{ matrix.arch }}" in + armv7) BIN="target/armv7-unknown-linux-musleabihf/release/rustnet" ;; + x86) BIN="target/i686-unknown-linux-musl/release/rustnet" ;; + *) BIN="target/release/rustnet" ;; + esac + + staging="rustnet-${VERSION}-${ARTIFACT}" + mkdir -p "$staging/assets" + cp "$BIN" "$staging/rustnet" + cp crates/rustnet-core/assets/services "$staging/assets/" 2>/dev/null || true + cp README.md "$staging/" + cp LICENSE "$staging/" 2>/dev/null || true + tar czf "$staging.tar.gz" "$staging" + + - name: Upload to release + run: | + VERSION="${{ inputs.version }}" + ARCHIVE="rustnet-${VERSION}-${{ matrix.artifact }}.tar.gz" + gh release upload "$VERSION" "$ARCHIVE" --repo "${{ github.repository }}" --clobber + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/build-musl-arm64.yml b/.github/workflows/build-musl-arm64.yml new file mode 100644 index 0000000..d4ad93e --- /dev/null +++ b/.github/workflows/build-musl-arm64.yml @@ -0,0 +1,73 @@ +name: Build ARM64 Musl Static + +# One-off workflow to build and upload aarch64 musl static binary +# to an existing release. Used when the regular release pipeline +# fails to upload this artifact. + +on: + workflow_dispatch: + inputs: + version: + description: 'Release tag (e.g., v1.0.0)' + required: true + ref: + description: 'Git ref to build from (default: tag)' + required: false + default: '' + +permissions: + contents: write + +jobs: + build-musl-arm64: + name: build-aarch64-musl-static + runs-on: ubuntu-24.04-arm + container: + image: rust:alpine + steps: + - name: Checkout repository + run: | + apk add --no-cache git + git config --global --add safe.directory /__w/rustnet/rustnet + REF="${{ inputs.ref || inputs.version }}" + git clone --depth 1 https://github.com/${{ github.repository }}.git . + git fetch --depth 1 origin tag "${{ inputs.version }}" + git checkout "${{ inputs.version }}" + + - name: Install dependencies + run: | + apk add --no-cache \ + musl-dev libpcap-dev pkgconfig build-base perl \ + elfutils-dev zlib-dev zlib-static zstd-dev zstd-static \ + clang llvm linux-headers github-cli + rustup component add rustfmt + + - name: Build static binary + env: + CFLAGS: "-mno-outline-atomics" + CXXFLAGS: "-mno-outline-atomics" + RUSTFLAGS: "-C strip=symbols -C link-arg=-l:libzstd.a" + run: | + cargo build --release + file target/release/rustnet + file target/release/rustnet | grep -q "static.* linked" || \ + (echo "ERROR: Binary is not statically linked" && exit 1) + + - name: Create release archive + run: | + VERSION="${{ inputs.version }}" + staging="rustnet-${VERSION}-aarch64-unknown-linux-musl" + mkdir -p "$staging/assets" + cp target/release/rustnet "$staging/" + cp crates/rustnet-core/assets/services "$staging/assets/" 2>/dev/null || true + cp README.md "$staging/" + cp LICENSE "$staging/" 2>/dev/null || true + tar czf "$staging.tar.gz" "$staging" + + - name: Upload to release + run: | + VERSION="${{ inputs.version }}" + ARCHIVE="rustnet-${VERSION}-aarch64-unknown-linux-musl.tar.gz" + gh release upload "$VERSION" "$ARCHIVE" --clobber + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/build-platforms.yml b/.github/workflows/build-platforms.yml new file mode 100644 index 0000000..46b4fe5 --- /dev/null +++ b/.github/workflows/build-platforms.yml @@ -0,0 +1,353 @@ +name: Build Platforms + +# Reusable workflow for building RustNet on all platforms. +# Called by both test-platform-builds.yml and release.yml + +on: + workflow_call: + inputs: + create-archives: + description: 'Create release archives with README/LICENSE (true for release, false for test)' + type: boolean + default: false + strip-symbols: + description: 'Strip debug symbols from binaries' + type: boolean + default: false + version: + description: 'Version string for archive naming (e.g., v0.3.0)' + type: string + default: '' + +env: + RUST_BACKTRACE: 1 + CARGO_TERM_COLOR: always + +jobs: + build: + permissions: + contents: read + name: build-${{ matrix.build }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + build: + - linux-x64-gnu + - linux-aarch64-gnu + - linux-armv7-gnueabihf + - macos-aarch64 + - macos-x64 + - windows-x64-msvc + - windows-x86-msvc + include: + - os: ubuntu-22.04 + - cargo: cargo + - build: linux-x64-gnu + target: x86_64-unknown-linux-gnu + run-tests: true + - build: linux-aarch64-gnu + target: aarch64-unknown-linux-gnu + cargo: cross + - build: linux-armv7-gnueabihf + target: armv7-unknown-linux-gnueabihf + cargo: cross + - build: macos-aarch64 + os: macos-14 + target: aarch64-apple-darwin + run-tests: true + - build: macos-x64 + os: macos-14 + target: x86_64-apple-darwin + - build: windows-x64-msvc + os: windows-latest + target: x86_64-pc-windows-msvc + - build: windows-x86-msvc + os: windows-latest + target: i686-pc-windows-msvc + + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Setup Linux dependencies + if: matrix.os == 'ubuntu-22.04' + uses: ./.github/actions/setup-linux-deps + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable # unpinned: maintained branch by Rust team member + with: + targets: ${{ matrix.target }} + components: rustfmt + + - name: Install cross + if: matrix.cargo == 'cross' + run: cargo install cross@0.2.5 + + - name: Build + uses: ./.github/actions/build-rustnet + with: + target: ${{ matrix.target }} + cargo-command: ${{ matrix.cargo }} + default-features: ${{ contains(matrix.target, 'linux') && 'true' || 'false' }} + strip-symbols: ${{ inputs.strip-symbols && 'true' || 'false' }} + + - name: Run tests + if: matrix.run-tests + run: cargo test --verbose + + # For test builds: upload raw binary + - name: Upload binary artifact + if: ${{ !inputs.create-archives }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: rustnet-${{ matrix.build }} + path: target/${{ matrix.target }}/release/rustnet${{ contains(matrix.os, 'windows') && '.exe' || '' }} + if-no-files-found: error + + # For release builds: create archive with README/LICENSE + - name: Create release archive + if: ${{ inputs.create-archives }} + shell: bash + env: + RUSTNET_BIN: ${{ contains(matrix.os, 'windows') && 'rustnet.exe' || 'rustnet' }} + run: | + staging="rustnet-${{ inputs.version }}-${{ matrix.target }}" + mkdir -p "$staging" + + cp "target/${{ matrix.target }}/release/$RUSTNET_BIN" "$staging/" + + if [ -d "crates/rustnet-core/assets" ] && [ -f "crates/rustnet-core/assets/services" ]; then + mkdir -p "$staging/assets" + cp "crates/rustnet-core/assets/services" "$staging/assets/" + fi + + cp README.md "$staging/" + cp LICENSE "$staging/" 2>/dev/null || true + + if [[ "${{ matrix.os }}" == "windows-latest" ]]; then + 7z a "$staging.zip" "$staging" + echo "ASSET=$staging.zip" >> $GITHUB_ENV + else + tar czf "$staging.tar.gz" "$staging" + echo "ASSET=$staging.tar.gz" >> $GITHUB_ENV + fi + + - name: Upload release archive + if: ${{ inputs.create-archives }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: build-${{ matrix.target }} + path: ${{ env.ASSET }} + if-no-files-found: error + + build-static: + permissions: + contents: read + name: build-linux-static-${{ matrix.arch }} + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - arch: x86_64 + runner: ubuntu-latest + target: x86_64-unknown-linux-musl + - arch: aarch64 + runner: ubuntu-24.04-arm + target: aarch64-unknown-linux-musl + features: '--features default' + - arch: aarch64-android + runner: ubuntu-24.04-arm + target: aarch64-linux-android-musl # Note: it's not a real rust target but we rename the artifact + features: '--no-default-features' + - arch: x86_64-android + runner: ubuntu-latest + target: x86_64-linux-android-musl + features: '--no-default-features' + - arch: armv7-android + runner: ubuntu-latest + target: armv7-linux-android-musl + features: '--no-default-features' + - arch: x86-android + runner: ubuntu-latest + target: i686-linux-android-musl + features: '--no-default-features' + container: + image: rust:alpine + steps: + # x86_64: use standard checkout action + - name: Checkout repository + if: matrix.arch == 'x86_64' || matrix.arch == 'x86_64-android' || matrix.arch == 'armv7-android' || matrix.arch == 'x86-android' + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + # aarch64: JS actions don't work in Alpine containers on ARM runners + - name: Checkout repository (ARM workaround) + if: contains(matrix.arch, 'aarch64') + run: | + apk add --no-cache git + git config --global --add safe.directory /__w/rustnet/rustnet + git clone --depth 1 https://github.com/${{ github.repository }}.git . + if [[ "${{ github.ref }}" == refs/tags/* ]]; then + git fetch --depth 1 origin tag "${{ github.ref_name }}" + git checkout "${{ github.ref_name }}" + fi + + # x86_64: use composite action + - name: Build static binary + if: matrix.arch == 'x86_64' + uses: ./.github/actions/build-static + + # aarch64 & android: inline build (composite actions don't work with ARM workaround/zig wrapper easily here) + - name: Install dependencies (ARM & Android) + if: matrix.arch != 'x86_64' + run: | + apk add --no-cache \ + musl-dev libpcap-dev pkgconfig build-base perl \ + elfutils-dev zlib-dev zlib-static zstd-dev zstd-static \ + clang llvm linux-headers curl github-cli + rustup component add rustfmt + + - name: Install cross-compiler and libpcap (armv7) + if: matrix.arch == 'armv7-android' + run: | + rustup target add armv7-unknown-linux-musleabihf + apk add zig flex bison bash + + printf '%s\n' \ + '#!/bin/bash' \ + 'args=()' \ + 'for arg in "$@"; do' \ + ' case "$arg" in --target=*) ;; *) args+=("$arg") ;; esac' \ + 'done' \ + 'exec zig cc -target arm-linux-musleabihf "${args[@]}"' \ + > /usr/local/bin/arm-linux-musleabihf-gcc + chmod +x /usr/local/bin/arm-linux-musleabihf-gcc + + # libpcap is pinned by version + sha256 (Dependabot does not track + # tarballs fetched in run steps, so bump the URL and the hash together). + curl -fsSL --retry 3 --retry-delay 5 \ + -o libpcap.tar.gz https://www.tcpdump.org/release/libpcap-1.10.5.tar.gz + echo "37ced90a19a302a7f32e458224a00c365c117905c2cd35ac544b6880a81488f0 libpcap.tar.gz" | sha256sum -c - + tar xzf libpcap.tar.gz + cd libpcap-1.10.5 + CC=arm-linux-musleabihf-gcc \ + ./configure --host=arm-linux-musleabihf \ + --disable-shared --enable-static \ + --disable-usb --disable-dbus --disable-bluetooth --disable-remote \ + --prefix=/arm-sysroot + make -j$(nproc) && make install + cd .. + + - name: Install cross-compiler and libpcap (x86/i686) + if: matrix.arch == 'x86-android' + run: | + rustup target add i686-unknown-linux-musl + apk add zig flex bison bash + + printf '%s\n' \ + '#!/bin/bash' \ + 'args=()' \ + 'for arg in "$@"; do' \ + ' case "$arg" in --target=*) ;; -Wl,-melf_i386) ;; *) args+=("$arg") ;; esac' \ + 'done' \ + 'exec zig cc -target x86-linux-musl "${args[@]}"' \ + > /usr/local/bin/i686-linux-musl-gcc + chmod +x /usr/local/bin/i686-linux-musl-gcc + + # libpcap is pinned by version + sha256 (Dependabot does not track + # tarballs fetched in run steps, so bump the URL and the hash together). + curl -fsSL --retry 3 --retry-delay 5 \ + -o libpcap.tar.gz https://www.tcpdump.org/release/libpcap-1.10.5.tar.gz + echo "37ced90a19a302a7f32e458224a00c365c117905c2cd35ac544b6880a81488f0 libpcap.tar.gz" | sha256sum -c - + tar xzf libpcap.tar.gz + cd libpcap-1.10.5 + CC=i686-linux-musl-gcc \ + ./configure --host=i686-linux-musl \ + --disable-shared --enable-static \ + --disable-usb --disable-dbus --disable-bluetooth --disable-remote \ + --prefix=/x86-sysroot + make -j$(nproc) && make install + cd .. + + - name: Build static binary (AArch64 / AArch64 Android) + if: matrix.arch == 'aarch64' || matrix.arch == 'aarch64-android' + env: + # -mno-outline-atomics: Prevent GCC from generating calls to __aarch64_ldadd4_sync etc. + # which aren't in Alpine's libatomic. Uses inline atomics instead. + CFLAGS: "-mno-outline-atomics" + CXXFLAGS: "-mno-outline-atomics" + RUSTFLAGS: "-C strip=symbols -C link-arg=-l:libzstd.a" + run: cargo build --release ${{ matrix.features }} + + - name: Build static binary (x86_64 Android) + if: matrix.arch == 'x86_64-android' + env: + RUSTFLAGS: "-C strip=symbols -C link-arg=-l:libzstd.a" + run: cargo build --release ${{ matrix.features }} + + - name: Build static binary (armv7-android) + if: matrix.arch == 'armv7-android' + env: + RUSTFLAGS: "-C strip=symbols -C link-self-contained=no" + CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER: arm-linux-musleabihf-gcc + PKG_CONFIG_PATH: /arm-sysroot/lib/pkgconfig + PKG_CONFIG_SYSROOT_DIR: /arm-sysroot + PKG_CONFIG_ALLOW_CROSS: "1" + run: cargo build --release --target armv7-unknown-linux-musleabihf ${{ matrix.features }} + + - name: Build static binary (x86-android) + if: matrix.arch == 'x86-android' + env: + RUSTFLAGS: "-C strip=symbols -C link-self-contained=no" + CARGO_TARGET_I686_UNKNOWN_LINUX_MUSL_LINKER: i686-linux-musl-gcc + PKG_CONFIG_PATH: /x86-sysroot/lib/pkgconfig + PKG_CONFIG_SYSROOT_DIR: /x86-sysroot + PKG_CONFIG_ALLOW_CROSS: "1" + run: cargo build --release --target i686-unknown-linux-musl ${{ matrix.features }} + + - name: Verify static linking (ARM & Android) + if: matrix.arch != 'x86_64' + run: | + BIN="${{ (matrix.arch == 'armv7-android' && 'target/armv7-unknown-linux-musleabihf/release/rustnet') || (matrix.arch == 'x86-android' && 'target/i686-unknown-linux-musl/release/rustnet') || 'target/release/rustnet' }}" + file "$BIN" + file "$BIN" | grep -q "static.* linked" || \ + (echo "ERROR: Binary is not statically linked" && exit 1) + + # For test builds: upload raw binary + - name: Upload binary artifact + if: ${{ !inputs.create-archives }} + continue-on-error: ${{ matrix.arch != 'x86_64' }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: rustnet-linux-static-${{ matrix.arch }} + path: ${{ (matrix.arch == 'armv7-android' && 'target/armv7-unknown-linux-musleabihf/release/rustnet') || (matrix.arch == 'x86-android' && 'target/i686-unknown-linux-musl/release/rustnet') || 'target/release/rustnet' }} + if-no-files-found: error + + # For release builds: create archive + - name: Create release archive + if: ${{ inputs.create-archives }} + run: | + BIN="${{ (matrix.arch == 'armv7-android' && 'target/armv7-unknown-linux-musleabihf/release/rustnet') || (matrix.arch == 'x86-android' && 'target/i686-unknown-linux-musl/release/rustnet') || 'target/release/rustnet' }}" + + staging="rustnet-${{ inputs.version }}-${{ matrix.target }}" + mkdir -p "$staging/assets" + + cp "$BIN" "$staging/rustnet" + cp crates/rustnet-core/assets/services "$staging/assets/" 2>/dev/null || true + cp README.md "$staging/" + cp LICENSE "$staging/" 2>/dev/null || true + + tar czf "$staging.tar.gz" "$staging" + + - name: Upload release archive + if: ${{ inputs.create-archives }} + # JS actions don't work in Alpine containers on ARM runners; + # release.yml has dedicated upload-arm-static/upload-android-static jobs as fallback + continue-on-error: ${{ contains(matrix.arch, 'aarch64') }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: build-${{ matrix.target }} + path: rustnet-${{ inputs.version }}-${{ matrix.target }}.tar.gz + if-no-files-found: error + diff --git a/.github/workflows/copr-update-spec.yml b/.github/workflows/copr-update-spec.yml new file mode 100644 index 0000000..d5006f5 --- /dev/null +++ b/.github/workflows/copr-update-spec.yml @@ -0,0 +1,72 @@ +name: Update COPR Spec Version + +on: + workflow_call: + workflow_dispatch: + +permissions: + contents: write + +jobs: + update-spec: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract version from tag + id: version + run: | + # Extract version by removing 'v' prefix + VERSION=${GITHUB_REF#refs/tags/v} + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Extracted version: $VERSION" + + - name: Update RPM spec file + run: | + VERSION="${{ steps.version.outputs.version }}" + SPEC_FILE="rpm/rustnet.spec" + + # Update the Version: line in the spec file + sed -i "s/^Version:.*/Version: $VERSION/" "$SPEC_FILE" + + echo "Updated $SPEC_FILE to version $VERSION" + echo "" + echo "Version line:" + grep "^Version:" "$SPEC_FILE" + + - name: Commit and push changes + run: | + VERSION="${{ steps.version.outputs.version }}" + + # Configure git + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + # Check if there are changes to commit + if git diff --quiet rpm/rustnet.spec; then + echo "No changes to rpm/rustnet.spec (already at version $VERSION)" + exit 0 + fi + + # Commit and push + git add rpm/rustnet.spec + git commit -m "chore: update RPM spec to version $VERSION" + git push origin HEAD:main + + echo "Pushed changes to main branch" + + - name: Summary + run: | + echo "## 🎉 RPM Spec Updated" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **Version**: ${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY + echo "- **File**: rpm/rustnet.spec" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "COPR will automatically rebuild the package via webhook." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "[View COPR Builds →](https://copr.fedorainfracloud.org/coprs/domcyrus/rustnet/builds/)" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..99ee5a7 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,72 @@ +name: Docker Build and Publish +on: + workflow_call: + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + attestations: write + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4 + + - name: Log in to Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + # Enable Kubernetes pod/container attribution in the container image. + # It is dependency-free and runtime-gated (`--kubernetes auto`, the + # default, is inert outside a pod), so a single image serves both + # general container use and kubectl-rustnet. Native installs + # (cargo/brew/deb/rpm) leave the feature off and stay lean. + build-args: | + CARGO_FEATURES=kubernetes + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Generate artifact attestation + if: github.event_name != 'pull_request' + uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}} + subject-digest: ${{ steps.build-and-push.outputs.digest }} + push-to-registry: true \ No newline at end of file diff --git a/.github/workflows/obs-release.yml b/.github/workflows/obs-release.yml new file mode 100644 index 0000000..7aeedf5 --- /dev/null +++ b/.github/workflows/obs-release.yml @@ -0,0 +1,146 @@ +name: Release to OBS + +on: + workflow_call: + workflow_dispatch: + +permissions: + contents: read + +jobs: + release-obs: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 + + - name: Get version + id: version + run: | + # Extract version by removing 'v' prefix if it exists, fallback to Cargo.toml + if [[ $GITHUB_REF == refs/tags/* ]]; then + VERSION=${GITHUB_REF#refs/tags/v} + else + # Read the binary's [package] version, not [workspace.package] (the + # latter is the 0.x library version and comes first in Cargo.toml). + VERSION=$(awk -F'"' '/^\[package\]/{p=1} p && /^version = /{print $2; exit}' Cargo.toml) + fi + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Version: $VERSION" + + - name: Setup Linux dependencies + uses: ./.github/actions/setup-linux-deps + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable # unpinned: maintained branch by Rust team member + with: + toolchain: stable + + - name: Install OBS dependencies + run: | + sudo apt-get update + sudo apt-get install -y osc obs-build build-essential + + - name: Configure OBS credentials + run: | + cat < ~/.oscrc + [general] + apiurl = https://api.opensuse.org + + [https://api.opensuse.org] + user = ${{ secrets.OBS_USER }} + pass = ${{ secrets.OBS_PASS }} + EOF + chmod 600 ~/.oscrc + + - name: Download source tarball + run: | + VERSION="${{ steps.version.outputs.version }}" + PACKAGE_NAME="rustnet" + + # Output filename must match Source0's basename (v%{version}.tar.gz) + # so the offline OBS build can resolve it; the internal dir prefix + # stays rustnet-%{version}/ to match %autosetup -n. + RELEASE_TAG="v${VERSION}" + if git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then + echo "✓ Found release tag: $RELEASE_TAG" + git archive --format=tar --prefix="${PACKAGE_NAME}-${VERSION}/" "$RELEASE_TAG" | gzip > "v${VERSION}.tar.gz" + else + echo "⚠ Release tag $RELEASE_TAG not found, using HEAD" + git archive --format=tar --prefix="${PACKAGE_NAME}-${VERSION}/" HEAD | gzip > "v${VERSION}.tar.gz" + fi + + - name: Vendor dependencies + run: | + VERSION="${{ steps.version.outputs.version }}" + PACKAGE_NAME="rustnet" + RELEASE_TAG="v${VERSION}" + + mkdir -p build-vendor + if git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then + git archive --format=tar "$RELEASE_TAG" | tar -x -C build-vendor + else + git archive --format=tar HEAD | tar -x -C build-vendor + fi + + cd build-vendor + # Capture the source-replacement config that cargo prints. The OBS + # build has no network, so it needs this at .cargo/config.toml to + # resolve crates from the vendored directory instead of crates.io. + mkdir -p .cargo + cargo vendor vendor > .cargo/config.toml + + # Clean up static libs to reduce size + find vendor -name "*.a" -delete + find vendor -name "*.lib" -delete + + # Bundle .cargo/config.toml alongside vendor/ so `%autosetup -a 1` + # drops both into the source tree for the offline build. + tar --use-compress-program="zstd -T0 -10" -cf ../vendor.tar.zst .cargo vendor + cd .. + rm -rf build-vendor + + - name: Prepare and push to OBS + run: | + VERSION="${{ steps.version.outputs.version }}" + OBS_PROJECT="${{ secrets.OBS_PROJECT }}" + OBS_PACKAGE="rustnet" + + if [ -z "$OBS_PROJECT" ]; then + echo "::error::OBS_PROJECT secret is not set!" + exit 1 + fi + + # Checkout OBS project + osc checkout "$OBS_PROJECT" "$OBS_PACKAGE" + cd "$OBS_PROJECT/$OBS_PACKAGE" + + # Clean old files + rm -f *.tar.gz *.tar.zst *.spec + + # Copy new files + cp ../../v${VERSION}.tar.gz . + cp ../../vendor.tar.zst . + cp ../../rpm/rustnet.spec . + + # Update version in spec file + sed -i "s/^Version:.*/Version: $VERSION/" rustnet.spec + + # Prepend a changelog entry. OBS has no rpmautospec, so the spec's + # %changelog is empty on SUSE and the changelog lives here instead. + STAMP="$(LC_ALL=C date -u '+%a %b %e %T %Z %Y')" + { + printf -- '-------------------------------------------------------------------\n' + printf '%s - %s@opensuse.org\n\n' "$STAMP" "${{ secrets.OBS_USER }}" + printf -- '- Update to version %s\n\n' "$VERSION" + [ -f rustnet.changes ] && cat rustnet.changes + } > rustnet.changes.new + mv rustnet.changes.new rustnet.changes + + # Add new files, remove deleted ones + osc addremove + + # Commit to OBS + osc commit -m "Update to version $VERSION" diff --git a/.github/workflows/ppa-release.yml b/.github/workflows/ppa-release.yml new file mode 100644 index 0000000..e4e37c6 --- /dev/null +++ b/.github/workflows/ppa-release.yml @@ -0,0 +1,254 @@ +name: Release to Ubuntu PPA + +on: + workflow_call: + inputs: + tarball_suffix: + description: 'Tarball suffix (e.g., ds1, ds2)' + required: false + type: string + default: '' + workflow_dispatch: + inputs: + ubuntu_release: + description: 'Ubuntu release codename' + required: true + default: 'resolute' + type: choice + options: + - questing # 25.10 + - resolute # 26.04 LTS + tarball_suffix: + description: 'Tarball suffix (e.g., ds1, ds2) - leave empty for new releases' + required: false + default: '' + type: string + +permissions: + contents: read + +env: + DEBEMAIL: cadetg@gmail.com + DEBFULLNAME: Marco Cadetg + PPA: ppa:domcyrus/rustnet + +jobs: + set-matrix: + runs-on: ubuntu-24.04 + outputs: + releases: ${{ steps.set.outputs.releases }} + steps: + - id: set + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo 'releases=["${{ inputs.ubuntu_release }}"]' >> $GITHUB_OUTPUT + echo "Dispatch: building only ${{ inputs.ubuntu_release }}" + else + echo 'releases=["questing","resolute"]' >> $GITHUB_OUTPUT + echo "Auto: building questing and resolute" + fi + + build-and-upload: + needs: set-matrix + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + ubuntu_release: ${{ fromJSON(needs.set-matrix.outputs.releases) }} + + steps: + - name: Checkout code + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + debhelper \ + devscripts \ + dput \ + gnupg \ + libpcap-dev \ + libelf-dev \ + elfutils \ + zlib1g-dev \ + clang \ + llvm \ + pkg-config + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable # unpinned: maintained branch by Rust team member + with: + toolchain: stable + + - name: Import GPG key + env: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + run: | + echo "$GPG_PRIVATE_KEY" | gpg --batch --import + gpg --list-secret-keys + + - name: Get version + id: version + run: | + # Read the binary's [package] version, not [workspace.package]. Since + # the workspace split, Cargo.toml has two `version =` lines and the + # workspace one (0.x libs) comes first, so `head -1` read the wrong + # value. Anchor on the [package] section. + VERSION=$(awk -F'"' '/^\[package\]/{p=1} p && /^version = /{print $2; exit}' Cargo.toml) + + # Add tarball suffix if provided (e.g., +ds1, +ds2) + TARBALL_SUFFIX="${{ inputs.tarball_suffix }}" + if [ -n "$TARBALL_SUFFIX" ]; then + TARBALL_VERSION="${VERSION}+${TARBALL_SUFFIX}" + echo "version=$TARBALL_VERSION" >> $GITHUB_OUTPUT + echo "Using tarball version: $TARBALL_VERSION" + else + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Using version: $VERSION" + fi + + - name: Update changelog + run: | + VERSION="${{ steps.version.outputs.version }}" + # Per-series version suffix so each Ubuntu release has an independent + # version line on Launchpad. The `+series1` suffix sorts strictly + # higher than a plain `-1ubuntu1`, so future uploads always supersede + # any previously published package. + NEW_VERSION="${VERSION}-1ubuntu1+${{ matrix.ubuntu_release }}1" + + DEBFULLNAME="${{ env.DEBFULLNAME }}" DEBEMAIL="${{ env.DEBEMAIL }}" \ + dch --newversion "$NEW_VERSION" \ + --distribution "${{ matrix.ubuntu_release }}" \ + "Build for ${{ matrix.ubuntu_release }} - upstream release $VERSION" + + echo "✓ Changelog set to $NEW_VERSION (${{ matrix.ubuntu_release }})" + + - name: Pin Rust toolchain version per release + run: | + # Each Ubuntu release ships a different set of versioned rustc/cargo + # packages. We pin to the lowest version available on the target + # release that still satisfies our >= 1.88 floor (let-chains). + case "${{ matrix.ubuntu_release }}" in + questing) RUST_VERSION=1.88 ;; + resolute) RUST_VERSION=1.91 ;; + *) echo "::error::Unknown release ${{ matrix.ubuntu_release }} - add it to the case statement"; exit 1 ;; + esac + echo "Pinning to rustc-$RUST_VERSION / cargo-$RUST_VERSION for ${{ matrix.ubuntu_release }}" + + sed -i -E "s/(cargo|rustc|rustdoc)-1\.[0-9]+/\1-${RUST_VERSION}/g" debian/control debian/rules + + echo "--- debian/control build-depends ---" + grep -E "(cargo|rustc)-1\." debian/control || true + echo "--- debian/rules toolchain exports ---" + grep -E "(CARGO|RUSTC|RUSTDOC) *=" debian/rules || true + + - name: Build source package + run: | + VERSION="${{ steps.version.outputs.version }}" + # Binary [package] version (drives the release tag to check out below). + BASE_VERSION=$(awk -F'"' '/^\[package\]/{p=1} p && /^version = /{print $2; exit}' Cargo.toml) + PACKAGE_NAME="rustnet-monitor" + + # Create build directory + mkdir -p build-ppa + + # Extract source from release tag + RELEASE_TAG="v${BASE_VERSION}" + if git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then + echo "✓ Found release tag: $RELEASE_TAG" + git archive --format=tar --prefix="${PACKAGE_NAME}-${VERSION}/" "$RELEASE_TAG" | tar -x -C build-ppa + else + echo "⚠ Release tag $RELEASE_TAG not found, using HEAD" + git archive --format=tar --prefix="${PACKAGE_NAME}-${VERSION}/" HEAD | tar -x -C build-ppa + fi + + # Vendor dependencies separately from orig tarball + echo "Vendoring Rust dependencies..." + cd build-ppa/${PACKAGE_NAME}-${VERSION} + + cargo vendor vendor + + # Remove prebuilt static libraries (keep .dll for tests) + echo "Cleaning vendor directory..." + find vendor -name "*.a" -delete + find vendor -name "*.lib" -delete + + # Pack vendor directory as separate tarball in debian/ + echo "Creating vendor tarball..." + tar -cJf ../vendor.tar.xz vendor + rm -rf vendor + + # Create orig tarball (without vendor directory) + echo "Creating orig tarball..." + cd .. + ORIG_TARBALL="${PACKAGE_NAME}_${VERSION}.orig.tar.gz" + tar -czf "${ORIG_TARBALL}" "${PACKAGE_NAME}-${VERSION}" + + # Add debian directory and vendor tarball + cp -r "$GITHUB_WORKSPACE/debian" "${PACKAGE_NAME}-${VERSION}/" + mv vendor.tar.xz "${PACKAGE_NAME}-${VERSION}/debian/" + + # Build source package + cd "${PACKAGE_NAME}-${VERSION}" + + # Always use -sa to include orig tarball + # Launchpad will reuse existing file if hash matches + debuild -S -sa -d -us -uc + + - name: Sign and upload + env: + GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }} + run: | + cd build-ppa + # Locate the source.changes file produced by debuild. The version + # suffix (+seriesN) is dynamic, so glob rather than reconstruct. + CHANGES_FILE=$(ls rustnet-monitor_*_source.changes | head -1) + if [ -z "$CHANGES_FILE" ]; then + echo "::error::No source.changes file found in build-ppa" + exit 1 + fi + echo "Signing and uploading $CHANGES_FILE" + + # Sign + debsign -k${GPG_KEY_ID} ${CHANGES_FILE} + + # Verify + gpg --verify ${CHANGES_FILE} + + # Upload to PPA + dput ${{ env.PPA }} ${CHANGES_FILE} + + - name: Upload artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: ppa-source-${{ matrix.ubuntu_release }} + path: | + build-ppa/*.dsc + build-ppa/*.tar.gz + build-ppa/*.tar.xz + build-ppa/*.changes + build-ppa/*.buildinfo + retention-days: 30 + + - name: Summary + run: | + cd build-ppa + CHANGES_FILE=$(ls rustnet-monitor_*_source.changes | head -1) + FULL_VERSION=$(echo "$CHANGES_FILE" | sed 's/rustnet-monitor_\(.*\)_source\.changes/\1/') + + echo "## 🎉 PPA Upload Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **Package**: rustnet-monitor" >> $GITHUB_STEP_SUMMARY + echo "- **Version**: $FULL_VERSION" >> $GITHUB_STEP_SUMMARY + echo "- **Ubuntu**: ${{ matrix.ubuntu_release }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Installation" >> $GITHUB_STEP_SUMMARY + echo '```bash' >> $GITHUB_STEP_SUMMARY + echo "sudo add-apt-repository ppa:domcyrus/rustnet" >> $GITHUB_STEP_SUMMARY + echo "sudo apt update && sudo apt install rustnet" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "[View PPA](https://launchpad.net/~domcyrus/+archive/ubuntu/rustnet/+packages)" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..47f3bfa --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,64 @@ +name: Publish to crates.io + +on: + workflow_call: + workflow_dispatch: + +permissions: + contents: read + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Install dependencies + run: | + sudo apt-get update -y + sudo apt-get install -y libpcap-dev libelf-dev zlib1g-dev clang llvm pkg-config + + - name: Set up Rust + uses: dtolnay/rust-toolchain@stable # unpinned: maintained branch by Rust team member + + - name: Publish to crates.io + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} + run: | + # True if $crate@$VERSION is already on crates.io. `cargo search` does + # fuzzy matching and prints `name = "version" # description`, so we + # anchor on the exact `name = "version"` prefix. A bare `grep VERSION` + # would false-match the version string appearing in a description, and + # `--limit 1` alone isn't guaranteed to be the exact crate. + is_published() { + cargo search "$1" --limit 20 \ + | grep -qF "$1 = \"$2\"" + } + + # The workspace must be published in dependency order: rustnet-core + # first, then the crates that depend on it, then the binary last. + # Each crate's path deps also carry a version, so dependents resolve + # against crates.io once their dependencies are indexed. + for crate in rustnet-core rustnet-capture rustnet-host rustnet-monitor; do + VERSION=$(cargo metadata --no-deps --format-version 1 \ + | jq -r --arg n "$crate" '.packages[] | select(.name == $n) | .version') + if is_published "$crate" "$VERSION"; then + echo "⚠️ $crate@$VERSION already published to crates.io, skipping" + continue + fi + echo "📦 Publishing $crate@$VERSION" + cargo publish -p "$crate" + # Wait for the new version to appear in the index before publishing + # a dependent that requires it. + if [ "$crate" != "rustnet-monitor" ]; then + for _ in $(seq 1 30); do + if is_published "$crate" "$VERSION"; then + break + fi + echo "⏳ waiting for $crate@$VERSION to appear on crates.io..." + sleep 10 + done + fi + done diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8b4bdec --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,817 @@ +name: Release + +# This workflow builds and packages Rustnet for multiple platforms. +# +# For macOS code signing and notarization, configure these GitHub repository secrets: +# - APPLE_DEVELOPER_CERTIFICATE_P12: Base64-encoded .p12 certificate file +# (Export from Keychain: Developer ID Application certificate → Export as .p12 → base64 encode) +# - APPLE_DEVELOPER_CERTIFICATE_PASSWORD: Password for the .p12 file +# - APPLE_ID_ISSUER_ID: App Store Connect API issuer ID (from App Store Connect → Users and Access → Keys) +# - APPLE_ID_KEY_ID: App Store Connect API key ID +# - APPLE_ID_KEY: App Store Connect API key content (.p8 file contents) +# +# Without these secrets, the DMG will be unsigned and users must use "Open Anyway" in +# System Settings → Privacy & Security to bypass Gatekeeper protection. +# +# To obtain Apple Developer credentials: +# 1. Join the Apple Developer Program ($99/year): https://developer.apple.com/programs/ +# 2. Create a Developer ID Application certificate in your Apple Developer account +# 3. Create an App Store Connect API key with "Developer" role +# 4. Export certificate as .p12, encode with: base64 -i certificate.p12 | pbcopy + +on: + push: + tags: + - "v[0-9]+.[0-9]+.[0-9]+" + workflow_dispatch: + +permissions: + contents: write + packages: write + attestations: write + id-token: write + +env: + RUST_BACKTRACE: 1 + RUSTNET_ASSET_DIR: assets + +jobs: + # Build all platforms using shared workflow + build: + uses: ./.github/workflows/build-platforms.yml + with: + create-archives: true + strip-symbols: true + version: ${{ github.ref_name }} + + create-release: + name: create-release + runs-on: ubuntu-latest + needs: build + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Download all artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + path: artifacts + + - name: Extract changelog for release + run: | + # Extract the version-specific section from CHANGELOG.md + VERSION="${{ github.ref_name }}" + + # Remove 'v' prefix if present for matching changelog headers + VERSION_NUMBER="${VERSION#v}" + + # Extract content between version headers + awk -v version="$VERSION_NUMBER" ' + /^## \[/ { + if (found) exit + if ($0 ~ "\\[" version "\\]") { + found=1 + next + } + } + found { print } + ' CHANGELOG.md > release_notes.md + + # Check if we extracted any content + if [ ! -s release_notes.md ]; then + echo "⚠️ No changelog entry found for $VERSION, will use auto-generated notes" + echo "USE_GENERATED_NOTES=true" >> "$GITHUB_ENV" + else + echo "✅ Extracted changelog content for $VERSION" + cat release_notes.md + fi + + - name: Create Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Create the release if it doesn't exist + if ! gh release view ${{ github.ref_name }} 2>/dev/null; then + if [ "$USE_GENERATED_NOTES" = "true" ]; then + # Fallback to auto-generated notes if changelog extraction failed + gh release create ${{ github.ref_name }} \ + --title "Release ${{ github.ref_name }}" \ + --draft \ + --generate-notes + else + # Use extracted changelog content + gh release create ${{ github.ref_name }} \ + --title "Release ${{ github.ref_name }}" \ + --draft \ + --notes-file release_notes.md + fi + fi + + # Upload all build artifacts + for artifact in artifacts/build-*/rustnet-*; do + gh release upload ${{ github.ref_name }} "$artifact" --clobber + done + + # aarch64 static: JS actions don't work in Alpine containers on ARM runners, + # so the reusable workflow can't upload artifacts. Build and upload here instead, + # where we have contents: write from the workflow-level permissions. + upload-arm-static: + name: upload-arm-static + runs-on: ubuntu-24.04-arm + needs: create-release + container: + image: rust:alpine + steps: + - name: Checkout repository (ARM workaround) + run: | + apk add --no-cache git + git config --global --add safe.directory /__w/rustnet/rustnet + git clone --depth 1 "https://github.com/${{ github.repository }}.git" . + # shellcheck disable=SC2193 + if [[ "${{ github.ref }}" == refs/tags/* ]]; then + git fetch --depth 1 origin tag "${{ github.ref_name }}" + git checkout "${{ github.ref_name }}" + fi + + - name: Install dependencies + run: | + apk add --no-cache \ + musl-dev libpcap-dev pkgconfig build-base perl \ + elfutils-dev zlib-dev zlib-static zstd-dev zstd-static \ + clang llvm linux-headers + rustup component add rustfmt + + - name: Build static binary + env: + CFLAGS: "-mno-outline-atomics" + CXXFLAGS: "-mno-outline-atomics" + RUSTFLAGS: "-C strip=symbols -C link-arg=-l:libzstd.a" + run: cargo build --release + + - name: Verify static linking + run: | + file target/release/rustnet + file target/release/rustnet | grep -q "static.* linked" || \ + (echo "ERROR: Binary is not statically linked" && exit 1) + + - name: Create and upload release archive + run: | + staging="rustnet-${{ github.ref_name }}-aarch64-unknown-linux-musl" + mkdir -p "$staging/assets" + + cp target/release/rustnet "$staging/" + cp crates/rustnet-core/assets/services "$staging/assets/" 2>/dev/null || true + cp README.md "$staging/" + cp LICENSE "$staging/" 2>/dev/null || true + + tar czf "$staging.tar.gz" "$staging" + + apk add --no-cache github-cli + gh release upload "${{ github.ref_name }}" "$staging.tar.gz" --clobber + env: + GH_TOKEN: ${{ github.token }} + + upload-android-static: + name: upload-${{ matrix.arch }}-android-static + runs-on: ${{ matrix.runner }} + needs: create-release + container: + image: rust:alpine + strategy: + fail-fast: false + matrix: + include: + - arch: aarch64 + runner: ubuntu-24.04-arm + artifact: aarch64-linux-android-musl + - arch: x86_64 + runner: ubuntu-latest + artifact: x86_64-linux-android-musl + - arch: armv7 + runner: ubuntu-latest + artifact: armv7-linux-android-musl + - arch: x86 + runner: ubuntu-latest + artifact: i686-linux-android-musl + + steps: + # aarch64: JS actions don't work in Alpine containers on ARM runners + - name: Checkout repository (aarch64 workaround) + if: matrix.arch == 'aarch64' + run: | + apk add --no-cache git + git config --global --add safe.directory /__w/rustnet/rustnet + git clone --depth 1 "https://github.com/${{ github.repository }}.git" . + # shellcheck disable=SC2193 + if [[ "${{ github.ref }}" == refs/tags/* ]]; then + git fetch --depth 1 origin tag "${{ github.ref_name }}" + git checkout "${{ github.ref_name }}" + fi + + - name: Checkout repository + if: matrix.arch != 'aarch64' + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Install common dependencies + run: | + apk add --no-cache \ + musl-dev pkgconfig build-base perl \ + zlib-dev zlib-static \ + clang llvm linux-headers github-cli curl + rustup component add rustfmt + + # aarch64/x86_64: libpcap-dev provides a native static libpcap. + # zstd-static is required because Alpine's libpcap-dev links against zstd. + - name: Install libpcap (native) + if: matrix.arch == 'aarch64' || matrix.arch == 'x86_64' + run: apk add --no-cache libpcap-dev elfutils-dev zstd-dev zstd-static + + # armv7: use zig cc as the cross-compiler (no external toolchain download needed). + # zig bundles musl libc for all targets, so no -l:libzstd.a needed. + - name: Install cross-compiler and libpcap (armv7) + if: matrix.arch == 'armv7' + run: | + rustup target add armv7-unknown-linux-musleabihf + apk add zig flex bison bash + + # zig cc wrapper: cc-rs detects it as clang and passes --target=armv7-* + # which zig rejects (zig uses 'arm' not 'armv7'). Filter those args out. + # shellcheck disable=SC2016 + printf '%s\n' \ + '#!/bin/bash' \ + 'args=()' \ + 'for arg in "$@"; do' \ + ' case "$arg" in --target=*) ;; *) args+=("$arg") ;; esac' \ + 'done' \ + 'exec zig cc -target arm-linux-musleabihf "${args[@]}"' \ + > /usr/local/bin/arm-linux-musleabihf-gcc + chmod +x /usr/local/bin/arm-linux-musleabihf-gcc + + # libpcap is pinned by version + sha256 (Dependabot does not track + # tarballs fetched in run steps, so bump the URL and the hash together). + curl -fsSL --retry 3 --retry-delay 5 \ + -o libpcap.tar.gz https://www.tcpdump.org/release/libpcap-1.10.5.tar.gz + echo "37ced90a19a302a7f32e458224a00c365c117905c2cd35ac544b6880a81488f0 libpcap.tar.gz" | sha256sum -c - + tar xzf libpcap.tar.gz + cd libpcap-1.10.5 + CC=arm-linux-musleabihf-gcc \ + ./configure --host=arm-linux-musleabihf \ + --disable-shared --enable-static \ + --disable-usb --disable-dbus --disable-bluetooth --disable-remote \ + --prefix=/arm-sysroot + make -j"$(nproc)" && make install + cd .. + + # x86: use zig cc as the cross-compiler (no external toolchain download needed). + # zig bundles musl libc for all targets, so no -l:libzstd.a needed. + - name: Install cross-compiler and libpcap (x86/i686) + if: matrix.arch == 'x86' + run: | + rustup target add i686-unknown-linux-musl + apk add zig flex bison bash + + # zig cc wrapper: cc-rs passes --target=i686-* which zig doesn't accept; + # filter those args and use -target x86-linux-musl instead. + # shellcheck disable=SC2016 + printf '%s\n' \ + '#!/bin/bash' \ + 'args=()' \ + 'for arg in "$@"; do' \ + ' case "$arg" in --target=*) ;; -Wl,-melf_i386) ;; *) args+=("$arg") ;; esac' \ + 'done' \ + 'exec zig cc -target x86-linux-musl "${args[@]}"' \ + > /usr/local/bin/i686-linux-musl-gcc + chmod +x /usr/local/bin/i686-linux-musl-gcc + + # libpcap is pinned by version + sha256 (Dependabot does not track + # tarballs fetched in run steps, so bump the URL and the hash together). + curl -fsSL --retry 3 --retry-delay 5 \ + -o libpcap.tar.gz https://www.tcpdump.org/release/libpcap-1.10.5.tar.gz + echo "37ced90a19a302a7f32e458224a00c365c117905c2cd35ac544b6880a81488f0 libpcap.tar.gz" | sha256sum -c - + tar xzf libpcap.tar.gz + cd libpcap-1.10.5 + CC=i686-linux-musl-gcc \ + ./configure --host=i686-linux-musl \ + --disable-shared --enable-static \ + --disable-usb --disable-dbus --disable-bluetooth --disable-remote \ + --prefix=/x86-sysroot + make -j"$(nproc)" && make install + cd .. + + - name: Build static binary (aarch64) + if: matrix.arch == 'aarch64' + env: + CFLAGS: "-mno-outline-atomics" + CXXFLAGS: "-mno-outline-atomics" + RUSTFLAGS: "-C strip=symbols -C link-arg=-l:libzstd.a" + run: cargo build --release --no-default-features + + - name: Build static binary (x86_64) + if: matrix.arch == 'x86_64' + env: + RUSTFLAGS: "-C strip=symbols -C link-arg=-l:libzstd.a" + run: cargo build --release --no-default-features + + - name: Build static binary (armv7) + if: matrix.arch == 'armv7' + env: + # link-self-contained=no: let zig provide musl CRT (avoid duplicate _start + # when both Rust's self-contained crt1.o and zig's crt1.o are linked) + RUSTFLAGS: "-C strip=symbols -C link-self-contained=no" + CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER: arm-linux-musleabihf-gcc + PKG_CONFIG_PATH: /arm-sysroot/lib/pkgconfig + PKG_CONFIG_SYSROOT_DIR: /arm-sysroot + PKG_CONFIG_ALLOW_CROSS: "1" + run: cargo build --release --target armv7-unknown-linux-musleabihf --no-default-features + + - name: Build static binary (x86/i686) + if: matrix.arch == 'x86' + env: + RUSTFLAGS: "-C strip=symbols -C link-self-contained=no" + CARGO_TARGET_I686_UNKNOWN_LINUX_MUSL_LINKER: i686-linux-musl-gcc + PKG_CONFIG_PATH: /x86-sysroot/lib/pkgconfig + PKG_CONFIG_SYSROOT_DIR: /x86-sysroot + PKG_CONFIG_ALLOW_CROSS: "1" + run: cargo build --release --target i686-unknown-linux-musl --no-default-features + + - name: Verify static linking + run: | + BIN="${{ (matrix.arch == 'armv7' && 'target/armv7-unknown-linux-musleabihf/release/rustnet') || (matrix.arch == 'x86' && 'target/i686-unknown-linux-musl/release/rustnet') || 'target/release/rustnet' }}" + file "$BIN" + file "$BIN" | grep -q "static.* linked" || \ + (echo "ERROR: Binary is not statically linked" && exit 1) + + - name: Create release archive + run: | + VERSION="${{ github.ref_name }}" + ARTIFACT="${{ matrix.artifact }}" + + case "${{ matrix.arch }}" in + armv7) BIN="target/armv7-unknown-linux-musleabihf/release/rustnet" ;; + x86) BIN="target/i686-unknown-linux-musl/release/rustnet" ;; + *) BIN="target/release/rustnet" ;; + esac + + staging="rustnet-${VERSION}-${ARTIFACT}" + mkdir -p "$staging/assets" + cp "$BIN" "$staging/rustnet" + cp crates/rustnet-core/assets/services "$staging/assets/" 2>/dev/null || true + cp README.md "$staging/" + cp LICENSE "$staging/" 2>/dev/null || true + tar czf "$staging.tar.gz" "$staging" + + - name: Upload to release + run: | + VERSION="${{ github.ref_name }}" + ARCHIVE="rustnet-${VERSION}-${{ matrix.artifact }}.tar.gz" + gh release upload "$VERSION" "$ARCHIVE" --repo "${{ github.repository }}" --clobber + env: + GH_TOKEN: ${{ github.token }} + + # Publish the release (un-draft) after all assets are uploaded. + # This ensures downstream jobs (Homebrew, Chocolatey) can download assets. + publish-release: + name: publish-release + runs-on: ubuntu-latest + needs: + - create-release + - upload-arm-static + - upload-android-static + - package-installers + - package-macos + - package-windows + if: always() && needs.create-release.result == 'success' + steps: + - name: Publish release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh release edit ${{ github.ref_name }} --draft=false --repo ${{ github.repository }} + + # Gate job that waits for all publish steps to finish before firing + # downstream package-manager triggers. Without this gate, triggers that + # run as soon as publish-release completes race against publish-crates, + # publish-docker, update-copr, and release-ppa — and the downstream + # Homebrew/Chocolatey workflows bail because their "is release.yml still + # running?" safety check sees the parent run as in-progress. + all-published: + name: all-published + runs-on: ubuntu-latest + needs: + - publish-release + - publish-crates + - publish-docker + - update-copr + - release-ppa + - release-obs + steps: + - name: All publish steps complete + run: echo "Release pipeline fully published; safe to fire downstream triggers." + + trigger-bsd-build: + name: trigger-bsd-build + runs-on: ubuntu-latest + needs: all-published + steps: + - name: Trigger FreeBSD build + run: | + gh api repos/domcyrus/rustnet-bsd/dispatches \ + -f event_type=freebsd-build \ + -f "client_payload[tag]=${{ github.ref_name }}" \ + -f "client_payload[rustnet_ref]=${{ github.sha }}" + env: + GH_TOKEN: ${{ secrets.BSD_DISPATCH_TOKEN }} + + trigger-homebrew-update: + name: trigger-homebrew-update + runs-on: ubuntu-latest + needs: all-published + steps: + - name: Trigger Homebrew formula update + run: | + gh workflow run update-formula.yml \ + --repo domcyrus/homebrew-rustnet \ + -f version=${{ github.ref_name }} + env: + GH_TOKEN: ${{ secrets.HOMEBREW_PAT }} + + trigger-chocolatey-update: + name: trigger-chocolatey-update + runs-on: ubuntu-latest + needs: all-published + steps: + - name: Trigger Chocolatey package update + run: | + gh workflow run update-package.yml \ + --repo domcyrus/rustnet-chocolatey \ + -f version=${{ github.ref_name }} + env: + GH_TOKEN: ${{ secrets.CHOCOLATEY_PAT }} + + # Dispatching a same-repo workflow via gh workflow run requires + # actions: write on the token. The top-level permissions block grants + # contents/packages/attestations/id-token only, so explicitly opt in here. + trigger-aur-update: + name: trigger-aur-update + runs-on: ubuntu-latest + needs: all-published + permissions: + actions: write + contents: read + steps: + - name: Trigger AUR package update + run: | + gh workflow run aur-update.yml \ + --repo domcyrus/rustnet + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + package-installers: + name: package-installers + runs-on: ubuntu-22.04 + needs: create-release + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Download build artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + path: artifacts + + - name: Setup Linux dependencies + uses: ./.github/actions/setup-linux-deps + + - name: Install Rust and tools + uses: dtolnay/rust-toolchain@stable # unpinned: maintained branch by Rust team member + with: + targets: x86_64-unknown-linux-gnu,aarch64-unknown-linux-gnu,armv7-unknown-linux-gnueabihf + components: rustfmt + + - name: Install packaging tools + run: | + cargo install cargo-deb cargo-generate-rpm + + - name: Package Debian packages + run: | + mkdir -p installers + for arch in amd64 arm64 armhf; do + case "$arch" in + amd64) target="x86_64-unknown-linux-gnu" ;; + arm64) target="aarch64-unknown-linux-gnu" ;; + armhf) target="armv7-unknown-linux-gnueabihf" ;; + esac + + # Extract binary and assets from artifact + tar -xzf "artifacts/build-$target/rustnet-${{ github.ref_name }}-$target.tar.gz" + mkdir -p "target/$target/release" + cp "rustnet-${{ github.ref_name }}-$target/rustnet" "target/$target/release/" + + # Ensure services file exists for packaging + mkdir -p crates/rustnet-core/assets + if [ -f "rustnet-${{ github.ref_name }}-$target/assets/services" ]; then + cp "rustnet-${{ github.ref_name }}-$target/assets/services" crates/rustnet-core/assets/ + fi + + # Create deb package + cargo deb --no-build --no-strip --target "$target" + mv "target/$target/debian"/*.deb "installers/Rustnet_LinuxDEB_$arch.deb" + + # Clean up for next iteration + rm -rf "rustnet-${{ github.ref_name }}-$target" "target/$target" + done + + - name: Package RPM packages + run: | + for arch in x86_64 aarch64; do + target="$arch-unknown-linux-gnu" + + # Extract binary and assets from artifact + tar -xzf "artifacts/build-$target/rustnet-${{ github.ref_name }}-$target.tar.gz" + mkdir -p "target/$target/release" + cp "rustnet-${{ github.ref_name }}-$target/rustnet" "target/$target/release/" + + # Ensure services file exists for packaging + mkdir -p crates/rustnet-core/assets + if [ -f "rustnet-${{ github.ref_name }}-$target/assets/services" ]; then + cp "rustnet-${{ github.ref_name }}-$target/assets/services" crates/rustnet-core/assets/ + fi + + # Fix library linking if needed + patchelf --replace-needed libpcap.so.0.8 libpcap.so.1 "target/$target/release/rustnet" 2>/dev/null || true + + # Create rpm package + cargo generate-rpm --target "$target" + mv "target/$target/generate-rpm"/*.rpm "installers/Rustnet_LinuxRPM_$arch.rpm" + + # Clean up for next iteration + rm -rf "rustnet-${{ github.ref_name }}-$target" "target/$target" + done + + - name: Upload installer packages + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Upload all installer packages + for installer in installers/*; do + gh release upload ${{ github.ref_name }} "$installer" --clobber + done + + package-macos: + name: package-macos + runs-on: macos-latest + needs: create-release + strategy: + matrix: + include: + - arch: Intel + target: x86_64-apple-darwin + - arch: AppleSilicon + target: aarch64-apple-darwin + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Install packaging tools + run: | + cargo install toml-cli + cargo install apple-codesign + brew install create-dmg + + - name: Download build artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: build-${{ matrix.target }} + path: artifacts + + - name: Package for macOS + run: | + # Extract binary + tar -xzf "artifacts/rustnet-${{ github.ref_name }}-${{ matrix.target }}.tar.gz" + + # Get version and update plist + VERSION=$(toml get Cargo.toml package.version --raw) + sed -i'.bak' -e "s/0\.0\.0/${VERSION}/g" -e "s/fffffff/${GITHUB_SHA:0:7}/g" resources/packaging/macos/Info.plist + + # Create app bundle + mkdir -p "Rustnet.app/Contents/"{MacOS,Resources/assets} + cp resources/packaging/macos/Info.plist "Rustnet.app/Contents/" + cp resources/packaging/macos/graphics/rustnet.icns "Rustnet.app/Contents/Resources/" + cp "rustnet-${{ github.ref_name }}-${{ matrix.target }}/rustnet" "Rustnet.app/Contents/MacOS/" + cp resources/packaging/macos/wrapper.sh "Rustnet.app/Contents/MacOS/" + cp "rustnet-${{ github.ref_name }}-${{ matrix.target }}/assets/services" "Rustnet.app/Contents/Resources/assets/" + chmod +x "Rustnet.app/Contents/MacOS/"{rustnet,wrapper.sh} + + - name: Code sign and notarize app bundle + env: + APPLE_CERTIFICATE_P12: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12 }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }} + APPLE_API_ISSUER: ${{ secrets.APPLE_ID_ISSUER_ID }} + APPLE_API_KEY_ID: ${{ secrets.APPLE_ID_KEY_ID }} + APPLE_API_KEY: ${{ secrets.APPLE_ID_KEY }} + run: | + # Skip signing if secrets are not configured + if [ -z "$APPLE_CERTIFICATE_P12" ]; then + echo "⚠️ Code signing secrets not configured. DMG will be unsigned." + echo "⚠️ Users will need to use 'Open Anyway' in System Settings > Privacy & Security" + echo "SKIP_SIGNING=true" >> "$GITHUB_ENV" + exit 0 + fi + + # Decode certificate and save to file + echo "$APPLE_CERTIFICATE_P12" | base64 --decode > certificate.p12 + + # Create temporary JSON for API credentials + cat > api-key.json < oui-clean.txt + gzip -9 -c oui-clean.txt > crates/rustnet-core/assets/oui.gz + rm oui-raw.txt oui-clean.txt + + - name: Create pull request if changed + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + if git diff --quiet crates/rustnet-core/assets/oui.gz; then + echo "OUI database is already up to date." + exit 0 + fi + + BRANCH="update-oui-db" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git checkout -B "$BRANCH" + git add crates/rustnet-core/assets/oui.gz + git commit -m "Update OUI vendor database" + git push -f origin "$BRANCH" + + # Only create PR if one doesn't already exist for this branch + if ! gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number' | grep -q .; then + gh pr create \ + --title "Update OUI vendor database" \ + --body "Automated monthly update of the IEEE OUI (MAC vendor) database. + + Source: https://standards-oui.ieee.org/oui/oui.txt" \ + --label dependencies + fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c93f878 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# RustRover +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Added by cargo + +/target +.aider* +/logs +.venv diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..f6b3925 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,490 @@ +

English | 简体中文

+ +# Architecture + +This document describes the technical architecture and implementation details of RustNet. + +## Table of Contents + +- [Crate Structure](#crate-structure) +- [Multi-threaded Architecture](#multi-threaded-architecture) +- [Key Components](#key-components) +- [Platform-Specific Implementations](#platform-specific-implementations) +- [Performance Considerations](#performance-considerations) +- [Dependencies](#dependencies) +- [Security](#security) + +## Crate Structure + +RustNet is a Cargo workspace of four crates. The analysis logic, capture backend, and process attribution each live in their own reusable library crate; the binary composes them into the TUI application. + +| Crate | Type | Responsibility | +| --- | --- | --- | +| [`rustnet-core`](crates/rustnet-core) | library | Platform- and capture-independent analysis core: packet parsing, protocol/connection types, deep packet inspection, link-layer parsers, connection merging, DNS/GeoIP/OUI lookups, and a reusable `ConnectionTracker` (live table + RTT + QUIC coalescing + lifecycle) for headless tools. Operates only on byte slices and parsed structures -- no libpcap, raw sockets, or OS process tables. | +| [`rustnet-capture`](crates/rustnet-capture) | library | libpcap/Npcap packet-capture backend: device selection, BPF filters, macOS PKTAP, TUN/TAP, and a raw-frame `PacketReader`. | +| [`rustnet-host`](crates/rustnet-host) | library | Per-connection process attribution behind one `ProcessLookup` trait: eBPF/procfs on Linux, PKTAP/lsof on macOS, the IP Helper API on Windows, and `sockstat` on FreeBSD. Owns the eBPF build tooling and bundled `vmlinux.h`. | +| `rustnet-monitor` (binary `rustnet`) | binary | The user-facing application: CLI, TUI, app event loop, sandboxing (Landlock/Seatbelt), and interface statistics. Dogfoods `ConnectionTracker` as the single source of truth. | + +The package is named `rustnet-monitor` because the `rustnet` crate name is taken on crates.io; the installed binary is `rustnet`. + +### Dependency Graph + +```mermaid +flowchart TD + BIN[rustnet-monitor
bin: rustnet] + CAP[rustnet-capture] + HOST[rustnet-host] + CORE[rustnet-core] + + BIN --> CAP + BIN --> HOST + BIN --> CORE + CAP --> CORE + HOST --> CORE +``` + +The graph is acyclic: `rustnet-core` has no workspace dependencies, and both `rustnet-capture` and `rustnet-host` depend only on it. Keeping `rustnet-core` a leaf lets it be published and reused independently -- a headless front-end (e.g. a Prometheus exporter) can pair `rustnet-capture` + `rustnet-core` without the TUI. + +### Re-export Facade + +To keep the split internal to the binary, `src/network/mod.rs` re-exports `rustnet_core::network::*` and `rustnet_capture` (as `capture`), so existing `crate::network::*` paths, integration tests, and benches compile unchanged. The `src/network/platform` module still hosts the OS sandboxing (Landlock/Seatbelt) and interface-stats collectors, and wires in `rustnet-host`'s process lookup. + +## Multi-threaded Architecture + +RustNet uses a multi-threaded architecture for efficient packet processing: + +```mermaid +flowchart LR + PC[Packet Capture
libpcap] + CH([Crossbeam Channel]) + PP[Packet Processors
Thread 0..N] + PE[Process Enrichment
Platform API] + DM[(DashMap)] + SP[Snapshot Provider] + UI[/RwLock<Vec<Connection>>
for UI/] + CT[Cleanup Thread] + + PC -- packets --> CH --> PP --> DM + PE --> DM + DM --> SP --> UI + DM --> CT +``` + +## Key Components + +### 1. Packet Capture Thread + +Uses libpcap to capture raw packets from the network interface. This thread runs independently and feeds packets into a Crossbeam channel for processing. + +**Responsibilities:** +- Open network interface for packet capture (non-promiscuous, read-only mode) +- Apply BPF filters if needed +- Capture raw packets +- Stream packets to PCAP file if `--pcap-export` is enabled (direct disk write, no memory buffering) +- Feed parsed packets to the annotated PCAPNG writer if `--pcapng-export` is enabled (bounded best-effort queue) +- Send packets to processing queue + +### 2. Packet Processors + +Multiple worker threads (up to 4 by default, based on CPU cores) that parse packets and perform Deep Packet Inspection (DPI) analysis. + +**Responsibilities:** +- Parse Ethernet, IP, TCP, UDP, ICMP, ARP headers +- Extract connection 5-tuple (protocol, src IP, src port, dst IP, dst port) +- Perform DPI to detect application protocols: + - HTTP with host information + - HTTPS/TLS with SNI (Server Name Indication) + - DNS queries and responses + - SSH connections with version detection + - FTP control channel with commands, response codes, username, server software, and system type + - QUIC protocol with CONNECTION_CLOSE frame detection + - MQTT with packet types, version, and client identifier + - BitTorrent handshakes and DHT messages + - STUN for WebRTC and NAT traversal + - NTP with version, mode, and stratum + - mDNS and LLMNR for local name resolution + - DHCP with message types and hostnames + - SNMP (v1, v2c, v3) with PDU types + - SSDP for UPnP device discovery + - NetBIOS Name Service and Datagram Service +- Track connection states and lifecycle +- Update connection metadata in DashMap +- Calculate bandwidth metrics + +### 3. Process Enrichment + +Platform-specific APIs to associate network connections with running processes. This component runs periodically to enrich connection data with process information. + +**Responsibilities:** +- Map socket inodes to process IDs +- Resolve process names and command lines +- Update connection records with process information +- Handle permission-related fallbacks + +See [Platform-Specific Implementations](#platform-specific-implementations) for details on each platform. + +### 4. Snapshot Provider + +Creates consistent snapshots of connection data for the UI at regular intervals (default: 1 second). This ensures the UI has a stable view of connections without race conditions. + +**Responsibilities:** +- Read from DashMap at configured intervals +- Apply filtering based on user criteria (localhost, etc.) +- Sort connections based on user-selected column +- Create immutable snapshot for UI rendering +- Provide RwLock-protected Vec for UI thread + +### 5. Cleanup Thread + +Removes inactive connections using smart, protocol-aware timeouts. This prevents memory leaks and keeps the connection list relevant. When `--pcap-export` is enabled, also streams connection metadata (PID, process name, timestamps) to a JSONL sidecar file as connections close. + +**Timeout Strategy:** + +#### TCP Connections +- **HTTP/HTTPS** (detected via DPI): **10 minutes** - supports HTTP keep-alive +- **SSH** (detected via DPI): **30 minutes** - accommodates long interactive sessions +- **Active established** (< 1 min idle): **10 minutes** +- **Idle established** (> 1 min idle): **5 minutes** +- **TIME_WAIT**: 30 seconds - standard TCP timeout +- **CLOSED**: 5 seconds - rapid cleanup +- **SYN_SENT, FIN_WAIT, etc.**: 30-60 seconds + +#### UDP Connections +- **HTTP/3 (QUIC with HTTP)**: **10 minutes** - connection reuse +- **HTTPS/3 (QUIC with HTTPS)**: **10 minutes** - connection reuse +- **SSH over UDP**: **30 minutes** - long-lived sessions +- **DNS**: **30 seconds** - short-lived queries +- **Regular UDP**: **60 seconds** - standard timeout + +#### QUIC Connections (Detected State) +- **Connected**: 3 minutes default, or the peer's `max_idle_timeout` transport parameter when present +- **With CONNECTION_CLOSE frame**: 1-10 seconds (based on close type) +- **Initial/Handshaking**: 60 seconds - allow connection establishment +- **Draining**: 10 seconds - RFC 9000 draining period +- **Closed**: 1 second - immediate cleanup + +**Visual Staleness Indicators:** + +Connections change color based on proximity to timeout: +- **White** (default): < 75% of timeout +- **Yellow**: 75-90% of timeout (warning) +- **Red**: > 90% of timeout (critical) + +### 6. Rate Refresh Thread + +Updates bandwidth calculations every second with gentle decay. This provides smooth bandwidth visualization without abrupt changes. + +**Responsibilities:** +- Calculate bytes/second for download and upload +- Apply exponential decay to older measurements +- Update visual bandwidth indicators +- Maintain rolling window of packet rates + +### 7. DashMap + +Concurrent hashmap (`DashMap`) for storing connection state. This lock-free data structure enables efficient concurrent access from multiple threads. + +**Key Features:** +- Fine-grained locking (per-shard) +- No global lock contention +- Safe concurrent reads and writes +- High performance under concurrent load + +## Platform-Specific Implementations + +### Process Lookup + +RustNet uses platform-specific APIs to associate network connections with processes: + +#### Linux + +**Standard Mode (procfs):** +- Parses `/proc/net/tcp` and `/proc/net/udp` to get socket inodes +- Iterates through `/proc//fd/` to find socket file descriptors +- Maps inodes to process IDs and resolves process names from `/proc//cmdline` + +**eBPF Mode (Default on Linux):** +- Uses kernel eBPF programs attached to socket syscalls +- Captures socket creation events with process context +- Provides lower overhead than procfs scanning +- **Limitations:** + - Process names limited to 16 characters (kernel `comm` field) + - May show thread names instead of full executable names + - Multi-threaded applications show internal thread names +- **Capability requirements:** + - Modern Linux (5.8+): `CAP_NET_RAW` (packet capture), `CAP_BPF`, `CAP_PERFMON` (eBPF) + - Legacy Linux (pre-5.8): eBPF requires broad `CAP_SYS_ADMIN`; RustNet packages do not grant it automatically and fall back to procfs instead + - 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 +- TUI Statistics panel shows active detection method + +#### macOS + +**PKTAP Mode (with sudo):** +- Uses PKTAP (Packet Tap) kernel interface +- Extracts process information directly from packet metadata +- Requires root privileges (privileged kernel interface) +- Faster and more accurate than lsof + +**lsof Mode (without sudo or fallback):** +- Uses `lsof -i -n -P` to list network connections +- Parses output to associate sockets with processes +- Higher CPU overhead but works without root +- Used automatically when PKTAP is unavailable + +**Detection:** +- TUI Statistics panel shows "pktap" or "lsof" based on active method +- Automatically selects best available method + +#### Windows + +**IP Helper API:** +- Uses `GetExtendedTcpTable` and `GetExtendedUdpTable` from Windows IP Helper API +- Retrieves connection tables with process IDs +- Supports both IPv4 and IPv6 connections +- Resolves process names using `OpenProcess` and `QueryFullProcessImageNameW` + +**Requirements:** +- May require Administrator privileges depending on system configuration +- Requires Npcap or WinPcap for packet capture + +### Network Interfaces + +The tool automatically detects and lists available network interfaces using platform-specific methods: + +- **Linux**: Uses `netlink` or falls back to `/sys/class/net/` +- **macOS**: Uses `getifaddrs()` system call +- **Windows**: Uses `GetAdaptersInfo()` from IP Helper API +- **All platforms**: Falls back to pcap's `pcap_findalldevs()` when native methods fail + +## Performance Considerations + +### Multi-threaded Processing + +Packet processing is distributed across multiple threads (up to 4 by default, based on CPU cores). This enables: +- Parallel packet parsing and DPI analysis +- Better utilization of multi-core systems +- Reduced latency for high packet rates + +### Concurrent Data Structures + +**DashMap** provides lock-free concurrent access with: +- Per-shard locking (16 shards by default) +- No global lock contention +- Read-heavy workload optimization +- Safe concurrent modifications + +### Batch Processing + +Packets are processed in batches to improve cache efficiency: +- Multiple packets processed before context switching +- Reduced system call overhead +- Better CPU cache utilization + +### Selective DPI + +Deep packet inspection can be disabled with `--no-dpi` for lower overhead: +- Reduces CPU usage by 20-40% on high-traffic networks +- Still tracks basic connection information +- Useful for performance-constrained environments + +### Configurable Intervals + +Adjust refresh rates based on your needs: +- **UI refresh**: Default 1000ms (adjustable with `--refresh-interval`) +- **Process enrichment**: Every 2 seconds +- **Cleanup check**: Every 5 seconds +- **Rate calculation**: Every 1 second + +### Memory Management + +**Connection cleanup** prevents unbounded memory growth: +- Protocol-aware timeouts remove stale connections +- Visual staleness warnings before removal +- Configurable timeout thresholds + +**Snapshot isolation** prevents UI blocking: +- UI reads from immutable snapshots +- Background threads update DashMap concurrently +- No lock contention between UI and packet processing + +## Dependencies + +RustNet is built with the following key dependencies: + +### Core Dependencies + +- **ratatui** - Terminal user interface framework with full widget support +- **crossterm** - Cross-platform terminal manipulation +- **pcap** - Packet capture library bindings for libpcap/Npcap +- **pnet_datalink** - Network interface enumeration and low-level networking + +### Concurrency & Threading + +- **dashmap** - Concurrent hashmap with fine-grained locking +- **crossbeam** - Multi-threading utilities and lock-free channels + +### Networking & Protocols + +- **dns-lookup** - DNS resolution capabilities +- **maxminddb** - GeoIP database lookups (GeoLite2) + +### Serialization + +- **serde** / **serde_json** - JSON serialization for event logging and PCAP sidecar + +### Command-line & Logging + +- **clap** - Command-line argument parsing with derive features +- **simplelog** - Flexible logging framework +- **log** - Logging facade +- **anyhow** - Error handling and context + +### Platform-Specific + +- **procfs** (Linux) - Process information from /proc filesystem (runtime fallback) +- **libbpf-rs** (Linux) - eBPF program loading and management +- **landlock** (Linux) - Filesystem and network sandboxing +- **caps** (Linux) - Linux capability management +- **windows** (Windows) - Windows API bindings for IP Helper API + +### Utilities + +- **arboard** - Clipboard access for copying addresses +- **chrono** - Date and time handling +- **ring** - Cryptographic operations (for TLS/SNI parsing) +- **aes** - AES encryption support (for protocol detection) +- **flate2** - Gzip decompression (for compressed embedded data) +- **libc** - Low-level C bindings + +## Embedded Data Files + +RustNet embeds static lookup databases at compile time, avoiding runtime file dependencies. Both follow the same pattern: embed the file, parse into a `HashMap` at startup, expose a `lookup()` method. + +### Service Lookup (`crates/rustnet-core/assets/services`) + +Port-to-service-name mappings (e.g., 80/tcp -> http). Loaded by `ServiceLookup` in `crates/rustnet-core/src/network/services.rs` using `include_str!`. + +### OUI Vendor Database (`crates/rustnet-core/assets/oui.gz`) + +IEEE MA-L OUI prefix-to-vendor mappings for MAC address vendor resolution (e.g., `00:1B:63` -> Apple). Gzip-compressed to reduce binary size (~400KB compressed vs ~1.2MB raw). Decompressed at startup by `OuiLookup` in `crates/rustnet-core/src/network/oui.rs` using `include_bytes!` + `flate2`. Currently used for ARP connections only. + +A GitHub Action (`.github/workflows/update-oui.yml`) updates this file monthly from the [IEEE public database](https://standards-oui.ieee.org/oui/oui.txt) and opens a PR if there are changes. + +## Security + +For security documentation including Landlock sandboxing, privilege requirements, and threat model, see [SECURITY.md](SECURITY.md). + +## Comparison with Similar Tools + +Network monitoring tools exist on a spectrum from simple connection listing to full packet forensics: + +``` +Simple ←─────────────────────────────────────────────────────→ Complex + +netstat iftop bandwhich RustNet tcpdump Wireshark + │ │ │ │ │ │ + └── Socket ┴── Bandwidth ──────────┴── Live DPI ┴── Capture ──┴── Forensics + state monitoring + Process & CLI & Deep + tracking Analysis +``` + +**RustNet's position**: Real-time connection monitoring with DPI and process identification - more capable than bandwidth monitors, more focused than forensic capture tools. + +### Feature Comparison + +| Feature | RustNet | bandwhich | sniffnet | iftop | netstat | ss | tcpdump/wireshark | +|---------|---------|-----------|----------|-------|---------|-----|-------------------| +| **Language** | Rust | Rust | Rust | C | C | C | C | +| **Interface** | TUI | TUI | GUI | TUI | CLI | CLI | CLI/GUI | +| **Real-time monitoring** | Yes | Yes | Yes | Yes | Snapshot | Snapshot | Yes | +| **Process identification** | Yes | Yes | No | No | Yes | Yes | No | +| **Deep Packet Inspection** | Yes | No | No | No | No | No | Yes | +| **SNI/Host extraction** | Yes | No | No | No | No | No | Yes | +| **Protocol state tracking** | Yes | No | Partial | No | Yes | Yes | Yes | +| **Bandwidth per connection** | Yes | Yes | Yes | Yes | No | No | No | +| **Connection filtering** | Yes | No | Yes | Yes | No | Yes | Yes (BPF) | +| **DNS reverse lookup** | Yes | Yes | Yes | Yes | No | No | Yes | +| **GeoIP lookup** | Yes | No | Yes | No | No | No | Yes | +| **Notifications** | No | No | Yes | No | No | No | No | +| **i18n (translations)** | No | No | Yes | No | No | No | No | +| **Cross-platform** | Linux, macOS, Windows, FreeBSD | Linux, macOS | Linux, macOS, Windows | Linux, macOS, BSD | All | Linux | All | +| **eBPF support** | Yes (Linux) | No | No | No | No | Yes | No | +| **Landlock sandboxing** | Yes (Linux) | No | No | No | No | No | No | +| **JSON event logging** | Yes | No | No | No | No | No | Yes | +| **PCAP export** | Yes (+ process sidecar / annotated PCAPNG) | No | Yes | No | No | No | Yes | +| **Packet capture** | libpcap | Raw sockets | libpcap | libpcap | Kernel | Kernel | libpcap | + +### Tool Focus Areas + +- **RustNet**: Real-time connection monitoring with DPI, protocol state tracking, and process identification in a TUI +- **bandwhich**: Bandwidth utilization by process/connection with minimal overhead +- **sniffnet**: Network traffic analysis with a graphical interface and notifications +- **iftop**: Interface bandwidth monitoring with per-host traffic display +- **netstat/ss**: System socket and connection state inspection (ss is the modern replacement for netstat on Linux) +- **tcpdump/wireshark/tshark**: Full packet capture and protocol analysis for deep debugging + +### Choosing the Right Tool + +| Your Goal | Best Tool | +|-----------|-----------| +| See which process is making a connection | RustNet | +| Decode packets byte-by-byte | Wireshark | +| Monitor connection states (SYN_SENT, ESTABLISHED, etc.) | RustNet | +| Extract files or credentials from traffic | Wireshark | +| Attribute network activity to specific applications | RustNet | +| Deep protocol dissection (3000+ protocols) | Wireshark | +| Quick terminal-based network overview | RustNet | +| Save captures with process attribution | RustNet (`--pcap-export` or `--pcapng-export`) | +| Save captures for deep analysis | Wireshark/tcpdump | + +### RustNet and Wireshark: Different Strengths + +The key difference: **RustNet knows which process owns each connection. Wireshark cannot.** + +Wireshark operates at the packet capture layer (libpcap) - it sees raw network traffic but has no visibility into which application created it. RustNet combines packet capture with OS-level socket introspection (via eBPF on Linux, /proc, or platform APIs) to attribute every connection to its owning process. + +| Capability | RustNet | Wireshark | +|------------|---------|-----------| +| Process identification | Yes (eBPF, procfs, platform APIs) | No | +| Connection state tracking | Native (TCP FSM, QUIC states) | Via dissectors | +| Protocol dissectors | ~15 common protocols | 3000+ protocols | +| Packet-level inspection | Metadata only | Full payload | +| Interface | TUI (terminal) | GUI | +| Capture to file | Yes (`--pcap-export`) | Yes (native) | + +Both tools can run in real-time. Choose based on what you need to see: +- **"What is making this connection?"** → RustNet +- **"What's inside this packet?"** → Wireshark + +### Bridging the Gap: PCAP Export with Process Attribution + +RustNet can now export packet captures while preserving process attribution - something neither tcpdump nor Wireshark can do alone: + +```bash +# Capture packets with RustNet (includes process tracking) +sudo rustnet -i eth0 --pcap-export capture.pcap + +# Creates: +# capture.pcap - Standard PCAP file +# capture.pcap.connections.jsonl - Process attribution (PID, name, timestamps) + +# Or write an annotated PCAPNG directly during live capture +sudo rustnet -i eth0 --pcapng-export annotated.pcapng + +# Or enrich a classic PCAP after capture +python scripts/pcap_enrich.py capture.pcap -o enriched.pcapng + +# Open in Wireshark - packets now show process info in comments +wireshark annotated.pcapng +``` + +This workflow gives you the best of both worlds: +- **RustNet's process attribution**: Know which application generated each packet +- **Wireshark's deep analysis**: Full protocol dissection with 3000+ analyzers + +Native PCAPNG export embeds live best-effort packet comments directly. The enrichment script remains useful when cleanup-time sidecar metadata completeness is more important than producing a single file during capture. + +See [USAGE.md - PCAP Export](USAGE.md#pcap-export) for detailed documentation. diff --git a/ARCHITECTURE.zh-CN.md b/ARCHITECTURE.zh-CN.md new file mode 100644 index 0000000..ca4685e --- /dev/null +++ b/ARCHITECTURE.zh-CN.md @@ -0,0 +1,490 @@ +

English | 简体中文

+ +# 架构 + +本文档描述 RustNet 的技术架构和实现细节。 + +## 目录 + +- [Crate 结构](#crate-structure) +- [多线程架构](#multi-threaded-architecture) +- [核心组件](#key-components) +- [平台特定实现](#platform-specific-implementations) +- [性能考量](#performance-considerations) +- [依赖项](#dependencies) +- [安全](#security) + +## Crate 结构 + +RustNet 是一个由四个 crate 组成的 Cargo 工作区。分析逻辑、捕获后端和进程归属各自位于独立的可复用库 crate 中;二进制 crate 将它们组合成 TUI 应用。 + +| Crate | 类型 | 职责 | +| --- | --- | --- | +| [`rustnet-core`](crates/rustnet-core) | 库 | 与平台和捕获无关的分析核心:数据包解析、协议/连接类型、深度包检测、链路层解析器、连接合并、DNS/GeoIP/OUI 查找,以及供无界面工具使用的可复用 `ConnectionTracker`(实时连接表 + RTT + QUIC 合并 + 生命周期)。仅操作字节切片和已解析的结构 —— 不依赖 libpcap、原始套接字或操作系统进程表。 | +| [`rustnet-capture`](crates/rustnet-capture) | 库 | 基于 libpcap/Npcap 的数据包捕获后端:设备选择、BPF 过滤器、macOS PKTAP、TUN/TAP,以及原始帧 `PacketReader`。 | +| [`rustnet-host`](crates/rustnet-host) | 库 | 单一 `ProcessLookup` trait 背后的按连接进程归属:Linux 上的 eBPF/procfs、macOS 上的 PKTAP/lsof、Windows 上的 IP Helper API,以及 FreeBSD 上的 `sockstat`。负责 eBPF 构建工具链及内置的 `vmlinux.h`。 | +| `rustnet-monitor`(二进制 `rustnet`) | 二进制 | 面向用户的应用:CLI、TUI、应用事件循环、沙箱(Landlock/Seatbelt)和接口统计。以 `ConnectionTracker` 作为唯一数据来源(dogfooding)。 | + +包名为 `rustnet-monitor`,因为 `rustnet` 这个 crate 名称在 crates.io 上已被占用;安装后的二进制文件名为 `rustnet`。 + +### 依赖关系图 + +```mermaid +flowchart TD + BIN[rustnet-monitor
二进制: rustnet] + CAP[rustnet-capture] + HOST[rustnet-host] + CORE[rustnet-core] + + BIN --> CAP + BIN --> HOST + BIN --> CORE + CAP --> CORE + HOST --> CORE +``` + +依赖关系是无环的:`rustnet-core` 没有任何工作区内依赖,`rustnet-capture` 和 `rustnet-host` 都仅依赖它。让 `rustnet-core` 保持为叶子节点,使其可以独立发布和复用 —— 无界面的前端(例如 Prometheus 导出器)可以仅组合 `rustnet-capture` + `rustnet-core` 而无需 TUI。 + +### 重导出门面 + +为了将拆分对二进制内部保持透明,`src/network/mod.rs` 重导出 `rustnet_core::network::*` 和 `rustnet_capture`(作为 `capture`),因此现有的 `crate::network::*` 路径、集成测试和基准测试无需改动即可编译。`src/network/platform` 模块仍承载操作系统沙箱(Landlock/Seatbelt)和接口统计采集器,并接入 `rustnet-host` 的进程查找。 + +## 多线程架构 + +RustNet 采用多线程架构以实现高效的数据包处理: + +```mermaid +flowchart LR + PC[数据包捕获
libpcap] + CH([Crossbeam Channel]) + PP[数据包处理器
线程 0..N] + PE[进程信息补全
平台 API] + DM[(DashMap)] + SP[快照提供器] + UI[/RwLock<Vec<Connection>>
供 UI 使用/] + CT[清理线程] + + PC -- 数据包 --> CH --> PP --> DM + PE --> DM + DM --> SP --> UI + DM --> CT +``` + +## 核心组件 + +### 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 + - BitTorrent 握手和 DHT 消息 + - 用于 WebRTC 和 NAT 穿越的 STUN + - 带版本、模式和 stratum 的 NTP + - 用于本地名称解析的 mDNS 和 LLMNR + - 带消息类型和主机名的 DHCP + - 带 PDU 类型的 SNMP(v1、v2c、v3) + - 用于 UPnP 设备发现的 SSDP + - NetBIOS 名称服务和数据报服务 +- 追踪连接状态和生命周期 +- 在 DashMap 中更新连接元数据 +- 计算带宽指标 + +### 3. 进程信息补全 + +使用平台特定的 API 将网络连接与运行中的进程关联。该组件定期运行,为连接数据补充进程信息。 + +**职责:** +- 将 socket inode 映射到进程 ID +- 解析进程名和命令行 +- 用进程信息更新连接记录 +- 处理与权限相关的回退 + +各平台的详情见[平台特定实现](#platform-specific-implementations)。 + +### 4. 快照提供器 + +按固定间隔(默认 1 秒)创建连接数据的一致快照供 UI 使用。这确保 UI 拥有稳定的连接视图,避免竞争条件。 + +**职责:** +- 按配置间隔从 DashMap 读取 +- 根据用户条件应用过滤(localhost 等) +- 按用户选择的列排序连接 +- 为 UI 渲染创建不可变快照 +- 为 UI 线程提供 RwLock 保护的 Vec + +### 5. 清理线程 + +使用智能的、协议感知的超时机制移除不活跃的连接。这防止内存泄漏并保持连接列表的相关性。当启用 `--pcap-export` 时,连接关闭时还会将连接元数据(PID、进程名、时间戳)流式写入 JSONL sidecar 文件。 + +**超时策略:** + +#### TCP 连接 +- **HTTP/HTTPS**(通过 DPI 检测):**10 分钟** —— 支持 HTTP keep-alive +- **SSH**(通过 DPI 检测):**30 分钟** —— 适应长交互会话 +- **活跃已建立**(< 1 分钟空闲):**10 分钟** +- **空闲已建立**(> 1 分钟空闲):**5 分钟** +- **TIME_WAIT**:30 秒 —— 标准 TCP 超时 +- **CLOSED**:5 秒 —— 快速清理 +- **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 帧**:1-10 秒(基于关闭类型) +- **Initial/Handshaking**:60 秒 —— 允许连接建立 +- **Draining**:10 秒 —— RFC 9000 draining 周期 +- **Closed**:1 秒 —— 立即清理 + +**视觉陈旧度指示器:** + +连接根据距离超时的远近改变颜色: +- **白色**(默认):< 75% 的超时时间 +- **黄色**:75-90% 的超时时间(警告) +- **红色**:> 90% 的超时时间(严重) + +### 6. 速率刷新线程 + +每秒更新带宽计算,采用温和衰减。这提供平滑的带宽可视化,避免突变。 + +**职责:** +- 计算下载和上传的字节/秒 +- 对旧测量值应用指数衰减 +- 更新可视化带宽指示器 +- 维护包速率的滚动窗口 + +### 7. DashMap + +并发哈希表(`DashMap`)用于存储连接状态。这种无锁数据结构支持来自多个线程的高效并发访问。 + +**关键特性:** +- 细粒度锁定(per-shard) +- 无全局锁竞争 +- 安全的并发读写 +- 高并发负载下的高性能 + +## 平台特定实现 + +### 进程查找 + +RustNet 使用平台特定的 API 将网络连接与进程关联: + +#### Linux + +**标准模式(procfs):** +- 解析 `/proc/net/tcp` 和 `/proc/net/udp` 获取 socket inode +- 遍历 `/proc//fd/` 查找 socket 文件描述符 +- 将 inode 映射到进程 ID,并从 `/proc//cmdline` 解析进程名 + +**eBPF 模式(Linux 默认):** +- 使用附加到 socket 系统调用的内核 eBPF 程序 +- 捕获带进程上下文的 socket 创建事件 +- 比 procfs 扫描开销更低 +- **局限性:** + - 进程名限制为 16 个字符(内核 `comm` 字段) + - 可能显示线程名而非完整可执行文件名 + - 多线程应用显示内部线程名 +- **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(使用只读、非混杂包捕获) + +**回退行为:** +- 如果 eBPF 加载失败(权限、内核兼容性),自动回退到 procfs 模式 +- TUI 统计面板显示当前使用的检测方法 + +#### macOS + +**PKTAP 模式(使用 sudo 时):** +- 使用 PKTAP(Packet Tap)内核接口 +- 直接从包元数据提取进程信息 +- 需要 root 特权(特权内核接口) +- 比 lsof 更快更准确 + +**lsof 模式(无 sudo 或回退时):** +- 使用 `lsof -i -n -P` 列出网络连接 +- 解析输出以将 socket 与进程关联 +- CPU 开销更高,但无需 root +- 当 PKTAP 不可用时自动使用 + +**检测:** +- TUI 统计面板根据当前方法显示 "pktap" 或 "lsof" +- 自动选择最佳可用方法 + +#### Windows + +**IP Helper API:** +- 使用 Windows IP Helper API 的 `GetExtendedTcpTable` 和 `GetExtendedUdpTable` +- 检索带进程 ID 的连接表 +- 支持 IPv4 和 IPv6 连接 +- 使用 `OpenProcess` 和 `QueryFullProcessImageNameW` 解析进程名 + +**需求:** +- 根据系统配置可能需要 Administrator 特权 +- 包捕获需要 Npcap 或 WinPcap + +### 网络接口 + +该工具使用平台特定的方法自动检测和列出可用网络接口: + +- **Linux**:使用 `netlink` 或回退到 `/sys/class/net/` +- **macOS**:使用 `getifaddrs()` 系统调用 +- **Windows**:使用 IP Helper API 的 `GetAdaptersInfo()` +- **所有平台**:当原生方法失败时回退到 pcap 的 `pcap_findalldevs()` + +## 性能考量 + +### 多线程处理 + +数据包处理分布在多个线程上(默认最多 4 个,基于 CPU 核心数)。这实现了: +- 并行数据包解析和 DPI 分析 +- 更好地利用多核系统 +- 降低高包速率下的延迟 + +### 并发数据结构 + +**DashMap** 提供无锁并发访问,具备: +- Per-shard 锁定(默认 16 个 shard) +- 无全局锁竞争 +- 读密集型工作负载优化 +- 安全的并发修改 + +### 批处理 + +数据包以批次处理以提高缓存效率: +- 上下文切换前处理多个数据包 +- 减少系统调用开销 +- 更好的 CPU 缓存利用率 + +### 选择性 DPI + +可以使用 `--no-dpi` 禁用深度包检测以降低开销: +- 在高流量网络中降低 20-40% 的 CPU 使用率 +- 仍然追踪基本连接信息 +- 适用于性能受限的环境 + +### 可配置间隔 + +根据需求调整刷新率: +- **UI 刷新**:默认 1000ms(可通过 `--refresh-interval` 调整) +- **进程补全**:每 2 秒 +- **清理检查**:每 5 秒 +- **速率计算**:每 1 秒 + +### 内存管理 + +**连接清理**防止内存无界增长: +- 协议感知的超时移除陈旧连接 +- 移除前的视觉陈旧度警告 +- 可配置的超时阈值 + +**快照隔离**防止 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)— 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` 在启动时解压。 + +GitHub Action(`.github/workflows/update-oui.yml`)每月从 [IEEE 公开数据库](https://standards-oui.ieee.org/oui/oui.txt) 更新此文件,如有变更则自动打开 PR。 + +## 安全 + +关于 Landlock 沙箱、权限需求和威胁模型的安全文档,参见 [SECURITY.zh-CN.md](SECURITY.zh-CN.md)。 + +## 与同类工具的对比 + +网络监控工具存在于从简单连接列表到完整数据包取证的光谱上: + +``` +简单 ←─────────────────────────────────────────────────────→ 复杂 + +netstat iftop bandwhich RustNet tcpdump Wireshark + │ │ │ │ │ │ + └── Socket ┴── Bandwidth ──────────┴── Live DPI ┴── Capture ──┴── Forensics + state monitoring + Process & CLI & Deep + tracking Analysis +``` + +**RustNet 的定位**:实时连接监控,带 DPI 和进程识别——比带宽监控器功能更强,比取证捕获工具更聚焦。 + +### 功能对比 + +| 功能 | RustNet | bandwhich | sniffnet | iftop | netstat | ss | tcpdump/wireshark | +|---------|---------|-----------|----------|-------|---------|-----|-------------------| +| **语言** | Rust | Rust | Rust | C | C | C | C | +| **界面** | TUI | TUI | GUI | TUI | CLI | CLI | CLI/GUI | +| **实时监控** | 是 | 是 | 是 | 是 | 快照 | 快照 | 是 | +| **进程识别** | 是 | 是 | 否 | 否 | 是 | 是 | 否 | +| **深度包检测** | 是 | 否 | 否 | 否 | 否 | 否 | 是 | +| **SNI/主机提取** | 是 | 否 | 否 | 否 | 否 | 否 | 是 | +| **协议状态追踪** | 是 | 否 | 部分 | 否 | 是 | 是 | 是 | +| **逐连接带宽** | 是 | 是 | 是 | 是 | 否 | 否 | 否 | +| **连接过滤** | 是 | 否 | 是 | 是 | 否 | 是 | 是(BPF) | +| **DNS 反向解析** | 是 | 是 | 是 | 是 | 否 | 否 | 是 | +| **GeoIP 查询** | 是 | 否 | 是 | 否 | 否 | 否 | 是 | +| **通知** | 否 | 否 | 是 | 否 | 否 | 否 | 否 | +| **i18n(翻译)** | 否 | 否 | 是 | 否 | 否 | 否 | 否 | +| **跨平台** | Linux、macOS、Windows、FreeBSD | Linux、macOS | Linux、macOS、Windows | Linux、macOS、BSD | 全部 | Linux | 全部 | +| **eBPF 支持** | 是(Linux) | 否 | 否 | 否 | 否 | 是 | 否 | +| **Landlock 沙箱** | 是(Linux) | 否 | 否 | 否 | 否 | 否 | 否 | +| **JSON 事件日志** | 是 | 否 | 否 | 否 | 否 | 否 | 是 | +| **PCAP 导出** | 是(PCAP + sidecar,或带注释 PCAPNG) | 否 | 是 | 否 | 否 | 否 | 是 | +| **数据包捕获** | libpcap | Raw sockets | libpcap | libpcap | Kernel | Kernel | libpcap | + +### 工具聚焦领域 + +- **RustNet**:TUI 中的实时连接监控,带 DPI、协议状态追踪和进程识别 +- **bandwhich**:按进程/连接的带宽利用率,开销最小 +- **sniffnet**:带图形界面和通知的网络流量分析 +- **iftop**:带逐主机流量显示的接口带宽监控 +- **netstat/ss**:系统 socket 和连接状态检查(ss 是 Linux 上 netstat 的现代替代品) +- **tcpdump/wireshark/tshark**:用于深度调试的完整数据包捕获和协议分析 + +### 如何选择合适的工具 + +| 你的目标 | 最佳工具 | +|-----------|-----------| +| 查看哪个进程正在建立连接 | RustNet | +| 逐字节解码数据包 | Wireshark | +| 监控连接状态(SYN_SENT、ESTABLISHED 等) | RustNet | +| 从流量中提取文件或凭据 | Wireshark | +| 将网络活动归因于特定应用 | RustNet | +| 深度协议解析(3000+ 协议) | Wireshark | +| 快速终端网络概览 | RustNet | +| 保存带进程归因的捕获 | RustNet(`--pcap-export` 或 `--pcapng-export`) | +| 保存捕获用于深度分析 | Wireshark/tcpdump | + +### RustNet 与 Wireshark:不同的强项 + +关键区别:**RustNet 知道每个连接属于哪个进程。Wireshark 不知道。** + +Wireshark 在数据包捕获层(libpcap)运行——它看到原始网络流量,但不知道哪个应用创建了它。RustNet 将数据包捕获与 OS 级 socket 内省(通过 Linux eBPF、/proc 或平台 API)相结合,将每个连接归因于其所属进程。 + +| 能力 | RustNet | Wireshark | +|------------|---------|-----------| +| 进程识别 | 是(eBPF、procfs、平台 API) | 否 | +| 连接状态追踪 | 原生(TCP FSM、QUIC 状态) | 通过解析器 | +| 协议解析器 | ~15 个常见协议 | 3000+ 协议 | +| 数据包级检查 | 仅元数据 | 完整 payload | +| 界面 | TUI(终端) | GUI | +| 捕获到文件 | 是(`--pcap-export`、`--pcapng-export`) | 是(原生) | + +两种工具都可以实时运行。根据你想看什么来选择: +- **"是什么在发起这个连接?"** → RustNet +- **"这个数据包里有什么?"** → Wireshark + +### 弥合差距:带进程归因的 PCAP 导出 + +RustNet 现在可以在保留进程归因的同时导出数据包捕获——这是 tcpdump 和 Wireshark 单独都无法做到的: + +```bash +# 使用 RustNet 捕获数据包(包含进程追踪) +sudo rustnet -i eth0 --pcap-export capture.pcap + +# 创建: +# capture.pcap - 标准 PCAP 文件 +# capture.pcap.connections.jsonl - 进程归因(PID、名称、时间戳) + +# 用进程信息富化 PCAP 并创建注释过的 PCAPNG +python scripts/pcap_enrich.py capture.pcap -o annotated.pcapng + +# 或者直接写出带 RustNet 数据包注释的 PCAPNG +sudo rustnet -i eth0 --pcapng-export annotated.pcapng + +# 在 Wireshark 中打开 —— 数据包现在显示进程信息注释 +wireshark annotated.pcapng +``` + +这个工作流让你兼得两者之长: +- **RustNet 的进程归因**:知道哪个应用生成了每个数据包 +- **Wireshark 的深度分析**:3000+ 解析器的完整协议解析 + +原生 PCAPNG 导出会直接嵌入实时 best-effort 数据包注释。富化脚本在清理阶段 sidecar 元数据完整性比捕获时生成单个文件更重要时仍然有用。 + +详见 [USAGE.zh-CN.md - PCAP 导出](USAGE.zh-CN.md#pcap-export)。 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d39f0c2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,601 @@ +# Changelog + +All notable changes to RustNet will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [1.4.0] - 2026-06-16 + +This release redesigns the TUI around a calmer visual hierarchy and, under the hood, +splits RustNet into a Cargo workspace of reusable library crates. Many of the TUI ideas +came from a detailed UI review by [@joshka](https://github.com/joshka) (Ratatui +maintainer) on our showcase submission +([ratatui/ratatui-website#1118](https://github.com/ratatui/ratatui-website/pull/1118)) — +thanks for the thoughtful feedback! + +### Added +- **Theme Presets**: New `--theme` flag. The default `muted` preset keeps a single + cyan accent and reserves color for signals (state changes, staleness, live + bandwidth) and addresses; `--theme classic` restores the previous full-color palette (#377) +- **System Sidebar Toggle**: The System panel now has a fixed width and can be + hidden with the `i` key (auto-hidden on narrow terminals) (#377) +- **Details Continuity Strip**: The Details tab opens with a mini connection table + of the selected row and its neighbors; `j`/`k` flips through them without leaving + the tab, following the grouped order when process grouping is enabled (#377) +- **Direct-Jump Tab Shortcuts**: Jump straight to a tab with keys `1`-`5`, with + bracket cycle aliases (#318, thanks @obchain) +- **Connection List Scrollbar**: A scrollbar appears on the connection list when it + overflows the viewport (#365) +- **FTP Deep Packet Inspection**: Detect the FTP control channel and extract command + and response metadata (#266, thanks @0xghost42) +- **DNS / mDNS / LLMNR Response IPs**: Populate `response_ips` from A/AAAA answer + records and extend the extraction to mDNS and LLMNR responses (#319, #333, #341, thanks @0xghost42) +- **Log Identity Banner**: Emit a program identity banner and the module target on + every log line for easier diagnostics (#320, thanks @0xghost42) +- **Landlock v6 IPC Scoping** (Linux): Best-effort Landlock that scopes abstract-socket + and signal IPC on kernels that support it, falling back gracefully on older ABIs (#363) +- **`no_new_privs` Always Set + cargo-deny**: Always set `no_new_privs` at startup, and + adopt `cargo-deny` for supply-chain and license auditing in CI (#382) +- **openSUSE OBS Release Pipeline**: Automated openSUSE Build Service releases (#356) + +### Changed +- **Stable Column Layout**: Column widths depend only on the terminal width — they + no longer shift while scrolling. Narrow terminals hide low-priority columns + instead of truncating cells; wide terminals distribute the spare width so the + table spans the full screen with the bandwidth column flush right (#377) +- **Merged Proto/App Column**: The Protocol column is merged into Application + ("TCP·HTTPS"), and the status-dot column is gone — staleness now lives entirely + in the row styling (#377) +- **Custom Tab Bar and Borderless Sections**: Numbered tab bar with an accent + underline, a single-line filter prompt, and section headers in place of the + border-box-around-everything look (#377) +- **Dependencies**: Routine dependency and GitHub Actions updates across the cycle + (Dependabot, ~18 PRs) + +### Fixed +- **Process attribution for short-lived and multithreaded processes** (Linux): + eBPF socket tracking now records the process name (thread-group leader) + instead of the calling thread's name, so connections from e.g. firefox or dig + no longer show up as "Socket Thread" or "isc-net-0000"; PID-to-name + resolution reads `/proc//comm` on demand instead of waiting for the + periodic scan; and new connections are enriched on a fast 250ms tick, so + process names appear almost immediately instead of after up to 2 seconds (#376) +- **DLT_NULL Link Layer**: Strip the 4-byte address-family header before parsing + DLT_NULL/loopback captures (#394, thanks @0xghost42) +- **Terminal Restore on Panic**: Restore the terminal via a chained panic hook so a + panic no longer leaves the terminal in raw mode (#364) +- **Scrollbar Thumb**: The scrollbar thumb now reaches the bottom at max scroll (#366) +- **Landlock `/sys` Access**: Allow read access to `/sys` so interface statistics work + under the Landlock sandbox (#370) +- **Filter Mode Backspace**: Handle raw backspace characters in filter mode (#335, thanks @iccccccccccccc) +- **eBPF Error Surfacing**: Classify libbpf errors and surface them in the TUI (#255, #258) +- **Native Builds**: Skip cross-compile library paths on native builds (#259) +- **RPM Packaging**: Own the directories and hicolor icon dirs the package creates, and + require `libcap-progs` on openSUSE so the `%post` `setcap` runs (#357, #358, #359, #360) + +### Performance +- **Per-Packet Allocations**: Cut per-packet allocations and snapshot copy-on-write + copies on the hot path (#380) +- **Core Types**: Add `Protocol::as_str()` and drop per-row/per-filter `to_string` + allocations (#392, thanks @obchain) +- **Connection Table**: Borrow the process name in `process_text` instead of cloning (#390, thanks @obchain) +- **Sparklines / Parsers**: Single-allocation sparkline getters, fewer redundant + collects in the HTTP and SSH parsers, and removed redundant clones in the render path + and sandbox init (#339, #345, #355, thanks @obchain) + +### Internal +- **Cargo Workspace Split**: RustNet is now a four-crate workspace — `rustnet-core` + (packet parsing, protocol/DPI types, link-layer, connection merging, DNS/GeoIP/OUI + lookups), `rustnet-capture` (libpcap/Npcap capture backend), `rustnet-host` + (per-connection process attribution), and the `rustnet-monitor` binary. The three + libraries are now published to crates.io alongside the binary (#367) + +### Documentation +- **Simplified Chinese**: Added a Simplified Chinese README translation and translated + the rest of the docs, plus zh-CN openSUSE Tumbleweed install instructions + (#263 thanks @whtis, #277 thanks @luojiyin1987, #361) +- **Install / Packaging Docs**: Nix and NixOS instructions and nixpkgs/NixOS-module + notes, Homebrew core formula pointer, Repology packaging overview, a Mermaid + architecture diagram, and a PR template with tightened contributor guidelines + (#264, #270, #281, #285, #286, #311, #332, #369) +- **Ubuntu 26.04 (Resolute) PPA**: Added the Resolute PPA build (#254, #256) + +### Contributors + +Special thanks to the contributors in this release: +- [@0xghost42](https://github.com/0xghost42) — FTP DPI, DNS/mDNS/LLMNR response-IP + extraction, the log identity banner, the DLT_NULL fix, and many DPI/eBPF refactors + (#266, #278, #279, #289, #290, #307, #309, #319, #320, #333, #341, #394) +- [@obchain](https://github.com/obchain) — performance and allocation cleanups across + the DPI parsers, render path, and core types, plus direct-jump tab shortcuts + (#292, #294, #296, #301, #303, #317, #318, #327, #339, #345, #355, #390, #392) +- [@iccccccccccccc](https://github.com/iccccccccccccc) — raw backspace handling in filter mode (#335) +- [@whtis](https://github.com/whtis) (HaiTao Wu) — Simplified Chinese README translation (#263) +- [@luojiyin1987](https://github.com/luojiyin1987) (luo jiyin) — Simplified Chinese documentation translation (#277) + +## [1.3.0] - 2026-05-05 + +The headline of this release is a major TUI refresh. The tabs, stats panel, and details view have all been redesigned, with new per-field colors, a status dot, and address scope labels making it easier to read connections at a glance. + +### Added +- **TUI Revamp**: Redesigned tabs, stats panel, and details view (#239) +- **Per-field Colors and Status Dot**: New per-field colors, status dot, and magenta panel borders for at-a-glance readability (#241) +- **Address Scope Labels**: Remote addresses are tagged PUBLIC, PRIVATE, etc. in the connection list (#251) +- **Reverse DNS Resolution by Default**: Reverse DNS resolution is now enabled by default. Use the new `--no-resolve-dns` flag to opt out (#245) + +### Fixed +- **Sandbox Info on Overview**: Show the full sandbox details on the overview tab (#250) +- **Search Scope and Status Bars**: Scope the `/` search to Overview and tidy the status bars (#229, #230) +- **QUIC Initial Packet Parser**: Bounds-check `token_len` in the Initial packet parser (#244) +- **QUIC Varint Parser**: Bounds-check varint lengths and isolate parser panics (#232) +- **Release Pipeline**: Fix the downstream trigger race and AUR token permissions (#223) + +### Changed +- **Demo Recording Automation**: Automate VHS recording for the demo GIF and README screenshots (#247) +- **OUI Vendor Database**: Refreshed IEEE OUI vendor database (#242) +- **Dependencies**: Bumped `rand` (0.8.5 to 0.8.6), `openssl` (0.10.75 to 0.10.78), `zip`, `libbpf-cargo`, and other rust-dependencies and actions group updates (#224, #225, #226, #227, #231, #233, #234, #238, #240, #243) + +### Documentation +- **Windows Sandbox Terminology**: Accurate Windows sandbox terminology and roadmap entry (#237) +- **README Polish**: README hero polish, metadata tune-up, and accuracy fixes (#236) +- **Crate and Module Docs**: Expanded crate and module docs and tuned metadata for discoverability (#235) + +## [1.2.0] - 2026-04-09 + +### Added +- **Windows Restricted Token Sandbox**: Drop privileges at startup on Windows using a restricted process token (#206) +- **macOS Seatbelt Sandboxing**: Apply a Seatbelt sandbox profile at startup on macOS, later tightened to restrict filesystem and IPC access (#196, #203) +- **Linux Sandbox Hardening**: Drop Linux capabilities and clear the ambient capability set after startup (#208) +- **Process Privilege in UI**: Show whether a process is privileged in the security section of the TUI (#197) +- **Filter: Exact Port Matching and Regex Support**: Filter syntax supports exact port matches and regex patterns (#195) +- **VLAN Support in PKTAP and SLL/SLL2**: Parse VLAN tags in PKTAP and SLL/SLL2 capture formats (#202) +- **VLAN Header in Layer 3 Extraction**: Account for VLAN headers when extracting layer 3 data (#199, thanks @deepakpjose) +- **IGMP Protocol Parsing**: Recognize and parse IGMP traffic (#209, thanks @deepakpjose) +- **Process Name for Wildcard /proc/net/ Entries**: Resolve process names for wildcard (`0.0.0.0`/`::`) entries in `/proc/net/` (#218, thanks @deepakpjose) +- **CI Supply-Chain Hardening**: Pin GitHub Actions to commit SHAs and verify Npcap installer checksums (#210) +- **Architecture Roadmap**: Added workspace split and macOS privilege separation roadmap docs (#211) + +### Fixed +- **Default Interface Selection**: Use the active routing table to pick the default interface (#194, thanks @l1a) +- **Root Detection on Unix**: Use `geteuid()` instead of `getuid()` to detect root (#192, thanks @DeepChirp) +- **Release Pipeline Reliability**: Improved release workflow reliability, gated downstream jobs on `publish-release`, added checksum verification to AUR updates, and documented the no-retag policy (2a38f2d, 795f7a1, 002eb55, 8403a0f) +- **FreeBSD CI Dispatch**: Restrict FreeBSD dispatch to manual triggers only (#201) + +### Changed +- **CPU Efficiency Improvements**: Substantial reductions in CPU usage across hot paths — rate calculation moved from per-update to per-refresh (#220), timeouts avoided to improve CPU performance (#213), threads given meaningful names to aid profiling (#212), and allocations reduced in sorting and snapshot paths (#222). Big thanks to [@deepakpjose](https://github.com/deepakpjose) for driving the CPU-efficiency work (#213, #220, #212) — these changes make RustNet noticeably lighter on the CPU. +- **FreeBSD Platform Cleanup**: Refactored FreeBSD platform support code (#205) +- **Dependencies**: Bumped `zip` (8.2.0 → 8.3.0 → 8.5.0), `clap_mangen`, `docker/login-action`, and other rust-dependencies group updates (#198, #200, #214, #216, #219, #221) +- **OUI Vendor Database**: Refreshed IEEE OUI vendor database (#215) + +### Contributors + +Special thanks to the external contributors in this release: +- [@deepakpjose](https://github.com/deepakpjose) — CPU-efficiency improvements and additional features (#199, #209, #212, #213, #218, #220) +- [@l1a](https://github.com/l1a) — default interface selection via active routing table (#194) +- [@DeepChirp](https://github.com/DeepChirp) — Unix root detection via `geteuid()` (#192) + +## [1.1.0] - 2026-03-16 + +### Added +- **OUI Vendor Lookup for ARP**: Display MAC vendor names for ARP connections using IEEE OUI database (#183) +- **Historic Connections Toggle**: Toggle to show/hide historic (closed) connections (#184) +- **Mouse Support**: Mouse interaction support for TUI navigation (#170) +- **Security Hardening & Packet Stats**: Enhanced security hardening and packet statistics display in TUI (#169) +- **GeoIP City Lookup**: Show city-level geolocation for remote IPs using GeoLite2 City database (#168) +- **Android Build Support**: Native Android builds with static musl linking (#167) +- **Multi-Arch Android Builds**: Added armv7, x86_64, and x86 Android static build targets +- **MQTT Protocol Detection**: Deep packet inspection for MQTT protocol traffic (#161) +- **STUN Traffic Detection**: Detect STUN protocol traffic per RFC 5389/8489 (#160) +- **BitTorrent Traffic Detection**: Detect BitTorrent protocol traffic (#159) +- **ARP Performance Benchmarks**: Added criterion benchmarks for ARP-related operations (#188) + +### Fixed +- **Undefined Behavior Fix**: Fix UB issues, remove clippy suppressions, add safety documentation (#187) +- **Light Terminal Readability**: Fix selection highlight unreadable on light terminal themes (#182) +- **Clipboard Warning**: Fix unused variable warning in copy_to_clipboard across platforms (#178) +- **Android Cross-Compilation**: Fix cross-compilation and release upload issues for Android targets (#174) +- **MQTT Detection Accuracy**: Restrict MQTT signature detection to CONNECT packets only (#164) + +### Changed +- **Documentation**: Synced docs with implementation, added missing keyboard shortcuts (#190, #157) +- **CI/CD**: Staged release pipeline so downstream jobs wait for builds (#154), added FreeBSD coverage to PR builds (#158) +- **Dependencies**: Bumped chrono, http_req, zip, and various rust-dependencies groups + +## [1.0.0] - 2026-02-09 + +### Added +- **GeoIP Location Support**: Show country codes for remote IPs using GeoLite2 databases with auto-discovery (#151) +- **PCAP Export with Process Attribution**: Export captured packets to PCAP files with a process attribution JSONL sidecar for Wireshark enrichment (#137) +- **eBPF-based ICMP PID Tracking**: Track process IDs for ICMP connections using eBPF on Linux (#136) +- **Process Detection Degradation Warnings**: Show warnings in the UI when process detection falls back to a less accurate method (#128) +- **ARM64 Musl Static Builds**: CI now produces arm64 musl static Linux builds with eBPF support + +### Fixed +- **Service Name Precedence**: Corrected ordering when multiple service name sources conflict (#150) +- **Pointer Dereference Safety**: Use `as_ref()` for safer pointer dereference in macOS/FreeBSD interface stats (#147) +- **Clippy Warnings**: Resolve `unnecessary_unwrap` errors flagged by clippy (#144) +- **ICMP Dead Code**: Remove dead code warning in ICMP handling (#138) +- **GitHub Actions Permissions**: Add explicit permissions to all GitHub Actions workflows (#131) +- **Logging Initialization**: Set up logging level before privileges check for earlier diagnostic output (#143) + +### Changed +- **SSH Heuristic Tightened**: Tighten SSH packet structure heuristic to reduce false positives (#135) +- **CI Reusable Workflows**: Share build logic via reusable workflow, remove redundant test-static-builds workflow +- **Chocolatey Automation**: Trigger Chocolatey package publish on release automatically +- **Code Alignment**: Refactoring and code alignment improvements (#149) +- **Dependencies**: Updated libbpf-rs to 0.26, bumped clap, time, zip, lru, and libc +- **Documentation**: Clarified RustNet vs Wireshark positioning, added PowerShell font troubleshooting, added JSON logging to feature comparison, added bandwhich to acknowledgments (#129, #130, #132, #133) + +## [0.18.0] - 2026-01-07 + +### Added +- **Process Grouping**: Expandable tree view to group connections by process (`a` to toggle grouping, `Space` to expand/collapse) +- **Traffic Visualization Graph Tab**: New Graph tab with real-time network traffic graphs and bandwidth visualization (press `Tab` to cycle through tabs) +- **Network Health Visualization**: Health indicators in Graph tab showing connection quality metrics +- **Reverse DNS Hostnames**: Display reverse DNS names in Details tab and filter PTR traffic (`--resolve-dns` to enable, `d` to toggle display) +- **BPF Filter Support**: New `--bpf-filter` option for custom packet capture filtering (e.g., `--bpf-filter "port 443"`) +- **Clear All Connections**: New hotkey (`x`) to clear all tracked connections +- **Enhanced JSON Logging**: Added pid, process_name, service_name fields to JSON log output +- **New DPI Protocols**: NTP, mDNS, LLMNR, DHCP, SNMP, SSDP, NetBIOS protocol detection with enhanced ARP display +- **Static Musl Builds**: Linux static binary builds using musl for better portability +- **Platform-Specific Help**: CLI help now shows platform-specific options + +### Fixed +- **macOS BPF Filter**: Skip PKTAP when BPF filter is specified to avoid conflicts +- **Linux Clipboard**: Handle clipboard access blocked by Landlock sandbox gracefully +- **Interface Stats**: Use safer pointer dereference in interface statistics + +### Changed +- **FreeBSD Builds**: Moved to separate rustnet-bsd repository for native builds +- **CI Improvements**: Homebrew formula auto-update on release, AUR workflow on publish +- **Dependencies**: Updated ratatui to 0.30.0, various dependency updates +- **Documentation**: Added contribution guidelines, Chocolatey and Arch Linux installation instructions + +## [0.17.0] - 2025-12-07 + +### Added +- **Landlock Sandbox for Linux**: Filesystem and network sandboxing for enhanced security + - Restricts filesystem access to `/proc` only after initialization + - Network sandbox blocks TCP bind/connect on kernel 6.4+ + - Drops `CAP_NET_RAW` capability after pcap handle is opened + - New CLI options: `--no-sandbox` and `--sandbox-strict` + - Comprehensive security documentation in SECURITY.md +- **eBPF Thread Name Resolution**: Resolve eBPF thread names (e.g., 'Socket Thread') to main process names (e.g., 'firefox') + - Uses periodic procfs PID cache for resolution + - Falls back to eBPF name for short-lived processes +- **AUR Package Automation**: Automated Arch Linux AUR package publishing workflow + +### Changed +- **Platform Code Reorganization**: Restructured platform-specific code into cleaner module hierarchy + - `src/network/platform/linux/` - Linux-specific code with eBPF and sandbox subdirectories + - `src/network/platform/macos/` - macOS-specific code + - `src/network/platform/freebsd/` - FreeBSD-specific code + - `src/network/platform/windows/` - Windows-specific code +- **QUIC DPI Simplification**: Unified SNI extraction helpers and simplified QUIC protocol handling + +### Fixed +- **Test Determinism**: Made RateTracker tests deterministic with injectable timestamps + +## [0.16.1] - 2025-11-22 + +### Fixed +- **Cross-Compilation**: Fixed eBPF build issues when cross-compiling to non-Linux platforms + - Made `libbpf-cargo` an optional build dependency + - Fixed `build.rs` to check TARGET environment variable instead of host platform + - Prevents Linux-specific dependencies from being built for FreeBSD, macOS, and Windows +- **FreeBSD Build**: Switched from cross-compilation to native FreeBSD VM builds + - Uses `vmactions/freebsd-vm` for native FreeBSD compilation + - Eliminates cross-compilation sysroot and library linking issues + - Ensures FreeBSD builds work reliably with native package manager + +## [0.16.0] - 2025-11-22 + +### Added +- **Network Interface Statistics**: Real-time monitoring of network interface statistics across all platforms + - Cross-platform support for Linux, macOS, Windows, and FreeBSD + - Display of interface-level metrics including packets sent/received, bytes transferred, and errors + - Platform-specific implementations optimized for each operating system + - New interface statistics module with dedicated platform handlers + +### Changed +- **Link Layer Parsing**: Refactored link layer parsing into modular components + - Separated link layer types (Ethernet, Linux SLL, Raw IP, TUN/TAP, PKTAP) + - Improved packet parsing architecture for better maintainability + - Enhanced support for various network interface types + +### Fixed +- **Windows Interface Stats**: Fixed interface statistics collection on Windows platforms + - Improved reliability of Windows network adapter statistics + - Better handling of Windows-specific network interfaces +- **macOS Interface Stats**: Platform-specific improvements for macOS interface statistics + - Enhanced accuracy of macOS network interface metrics + - Better integration with macOS network stack + +## [0.15.0] - 2025-10-25 + +### Added +- **Ubuntu PPA Packaging**: Official Ubuntu PPA repository for easy installation on Ubuntu/Debian-based distributions + - Automated GitHub Actions workflow for PPA releases + - Support for multiple Ubuntu versions + +### Changed +- **Bandwidth Sorting**: Changed bandwidth sorting to use combined up+down total instead of separate up/down sorting + - Simpler sorting behavior: press `s` once to sort by total bandwidth + - Display still shows "Down/Up" with individual values + - Arrow indicator shows when sorting by combined bandwidth total +- **Packet Capture Permissions**: Removed CAP_NET_ADMIN and CAP_SYS_ADMIN requirements + - Uses read-only packet capture (non-promiscuous mode) + - Reduced security footprint with minimal required capabilities + +### Fixed +- **Bandwidth Rate Tracking**: Improved accuracy and stability of bandwidth rate calculations + - More consistent rate measurements + - Better handling of network traffic bursts + +## [0.14.0] - 2025-10-12 + +### Added +- **eBPF Enabled by Default on Linux**: eBPF support is now enabled by default on Linux builds for enhanced performance + - Provides faster socket tracking with reduced overhead + - Includes CO-RE (Compile Once - Run Everywhere) support + - Graceful fallback to procfs when eBPF is unavailable +- **JSON Logging for SIEM Integration**: New JSON-structured logging output for security information and event management systems + - Enables integration with enterprise monitoring and security platforms + - Structured log format for easier parsing and analysis +- **TUN/TAP Interface Support**: Added support for TUN/TAP virtual network interfaces + - Enables monitoring of VPN connections and virtual network devices + - Expands interface compatibility for complex network setups +- **Fedora COPR RPM Packaging**: Official Fedora COPR repository for easy installation on Fedora/RHEL-based distributions + +### Fixed +- **High CPU Usage on Linux**: Eliminated excessive procfs scanning causing high CPU utilization + - Optimized process lookup frequency and caching strategy + - Significantly reduced system resource consumption during monitoring + +### Changed +- **Build Dependencies**: Bundled vmlinux.h files to eliminate network dependency during builds + - Improves build reliability and offline build capability + - Reduces external dependencies for compilation +- **Documentation**: Restructured documentation into focused files with improved musl static build documentation + +## [0.13.0] - 2025-10-04 + +### Added +- **Windows Process Identification**: Implemented full process lookup using Windows IP Helper API + - Uses GetExtendedTcpTable and GetExtendedUdpTable for connection-to-process mapping + - Resolves process names via OpenProcess and QueryFullProcessImageNameW + - Supports both TCP/UDP and IPv4/IPv6 connections + - Implements time-based caching with 2-second TTL for performance + - Migrated from winapi to windows crate (v0.59) for better maintainability +- **Privilege Detection**: Pre-flight privilege checking before network interface access + - Detects insufficient privileges on Linux, macOS, and Windows + - Provides platform-specific instructions (sudo, setcap, Docker flags) + - Shows errors before TUI initialization for better visibility + - Detects container environments with Docker-specific guidance + +### Fixed +- **Packet Length Calculation**: Use actual packet length from IP headers instead of captured length + - Extracts Total Length field from IP headers for accurate byte counting + - Fixes severe undercounting for large packets (NFS, jumbo frames) + - Resolves issues with snaplen-limited capture buffers + +### Changed +- **Documentation**: Updated ROADMAP.md and README.md with Windows process identification status and Arch Linux installation instructions + +## [0.12.1] - 2025-10-02 + +### Changed +- **Build Configuration**: Improved crate metadata for crates.io publishing + - No functional changes to the binary or runtime behavior + - Enhanced package configuration for better crate ecosystem integration + +## [0.12.0] - 2025-10-01 + +### Added +- **Vim-style Navigation**: Jump to beginning of connection list with `g` and end with `G` (Shift+g) +- **Table Sorting**: Comprehensive sorting functionality for all connection table columns + - Press `s` to cycle through sortable columns (Protocol, Local Address, Remote Address, State, Service, Application, Bandwidth ↓, Bandwidth ↑, Process) + - Press `S` (Shift+s) to toggle sort direction (ascending/descending) + - Visual indicators with arrows and cyan highlighting on active sort column + - Sort by download/upload bandwidth to find bandwidth hogs + - Alphabetical sorting for text columns +- **Port Display Toggle**: Press `p` to switch between service names and port numbers display +- **Connection Navigation Improvements**: Enhanced navigation with better visual cleanup indication +- **Localhost Filtering Control**: New `--show-localhost` command-line flag to override default localhost filtering + +### Fixed +- **Windows Double Key Issue**: Fixed duplicate key event handling on Windows platforms +- **Windows MSI Runtime Dependencies**: Added startup check for missing Npcap/WinPcap DLLs + - Displays helpful error message with installation instructions when DLLs are missing + - Added winapi dependency for Windows DLL detection + - Updated README with runtime dependency information +- **Linux Interface Selection**: Fixed "any" interface selection on Linux + - Improved interface detection and validation + - Better error handling for interface configuration +- **Package Dependencies**: Removed unnecessary runtime dependencies (clang, llvm) from RPM and DEB packages + - Reduces installation footprint and dependency conflicts +- **Docker Build**: Removed armv7 architecture from Docker builds for improved stability + +### Changed +- **Documentation**: Updated roadmap and README with new features and keyboard shortcuts + +## [0.11.0] - 2025-09-30 + +### Added +- **Docker Support with eBPF**: Docker images now include eBPF support for enhanced performance + - Multi-architecture Docker builds (amd64, arm64) + - eBPF-enabled images for advanced socket tracking on Linux + - Optimized container builds with proper dependency management +- **Cross-Platform Packaging and Release Automation**: Comprehensive automated release workflow + - Automated DEB, RPM, DMG, and MSI package generation + - Cross-platform CI/CD improvements + +### Fixed +- **RPM Package Dependencies**: Corrected libelf dependency specification in RPM packages +- **Windows MSI Packaging**: Fixed MSI installer generation issues +- **Release Workflow**: Resolved various release automation issues + +## [0.10.0] - 2025-09-28 + +### Added +- **Rust Version Requirements**: Added minimum Rust version requirement (1.88.0+) for let-chains support + +### Changed +- **Build Requirements**: Now requires Rust 1.88.0 or later for advanced language features + +## [0.9.0] - 2025-09-18 + +### Added +- **Experimental eBPF Support for Linux**: Enhanced socket tracking with optional eBPF backend + - eBPF-based socket tracker with CO-RE (Compile Once - Run Everywhere) support + - Minimal vmlinux header (5.5KB instead of full 3.4MB file) + - Graceful fallback mechanism to procfs when eBPF unavailable + - Support for both IPv4 and IPv6 socket tracking + - Optional feature disabled by default (enable with `--features=ebpf`) + - Comprehensive capability checking for required permissions +- **Windows Platform Support**: Network monitoring capability on Windows (without process identification) + +## [0.8.0] - 2025-09-11 + +### Added +- **SSH Deep Packet Inspection (DPI)**: Comprehensive SSH protocol analysis including: + - SSH version detection (SSH-1.x, SSH-2.0) + - Client and server software identification (OpenSSH, PuTTY, libssh, etc.) + - Connection state tracking: Banner, KeyExchange, Authentication, Established + - Algorithm detection and negotiation monitoring + - SSH-specific filtering with `ssh:` prefix in connection filters +- **Enhanced Filtering**: SSH connections now support detailed filtering by software name and connection state + +### Improved +- **CI/CD**: Enhanced GitHub Actions with path-based triggers for more efficient workflows +- **Documentation**: Updated README with SSH DPI examples and state descriptions + +## [0.7.0] - 2025-09-11 + +### Fixed +- SecureCRT backspace handling issue + +## [0.6.0] - 2025-09-10 + +### Added +- Connection state filtering (ESTABLISHED, TIME_WAIT, etc.) + +## [0.5.0] - 2025-01-09 + +### Added +- **Connection Filtering System**: New comprehensive filtering capability allowing users to filter connections by: + - Protocol type (TCP, UDP, ICMP) + - Local and remote IP addresses + - Local and remote ports + - Process names + - Service names + - Customizable filter expressions with intuitive UI +- **Enhanced Documentation**: Added asciinema demo recording for better user onboarding +- **Visual Demonstrations**: Added animated GIF showcasing RustNet functionality + +### Fixed +- **README Improvements**: Fixed image syntax and formatting issues for better GitHub display + +### Changed +- **User Interface**: Enhanced TUI to support dynamic filtering with keyboard shortcuts +- **Documentation**: Improved project presentation with visual aids and demonstrations + +## [0.4.0] - 2025-01-29 + +### Improved +- Enhanced traffic monitoring with better rate tracking and byte counters +- Fixed Linux platform build warnings for improved compilation stability +- Corrected version display to use dynamic version from Cargo.toml instead of hardcoded value + +## [0.3.0] - 2024-12-28 + +### Added +- Created `RELEASE.md` and `ROADMAP.md` for better project organization +- Enhanced memory efficiency through enum variant boxing + +### Fixed +- Major clippy warning cleanup (97% reduction from 38 to 1 warnings) +- Refactored functions using `TransportParams` struct to reduce complexity +- Fixed collapsible if patterns and improved code readability +- Eliminated needless borrows and manual implementations + +### Changed +- Moved release documentation to dedicated files +- Streamlined README to focus on user information +- Improved code organization and Rust best practices + +## [0.2.0] - 2024-12-19 + +### Added +- **Enhanced PKTAP Support on macOS**: Comprehensive process identification using macOS PKTAP (Packet Tap) headers + - Direct extraction of process names and PIDs from kernel packet metadata + - Robust handling of 20-byte PKTAP process name fields with proper normalization + - Support for both `pth_comm` and `pth_e_comm` (effective command name) fields + - Fallback to `lsof` system commands when PKTAP data is unavailable +- **Process Data Immutability System**: Once process information is set from any source, it becomes immutable to prevent display inconsistencies +- **Advanced Process Name Normalization**: Handles all types of whitespace, control characters, and padding in process names +- **Comprehensive Debug Logging**: Extensive logging for PKTAP header processing, process name extraction, and data flow tracking + +### Fixed +- **Process Display Stability on macOS**: Fixed issue where process names would change format during UI scrolling (e.g., "firefox (123)" → "firefox (123)") +- **PKTAP Header Processing**: Improved parsing of raw PKTAP packet headers with better error handling and validation +- **Process Name Consistency**: Eliminated race conditions and data inconsistencies in process name display +- **Whitespace Normalization**: Fixed handling of tabs, multiple spaces, unicode whitespace, and control characters in process names + +### Changed +- **Process Enrichment Logic**: Modified to respect existing PKTAP data and only fill in missing information from `lsof` +- **UI Rendering Optimization**: Simplified process name rendering to use pre-normalized data from sources +- **Error Handling**: Enhanced error reporting for PKTAP processing and process lookup failures + +### Technical Details +- Implemented `extract_process_name_from_bytes()` function for robust PKTAP process name extraction +- Added immutability enforcement in connection merge logic with violation detection +- Enhanced macOS process lookup with `normalize_process_name_robust()` function +- Improved byte-level debugging and logging for process identification troubleshooting + +### Platform-Specific Improvements +- **macOS**: PKTAP now provides primary process identification with significant performance and accuracy improvements over `lsof`-only approach +- **Linux**: Process enrichment logic updated to work consistently with new immutability system + +## [0.1.0] - 2024-XX-XX + +### Added +- Initial release of RustNet +- Real-time network connection monitoring +- Deep packet inspection (DPI) for HTTP, HTTPS, DNS, SSH, and QUIC +- Cross-platform support (Linux, macOS, Windows) +- Terminal user interface with ratatui +- Multi-threaded packet processing +- Process identification using platform-specific APIs +- Service name resolution +- Configurable refresh intervals and filtering options +- Optional logging with multiple log levels + +[Unreleased]: https://github.com/domcyrus/rustnet/compare/v1.4.0...HEAD +[1.4.0]: https://github.com/domcyrus/rustnet/compare/v1.3.0...v1.4.0 +[1.3.0]: https://github.com/domcyrus/rustnet/compare/v1.2.0...v1.3.0 +[1.2.0]: https://github.com/domcyrus/rustnet/compare/v1.1.0...v1.2.0 +[1.1.0]: https://github.com/domcyrus/rustnet/compare/v1.0.0...v1.1.0 +[1.0.0]: https://github.com/domcyrus/rustnet/compare/v0.18.0...v1.0.0 +[0.18.0]: https://github.com/domcyrus/rustnet/compare/v0.17.0...v0.18.0 +[0.17.0]: https://github.com/domcyrus/rustnet/compare/v0.16.1...v0.17.0 +[0.16.1]: https://github.com/domcyrus/rustnet/compare/v0.15.0...v0.16.1 +[0.15.0]: https://github.com/domcyrus/rustnet/compare/v0.14.0...v0.15.0 +[0.14.0]: https://github.com/domcyrus/rustnet/compare/v0.13.0...v0.14.0 +[0.13.0]: https://github.com/domcyrus/rustnet/compare/v0.12.1...v0.13.0 +[0.12.1]: https://github.com/domcyrus/rustnet/compare/v0.12.0...v0.12.1 +[0.12.0]: https://github.com/domcyrus/rustnet/compare/v0.11.0...v0.12.0 +[0.11.0]: https://github.com/domcyrus/rustnet/compare/v0.10.0...v0.11.0 +[0.10.0]: https://github.com/domcyrus/rustnet/compare/v0.9.0...v0.10.0 +[0.9.0]: https://github.com/domcyrus/rustnet/compare/v0.8.0...v0.9.0 +[0.8.0]: https://github.com/domcyrus/rustnet/compare/v0.7.0...v0.8.0 +[0.7.0]: https://github.com/domcyrus/rustnet/compare/v0.6.0...v0.7.0 +[0.6.0]: https://github.com/domcyrus/rustnet/compare/v0.5.0...v0.6.0 +[0.5.0]: https://github.com/domcyrus/rustnet/compare/v0.4.0...v0.5.0 +[0.4.0]: https://github.com/domcyrus/rustnet/compare/v0.3.0...v0.4.0 +[0.3.0]: https://github.com/domcyrus/rustnet/compare/v0.2.0...v0.3.0 +[0.2.0]: https://github.com/domcyrus/rustnet/compare/v0.1.0...v0.2.0 +[0.1.0]: https://github.com/domcyrus/rustnet/releases/tag/v0.1.0 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2d070d0 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,120 @@ +

English | 简体中文

+ +# Contributing to RustNet + +Pull requests are very welcome! Whether you're fixing bugs, adding features, improving documentation, or providing feedback, all contributions help make RustNet better. + +## Project Scope + +RustNet aims to stay small and fast. Not every protocol or feature belongs in the core tool, even when a contribution is well-written. + +For Deep Packet Inspection in particular, we lean toward protocols that: + +- A meaningful share of RustNet users will actually encounter on their networks. +- Produce visible, useful information (the plaintext window is wide enough to extract real metadata, not just to confirm the protocol exists before everything goes TLS). +- Fit the existing architecture without disproportionate maintenance cost. + +If a protocol is rarely seen in modern traffic, almost always TLS-wrapped, or niche to a single user base, we may still close the PR with thanks even if the code is correct. Please open an issue first for any new protocol so we can sanity-check fit before you invest implementation time. + +## Performance & Optimizations + +If a PR is motivated by performance, it must show that the code is on a real hot path. A microbenchmark alone is **not** enough: it only proves a function got faster in isolation, not that the function matters to overall runtime. + +- **Profile first.** Capture a flamegraph under real traffic (see [PROFILING.md](PROFILING.md)) and confirm the code shows up as meaningful CPU time. +- **A bench is supporting evidence, not proof.** A `cargo bench` / criterion comparison showing your change is faster is welcome, but it does not establish that the code is hot. +- **Cold-path micro-optimizations may be closed with thanks.** Optimizing code that runs rarely (connection setup, one-time parsing, error handling) adds review and maintenance cost without a meaningful gain. + +## Development Workflow + +We use the standard open-source fork and feature branch approach: + +1. **Open an issue first** to discuss your proposed changes. For features and non-trivial refactors, please wait for a maintainer response before writing code. Typo fixes, small bug fixes, and documentation corrections do not need a prior issue. +2. Fork the repository +3. Clone your fork locally +4. Create a feature branch from `main` (`git checkout -b feature/your-feature`) +5. Make your changes +6. Push to your fork (`git push origin feature/your-feature`) +7. Open a Pull Request against `main` and reference the related issue + +## Code Quality Requirements + +Before submitting a PR, please ensure: + +- **Unit tests**: Add tests for complex or critical code paths +- **No dead code**: Remove unused code, imports, and dependencies +- **Code style**: Follow the existing code style and patterns in the codebase +- **Clippy**: Fix all clippy warnings + ```bash + cargo clippy --all-targets --all-features -- -D warnings + ``` +- **No clippy suppression**: Do not use `#[allow(clippy::...)]` to suppress warnings. Fix the underlying issue instead (e.g., reduce arguments, refactor code). If a suppression is truly unavoidable, discuss it in the PR. +- **Formatting**: Run the formatter + ```bash + cargo fmt + ``` +- **Security audit**: Check for known vulnerabilities and policy violations in dependencies + ```bash + cargo deny check + ``` + +## CI Checks + +When you open a PR, our CI pipeline will automatically run checks including: + +- Clippy lints +- Code formatting verification +- Build on multiple platforms +- Test suite + +Please ensure all CI checks pass before requesting a review. + +## Dependency Policy + +Please be conservative with dependencies: + +- Don't add dependencies unless there's a good reason +- Prefer standard library solutions when possible +- If adding a dependency, explain the rationale in your PR description +- Consider the dependency's maintenance status and security track record + +## Security + +Security is important for a network monitoring tool: + +- Keep security in mind when writing code +- Avoid introducing common vulnerabilities (injection, buffer issues, etc.) +- Be careful with user input and network data parsing +- Report security issues responsibly (see [SECURITY.md](SECURITY.md)) + +## PR Guidelines + +- Write a clear description of what your changes do and why +- Link any related issues +- Keep PRs focused - one feature or fix per PR +- Be responsive to review feedback +- Verify locally before opening the PR. The PR template lists the exact commands. +- Add user-visible changes to the `## [Unreleased]` section of `CHANGELOG.md` in the same PR + +## Duplicate Pull Requests + +If two or more PRs address the same issue, the maintainers will evaluate them on their merits (code quality, test coverage, architectural fit) rather than submission order. The PR that best fits the project will be merged; others will be closed with thanks. If your PR is closed in favor of another, useful pieces from your work (documentation, tests, edge cases) may be ported over and credited. + +To avoid duplicate work, please comment on the linked issue stating that you intend to work on it before you start writing code. + +## AI-Assisted Contributions + +AI-assisted contributions are welcome, provided you treat the output as your own work and take responsibility for it. + +If you use an AI assistant (Copilot, Claude, ChatGPT, Cursor, or similar) to help write code or documentation: + +- **Read every line** you are submitting. You are accountable for it. +- **Run the verification commands** listed in the PR template yourself. Do not submit code with a note that tests "could not be run locally" and ask the reviewer to verify. +- **Make sure the PR description matches the code.** If the model wrote the PR body, confirm it describes what the diff actually does. +- **Do not split a single change into many cosmetic per-file commits** to make the work look incremental. One logical change, one commit, with a real commit message. +- **Do not open a PR seconds after filing the issue that motivates it.** Allow time for the maintainers and other contributors to weigh in on the proposed approach. + +PRs that show signs of unreviewed AI output (failing tests, fabricated APIs, unrelated bundled changes, mismatched descriptions) will be closed. + +## Questions? + +Feel free to open an issue if you have questions or want to discuss a potential contribution before starting work. diff --git a/CONTRIBUTING.zh-CN.md b/CONTRIBUTING.zh-CN.md new file mode 100644 index 0000000..0fb2565 --- /dev/null +++ b/CONTRIBUTING.zh-CN.md @@ -0,0 +1,119 @@ +

English | 简体中文

+ +# 参与贡献 + +欢迎提交 Pull Request!无论你是修复 bug、添加功能、改进文档,还是提供反馈,所有贡献都能让 RustNet 变得更好。 + +## 项目范围 + +RustNet 追求小而快。并不是每个协议或功能都适合放在核心工具里,即使贡献的代码写得很好。 + +对于深度包检测(DPI),我们倾向于满足以下条件的协议: + +- RustNet 用户中有相当一部分会在自己的网络中实际遇到。 +- 能提取出可见且有用的信息(明文窗口足够宽,能提取出真正的元数据,而不仅仅是确认协议存在就进入 TLS)。 +- 能以合理的维护成本融入现有架构。 + +如果某个协议在现代流量中很少见、几乎总是包裹在 TLS 里,或者仅服务于单一小众用户群,即使代码正确,我们也可能婉拒该 PR。请在实现任何新协议前先开一个 issue,以便我们在你投入实现时间之前确认其是否合适。 + +## 性能与优化 + +如果一个 PR 的出发点是性能,它必须证明相关代码确实位于真正的热点路径上。仅有微基准测试是**不够**的:它只能证明某个函数在孤立测试中变快了,并不能说明该函数对整体运行时间有影响。 + +- **先做性能分析。** 在真实流量下采集火焰图(参见 [PROFILING.zh-CN.md](PROFILING.zh-CN.md)),确认相关代码确实占用了可观的 CPU 时间。 +- **基准测试只是佐证,而非证明。** 欢迎附上 `cargo bench` / criterion 的对比结果来说明你的改动更快,但这并不能证明该代码是热点。 +- **冷路径上的微优化可能会被婉拒并致谢。** 优化很少执行的代码(连接建立、一次性解析、错误处理)会增加 review 和维护成本,却没有有意义的收益。 + +## 开发流程 + +我们采用标准的开源 fork 和功能分支方式: + +1. **先开 issue** 讨论你打算做的改动。对于功能性和非平凡的代码重构,请在写代码之前等待维护者的回复。拼写错误、小型 bug 修复和文档勘误不需要先开 issue。 +2. Fork 本仓库 +3. 将 fork 克隆到本地 +4. 从 `main` 创建功能分支(`git checkout -b feature/your-feature`) +5. 进行你的修改 +6. 推送到你的 fork(`git push origin feature/your-feature`) +7. 向 `main` 提交 Pull Request,并引用相关 issue + +## 代码质量要求 + +在提交 PR 之前,请确保: + +- **单元测试**:为复杂或关键的代码路径添加测试 +- **无死代码**:删除未使用的代码、导入和依赖 +- **代码风格**:遵循代码库中已有的代码风格和模式 +- **Clippy**:修复所有 clippy 警告 + ```bash + cargo clippy --all-targets --all-features -- -D warnings + ``` +- **禁止 clippy 抑制**:不要使用 `#[allow(clippy::...)]` 来抑制警告。请修复根本问题(例如减少参数、重构代码)。如果确实无法避免,请在 PR 中说明。 +- **格式化**:运行格式化工具 + ```bash + cargo fmt + ``` +- **安全审计**:检查依赖中的已知漏洞和策略违规 + ```bash + cargo deny check + ``` + +## CI 检查 + +当你提交 PR 时,我们的 CI 流水线会自动运行以下检查: + +- Clippy lint +- 代码格式化验证 +- 多平台构建 +- 测试套件 + +请在请求 review 之前确保所有 CI 检查通过。 + +## 依赖策略 + +请谨慎添加依赖: + +- 除非有充分的理由,否则不要添加依赖 +- 尽可能优先使用标准库方案 +- 如果必须添加依赖,请在 PR 描述中说明理由 +- 考虑依赖的维护状态和安全记录 + +## 安全 + +对于网络监控工具,安全至关重要: + +- 编写代码时牢记安全 +- 避免引入常见漏洞(注入、缓冲区问题等) +- 谨慎处理用户输入和网络数据解析 +- 请负责任地报告安全问题(参见 [SECURITY.zh-CN.md](SECURITY.zh-CN.md)) + +## PR 指南 + +- 清晰描述你的改动做了什么以及为什么 +- 链接任何相关的 issue +- 保持 PR 聚焦——每个 PR 只做一件事 +- 及时响应 review 反馈 +- 在提交 PR 之前在本地验证。PR 模板中列出了需要执行的精确命令。 + +## 重复 Pull Request + +如果两个或多个 PR 解决了同一个 issue,维护者将根据代码质量、测试覆盖率和架构契合度来评估,而不是提交顺序。最符合项目需求的 PR 将被合并;其他 PR 将被关闭并致谢。如果你的 PR 被另一个替代,你工作中的有用部分(文档、测试、边界情况)可能会被移植并给予署名。 + +为避免重复工作,请在开始编码之前,在相关 issue 下评论说明你有意处理它。 + +## AI 辅助贡献 + +欢迎使用 AI 辅助贡献,但前提是你将其输出视为自己的作品并对其负责。 + +如果你使用 AI 助手(Copilot、Claude、ChatGPT、Cursor 或类似工具)来辅助编写代码或文档: + +- **阅读你提交的每一行**。你要对它负责。 +- **自己运行 PR 模板中列出的验证命令**。不要提交代码后附带一句“无法在本地运行测试”并请 reviewer 验证。 +- **确保 PR 描述与代码一致。** 如果模型写了 PR 正文,请确认它确实描述了 diff 实际做的事情。 +- **不要把一个改动拆成多个仅涉及样式的逐文件提交** 来让工作看起来是增量完成的。一个逻辑改动,一个提交,附带有意义的提交信息。 +- **不要在提交 issue 的下一秒就开 PR。** 给维护者和其他贡献者留出时间对你提出的方案发表意见。 + +那些显示出未经 review 的 AI 输出痕迹的 PR(测试失败、虚构 API、无关的捆绑改动、描述不匹配)将被关闭。 + +## 有问题? + +如果你有任何问题,或者想在开始工作之前讨论潜在的贡献,欢迎随时开一个 issue。 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 0000000..2d6b282 --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,21 @@ +# Contributors + +We would like to thank the following people for their contributions to RustNet: + +## Maintainer + +- **Marco Cadetg** ([@domcyrus](https://github.com/domcyrus)) - Creator and maintainer + +## Contributors + +We would like to thank these people for their valuable contributions: + +- **DeepChirp** ([@DeepChirp](https://github.com/DeepChirp)) - Code contributions +- **Conor O'Callaghan** ([@Conor0Callaghan](https://github.com/Conor0Callaghan)) - JSON/SIEM logging research and design input +- **Ken Tobias** ([@l1a](https://github.com/l1a)) - Code contributions + +## Contributing + +We welcome and appreciate all contributions! Whether you're fixing bugs, adding features, improving documentation, or helping with ideas and feedback, your contributions make RustNet better. + +See our [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on how to contribute. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..96000ea --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4142 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" +dependencies = [ + "cipher", + "cpubits", + "cpufeatures 0.3.0", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloca" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "wl-clipboard-rs", + "x11rb", +] + +[[package]] +name = "atomic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", + "zeroize", +] + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bzip2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" +dependencies = [ + "libbz2-rs-sys", +] + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "caps" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd1ddba47aba30b6a889298ad0109c3b8dcb0e8fc993b459daa7067d46f865e0" +dependencies = [ + "libc", +] + +[[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 2.0.18", +] + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.2.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[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.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "cipher" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34d8227fe1ba289043aeb13792056ff80fd6de1a9f49137a5f499de8e8c78ea" +dependencies = [ + "crypto-common 0.2.1", + "inout", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_complete" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8b397918185f0161ff3d6fcaa9e4bfc09b8367caf6e1d4a2848e5477ed027b" +dependencies = [ + "clap", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "clap_mangen" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82842b45bf9f6a3be090dd860095ac30728042c08e0d6261ca7259b5d850f07" +dependencies = [ + "clap", + "roff", +] + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "compact_str" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "console" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +dependencies = [ + "encode_unicode", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpubits" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef0c543070d296ea414df2dd7625d1b24866ce206709d8a4a424f28377f5861" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" +dependencies = [ + "alloca", + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools 0.13.0", + "num-traits", + "oorandom", + "page_size", + "plotters", + "rayon", + "regex", + "serde", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" +dependencies = [ + "cast", + "itertools 0.13.0", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.13.0", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "csscolorparser" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" +dependencies = [ + "lab", + "phf", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dashmap" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "deflate64" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" + +[[package]] +name = "deltae" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +dependencies = [ + "block-buffer 0.12.0", + "const-oid", + "crypto-common 0.2.1", + "ctutils", + "zeroize", +] + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.13.0", + "objc2", +] + +[[package]] +name = "dns-lookup" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e39034cee21a2f5bbb66ba0e3689819c4bb5d00382a282006e802a7ffa6c41d" +dependencies = [ + "cfg-if", + "libc", + "socket2", + "windows-sys 0.60.2", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "euclid" +version = "0.22.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df61bf483e837f88d5c2291dcf55c67be7e676b3a51acc48db3a7b163b91ed63" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "fast-srgb8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "finl_unicode" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", + "zlib-rs", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix", + "windows-link", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasip3", + "wasm-bindgen", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.2", +] + +[[package]] +name = "http_req" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a74cb441cd28aa6f00dfb4d17216c134cfa17d252b2c65367e30fcbb824923f8" +dependencies = [ + "base64", + "native-tls", + "unicase", + "zeroize", +] + +[[package]] +name = "hybrid-array" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8655f91cd07f2b9d0c24137bd650fe69617773435ee5ec83022377777ce65ef1" +dependencies = [ + "typenum", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "image" +version = "0.25.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png", + "tiff", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "insta" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f0f8fee8c926415c58d6ae43a08523a26faccb2323f5e6b644fe7dd4ef6b82" +dependencies = [ + "console", + "once_cell", + "regex", + "similar", + "strip-ansi-escapes", + "tempfile", +] + +[[package]] +name = "instability" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ipnetwork" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +dependencies = [ + "serde", +] + +[[package]] +name = "ipnetwork" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf370abdafd54d13e54a620e8c3e1145f28e46cc9d704bc6d94414559df41763" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "kasuari" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fe90c1150662e858c7d5f945089b7517b0a80d8bf7ba4b1b5ffc984e7230a5b" +dependencies = [ + "hashbrown 0.16.1", + "portable-atomic", + "thiserror 2.0.18", +] + +[[package]] +name = "lab" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" + +[[package]] +name = "landlock" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "635839550ae8b90d9fd2571460a6645dc0aec070225956ca7a2831ed31d2795d" +dependencies = [ + "enumflags2", + "libc", + "thiserror 2.0.18", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libbpf-cargo" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e63283df9689af55b85f91e99dc8b213ae89fc3e1d9bb29ab7f75eb8a8a783" +dependencies = [ + "anyhow", + "cargo_metadata", + "clap", + "libbpf-rs", + "memmap2", + "serde", + "serde_json", + "tempfile", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "libbpf-rs" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd8e8db46a2d5967b5b3dba7aba32a875dc38f222395040607045635ec1f9b63" +dependencies = [ + "bitflags 2.13.0", + "libbpf-sys", + "libc", + "vsprintf", +] + +[[package]] +name = "libbpf-sys" +version = "1.7.0+v1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a109478760b2900aa2a6f2087e9d0de1d9c535b1758602af2845d5d2ccfaed7c" +dependencies = [ + "cc", + "nix 0.31.2", + "pkg-config", +] + +[[package]] +name = "libbz2-rs-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "line-clipping" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4de44e98ddbf09375cbf4d17714d18f39195f4f4894e8524501726fd9a8a4a" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9" +dependencies = [ + "hashbrown 0.17.1", +] + +[[package]] +name = "lzma-rust2" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47bb1e988e6fb779cf720ad431242d3f03167c1b3f2b1aae7f1a94b2495b36ae" +dependencies = [ + "sha2 0.10.9", +] + +[[package]] +name = "mac_address" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" +dependencies = [ + "nix 0.29.0", + "winapi", +] + +[[package]] +name = "maxminddb" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65e84ef32bcbf18a95548989e880db4af6fafd563463753afb4b9a149fb2782c" +dependencies = [ + "ipnetwork 0.21.1", + "log", + "memchr", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memmap2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +dependencies = [ + "libc", +] + +[[package]] +name = "memmem" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moxcms" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.13.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nix" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +dependencies = [ + "bitflags 2.13.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 = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-graphics", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "openssl" +version = "0.10.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" +dependencies = [ + "bitflags 2.13.0", + "cfg-if", + "foreign-types", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "palette" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" +dependencies = [ + "approx", + "fast-srgb8", + "libm", + "palette_derive", +] + +[[package]] +name = "palette_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pbkdf2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" +dependencies = [ + "digest 0.11.2", + "hmac", +] + +[[package]] +name = "pcap" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2eecc2ddc671ec563b5b39f846556aade68a65d1afb14d8fe6b30b0457d75" +dependencies = [ + "bitflags 1.3.2", + "errno 0.2.8", + "libc", + "libloading", + "pkg-config", + "regex", + "windows-sys 0.36.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2 0.10.9", +] + +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset 0.5.7", + "hashbrown 0.15.5", + "indexmap", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "pnet_base" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc190d4067df16af3aba49b3b74c469e611cad6314676eaf1157f31aa0fb2f7" +dependencies = [ + "no-std-net", +] + +[[package]] +name = "pnet_datalink" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79e70ec0be163102a332e1d2d5586d362ad76b01cec86f830241f2b6452a7b7" +dependencies = [ + "ipnetwork 0.20.0", + "libc", + "pnet_base", + "pnet_sys", + "winapi", +] + +[[package]] +name = "pnet_sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d4643d3d4db6b08741050c2f3afa9a892c4244c085a72fcda93c9c2c9a00f4b" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "png" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +dependencies = [ + "bitflags 2.13.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppmd-rust" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "procfs" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25485360a54d6861439d60facef26de713b1e126bf015ec8f98239467a2b82f7" +dependencies = [ + "bitflags 2.13.0", + "chrono", + "flate2", + "procfs-core", + "rustix", +] + +[[package]] +name = "procfs-core" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6401bf7b6af22f78b563665d15a22e9aef27775b79b149a66ca022468a4e405" +dependencies = [ + "bitflags 2.13.0", + "chrono", + "hex", +] + +[[package]] +name = "pxfm" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" +dependencies = [ + "num-traits", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +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 = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "ratatui" +version = "0.30.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3274ba0a2c5e1bcad2a2005d20f4dc59dad26b2eb0940fb094500dba4099d57d" +dependencies = [ + "instability", + "ratatui-core", + "ratatui-crossterm", + "ratatui-macros", + "ratatui-termina", + "ratatui-termwiz", + "ratatui-widgets", + "serde", +] + +[[package]] +name = "ratatui-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c" +dependencies = [ + "bitflags 2.13.0", + "compact_str", + "critical-section", + "hashbrown 0.17.1", + "itertools 0.14.0", + "kasuari", + "lru", + "palette", + "serde", + "strum", + "thiserror 2.0.18", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + +[[package]] +name = "ratatui-crossterm" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567584a3b0e6a8203c23de40b4861497266725eb5363dbfd18a1edd603cca9f0" +dependencies = [ + "cfg-if", + "crossterm", + "instability", + "ratatui-core", +] + +[[package]] +name = "ratatui-macros" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7dc68daa7498a43e4d68e0eb078427e10c38fbcfbb1e42d955f1fa2140d814" +dependencies = [ + "ratatui-core", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-termina" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0bf912d9e66f057a759d92e386a280ea886b352ab757d6ac4d653c7ed2c43c2" +dependencies = [ + "instability", + "ratatui-core", + "termina", +] + +[[package]] +name = "ratatui-termwiz" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf03e0380b7744054d6cb74224fe3adf062a029754933f575ca1e3b4c2ce977" +dependencies = [ + "ratatui-core", + "termwiz", +] + +[[package]] +name = "ratatui-widgets" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66e3d19bcc9130ca376277d93b60767ff121ace3be06f5f95f81dd68956407d1" +dependencies = [ + "bitflags 2.13.0", + "hashbrown 0.17.1", + "indoc", + "instability", + "itertools 0.14.0", + "line-clipping", + "ratatui-core", + "serde", + "strum", + "time", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "roff" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf2048e0e979efb2ca7b91c4f1a8d77c91853e9b987c94c555668a8994915ad" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags 2.13.0", + "errno 0.3.14", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustnet-capture" +version = "0.3.0" +dependencies = [ + "anyhow", + "log", + "pcap", +] + +[[package]] +name = "rustnet-core" +version = "0.3.0" +dependencies = [ + "aes", + "anyhow", + "crossbeam", + "dashmap", + "dns-lookup", + "flate2", + "log", + "maxminddb", + "pnet_datalink", + "ring", + "rustc-hash", +] + +[[package]] +name = "rustnet-host" +version = "0.3.0" +dependencies = [ + "anyhow", + "libbpf-cargo", + "libbpf-rs", + "libc", + "log", + "procfs", + "rustnet-core", + "windows", +] + +[[package]] +name = "rustnet-monitor" +version = "1.4.0" +dependencies = [ + "anyhow", + "arboard", + "caps", + "chrono", + "clap", + "clap_complete", + "clap_mangen", + "criterion", + "crossbeam", + "crossterm", + "dashmap", + "http_req", + "insta", + "landlock", + "libc", + "log", + "pcap", + "ratatui", + "regex-lite", + "rustc-hash", + "rustnet-capture", + "rustnet-core", + "rustnet-host", + "serde", + "serde_json", + "sha2 0.11.0", + "simplelog", + "windows", + "zip", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.13.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.2", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.2", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno 0.3.14", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "simplelog" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0" +dependencies = [ + "log", + "termcolor", + "time", +] + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strip-ansi-escapes" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" +dependencies = [ + "vte", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "termina" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9048a889effe34a5cddee0af7f53285198b16dca3be510858d38dfdb3e62a04e" +dependencies = [ + "bitflags 2.13.0", + "parking_lot", + "rustix", + "signal-hook", + "windows-sys 0.61.2", +] + +[[package]] +name = "terminfo" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" +dependencies = [ + "fnv", + "nom 7.1.3", + "phf", + "phf_codegen", +] + +[[package]] +name = "termios" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" +dependencies = [ + "libc", +] + +[[package]] +name = "termwiz" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" +dependencies = [ + "anyhow", + "base64", + "bitflags 2.13.0", + "fancy-regex", + "filedescriptor", + "finl_unicode", + "fixedbitset 0.4.2", + "hex", + "lazy_static", + "libc", + "log", + "memmem", + "nix 0.29.0", + "num-derive", + "num-traits", + "ordered-float", + "pest", + "pest_derive", + "phf", + "sha2 0.10.9", + "signal-hook", + "siphasher", + "terminfo", + "termios", + "thiserror 1.0.69", + "ucd-trie", + "unicode-segmentation", + "vtparse", + "wezterm-bidi", + "wezterm-blob-leases", + "wezterm-color-types", + "wezterm-dynamic", + "wezterm-input-types", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "js-sys", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "chrono", + "nu-ansi-term", + "sharded-slab", + "thread_local", + "tracing-core", +] + +[[package]] +name = "tree_magic_mini" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6" +dependencies = [ + "memchr", + "nom 8.0.0", + "petgraph", +] + +[[package]] +name = "typed-path" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3015e6ce46d5ad8751e4a772543a30c7511468070e98e64e20165f8f81155b64" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-truncate" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" +dependencies = [ + "itertools 0.14.0", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +dependencies = [ + "atomic", + "getrandom 0.3.4", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +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 = "vte" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" +dependencies = [ + "memchr", +] + +[[package]] +name = "vtparse" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.13.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wayland-backend" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fee64194ccd96bf648f42a65a7e589547096dfa702f7cadef84347b66ad164f9" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e6faa537fbb6c186cb9f1d41f2f811a4120d1b57ec61f50da451a0c5122bec" +dependencies = [ + "bitflags 2.13.0", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baeda9ffbcfc8cd6ddaade385eaf2393bd2115a69523c735f12242353c3df4f3" +dependencies = [ + "bitflags 2.13.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9597cdf02cf0c34cd5823786dce6b5ae8598f05c2daf5621b6e178d4f7345f3" +dependencies = [ + "bitflags 2.13.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5423e94b6a63e68e439803a3e153a9252d5ead12fd853334e2ad33997e3889e3" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6dbfc3ac5ef974c92a2235805cc0114033018ae1290a72e474aa8b28cbbdfd" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "wezterm-bidi" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" +dependencies = [ + "log", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-blob-leases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" +dependencies = [ + "getrandom 0.3.4", + "mac_address", + "sha2 0.10.9", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "wezterm-color-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" +dependencies = [ + "csscolorparser", + "deltae", + "lazy_static", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-dynamic" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" +dependencies = [ + "log", + "ordered-float", + "strsim", + "thiserror 1.0.69", + "wezterm-dynamic-derive", +] + +[[package]] +name = "wezterm-dynamic-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "wezterm-input-types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" +dependencies = [ + "bitflags 1.3.2", + "euclid", + "lazy_static", + "serde", + "wezterm-dynamic", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc 0.36.1", + "windows_i686_gnu 0.36.1", + "windows_i686_msvc 0.36.1", + "windows_x86_64_gnu 0.36.1", + "windows_x86_64_msvc 0.36.1", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.13.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "wl-clipboard-rs" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9651471a32e87d96ef3a127715382b2d11cc7c8bb9822ded8a7cc94072eb0a3" +dependencies = [ + "libc", + "log", + "os_pipe", + "rustix", + "thiserror 2.0.18", + "tree_magic_mini", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "gethostname", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "zerocopy" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zip" +version = "8.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" +dependencies = [ + "aes", + "bzip2", + "constant_time_eq", + "crc32fast", + "deflate64", + "flate2", + "getrandom 0.4.1", + "hmac", + "indexmap", + "lzma-rust2", + "memchr", + "pbkdf2", + "ppmd-rust", + "sha1", + "time", + "typed-path", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zlib-rs" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7948af682ccbc3342b6e9420e8c51c1fe5d7bf7756002b4a3c6cabfe96a7e3c" + +[[package]] +name = "zmij" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ad8fbc4 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,245 @@ +[workspace] +members = [".", "crates/rustnet-core", "crates/rustnet-capture", "crates/rustnet-host"] +resolver = "3" + +# Shared metadata for every crate in the workspace. Members opt in with +# `.workspace = true`. The binary keeps its own `version` (it tracks the +# user-facing 1.x release line, independent of the 0.x library crates). +[workspace.package] +version = "0.3.0" +authors = ["domcyrus"] +edition = "2024" +rust-version = "1.88.0" # Let-chains require Rust 1.88.0+ +repository = "https://github.com/domcyrus/rustnet" +homepage = "https://github.com/domcyrus/rustnet" +license = "Apache-2.0" + +# Single source of truth for the internal crate versions (and the few deps +# shared across crates). Bumping a library version is now a one-line edit here +# instead of one edit per member manifest. Members reference these with +# `.workspace = true`. +[workspace.dependencies] +rustnet-core = { version = "0.3.0", path = "crates/rustnet-core" } +rustnet-capture = { version = "0.3.0", path = "crates/rustnet-capture" } +rustnet-host = { version = "0.3.0", path = "crates/rustnet-host" } +anyhow = "1.0" +log = "0.4" + +[package] +name = "rustnet-monitor" +version = "1.4.0" +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +description = "A cross-platform network monitoring terminal UI tool built with Rust" +repository.workspace = true +homepage.workspace = true +documentation = "https://docs.rs/rustnet-monitor" +readme = "README.md" +license.workspace = true +keywords = ["network", "monitoring", "tui", "ebpf", "packet-capture"] +categories = ["command-line-utilities", "network-programming", "visualization"] +exclude = [".github/", "scripts/", "tests/", "*.log", "target/", ".gitignore"] + +[lib] +name = "rustnet_monitor" +path = "src/lib.rs" + +[[bin]] +name = "rustnet" +path = "src/main.rs" +# The bin re-compiles every src/* module that lib.rs also re-exports. +# Running `cargo test` on the bin therefore duplicates every unit test +# under a different crate prefix, which breaks snapshot-test file paths +# (`rustnet__...` vs `rustnet_monitor__...`). Tests live in the lib and +# in `tests/`; the bin is just the entry point. +test = false + +[dependencies] +rustnet-core.workspace = true +rustnet-capture.workspace = true +rustnet-host.workspace = true +anyhow.workspace = true +libc = "0.2" +arboard = { version = "3.6", features = ["wayland-data-control"] } +crossterm = "0.29" +crossbeam = "0.8" +dashmap = "6.2" +log.workspace = true +pcap = "2.4.0" +clap = { version = "4.6", features = ["derive"] } +simplelog = "0.12" +chrono = "0.4" +ratatui = { version = "0.30", features = ["all-widgets"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +regex-lite = "0.1" +# Note: dns-lookup, ring, aes, flate2, and maxminddb moved to rustnet-core, +# which is the only place they are used. They are re-exported transitively +# through the `crate::network` facade in src/network/mod.rs. + +[target.'cfg(target_os = "linux")'.dependencies] +# procfs + libbpf-rs moved to the rustnet-host crate (process attribution). +landlock = { version = "0.4", optional = true } +caps = { version = "0.5", optional = true } + +[target.'cfg(windows)'.dependencies] +windows = { version = "0.62", features = [ + "Win32_Foundation", + "Win32_NetworkManagement_IpHelper", + "Win32_NetworkManagement_Ndis", + "Win32_Networking_WinSock", + "Win32_Security", + "Win32_System_JobObjects", + "Win32_System_LibraryLoader", + "Win32_System_Threading", +] } + +# FreeBSD support uses the system's sockstat command for process lookup +# and native libpcap (via pcap crate) for packet capture. +# No additional FreeBSD-specific dependencies required at this time. + +[build-dependencies] +anyhow.workspace = true +clap = { version = "4.6", features = ["derive"] } +clap_complete = "4.6" +clap_mangen = "0.3" + +[target.'cfg(windows)'.build-dependencies] +http_req = "0.14" +zip = "8.6" +sha2 = "0.11" +windows = { version = "0.62", features = [ + "Win32_Foundation", + "Win32_NetworkManagement_IpHelper", + "Win32_NetworkManagement_Ndis", + "Win32_Networking_WinSock", + "Win32_Security", + "Win32_System_LibraryLoader", + "Win32_System_Threading", +] } + +[features] +# eBPF is enabled by default for enhanced performance on Linux. +# On non-Linux platforms, this feature has no effect as all eBPF code +# and dependencies are Linux-specific (guarded by target_os checks). +# Landlock provides security sandboxing on Linux 5.13+. +# macos-sandbox provides Seatbelt sandboxing on macOS 10.5+ (no extra deps). +default = ["ebpf", "landlock", "macos-sandbox"] +linux-default = ["ebpf"] # Deprecated: kept for backwards compatibility +# eBPF now lives in the rustnet-host crate; this is a passthrough so +# `--features ebpf` (and the default) keep enabling it as before. +ebpf = ["rustnet-host/ebpf"] +landlock = ["dep:landlock", "dep:caps"] +macos-sandbox = [] +# Kubernetes pod and container resolution from cgroup paths and (Phase 2) the +# on-disk kubelet pods directory. Linux-only at runtime; no extra dependencies. +kubernetes = ["rustnet-core/kubernetes"] + +# Minimal cross configuration to override dependency conflicts +[workspace.metadata.cross.build.env] +passthrough = [ + "CARGO_INCREMENTAL", + "CARGO_NET_RETRY", + "CARGO_NET_TIMEOUT", +] + +[package.metadata.deb] +maintainer = "domcyrus " +copyright = "2024, domcyrus " +license-file = ["LICENSE", "4"] +extended-description = """\ +A real-time network monitoring terminal UI tool built with Rust. + +Features: +- Real-time network monitoring with detailed state information +- Deep packet inspection for HTTP/HTTPS, DNS, SSH, and QUIC +- Connection lifecycle management with configurable timeouts +- Process identification and service name resolution +- Advanced filtering with vim/fzf-style search +- Multi-threaded processing for optimal performance +- eBPF-enhanced process detection on Linux (with automatic fallback) +""" +depends = "libpcap0.8, libelf1" +section = "net" +priority = "optional" +assets = [ + [ + "target/release/rustnet", + "usr/bin/", + "755", + ], + [ + "README.md", + "usr/share/doc/rustnet-monitor/", + "644", + ], + [ + "crates/rustnet-core/assets/services", + "usr/share/rustnet-monitor/", + "644", + ], + [ + "resources/packaging/linux/graphics/rustnet.png", + "usr/share/icons/hicolor/256x256/apps/", + "644", + ], + [ + "resources/packaging/linux/rustnet.desktop", + "usr/share/applications/", + "644", + ], +] +conf-files = [] + +[package.metadata.generate-rpm] +assets = [ + { source = "target/release/rustnet", dest = "/usr/bin/rustnet", mode = "755" }, + { source = "README.md", dest = "/usr/share/doc/rustnet-monitor/README.md", mode = "644" }, + { source = "crates/rustnet-core/assets/services", dest = "/usr/share/rustnet-monitor/services", mode = "644" }, + { source = "resources/packaging/linux/graphics/rustnet.png", dest = "/usr/share/icons/hicolor/256x256/apps/rustnet.png", mode = "644" }, + { source = "resources/packaging/linux/rustnet.desktop", dest = "/usr/share/applications/rustnet.desktop", mode = "644" }, +] +[package.metadata.generate-rpm.requires] +libpcap = "*" +elfutils-libelf = "*" + +[package.metadata.wix] +upgrade-guid = "455c823b-9665-43e0-baa4-bd0fcb762463" +path-guid = "d3a2452e-f04f-4d4f-becf-c3580f49f8fc" +license = false +eula = false + +[dev-dependencies] +criterion = { version = "0.8", features = ["html_reports"] } +insta = { version = "1", features = ["filters"] } +rustc-hash = "2" + +[[bench]] +name = "packet_parsing" +harness = false + +[[bench]] +name = "connection_merge" +harness = false + +[[bench]] +name = "snapshot" +harness = false + +[[bench]] +name = "rate_tracker" +harness = false + +[[bench]] +name = "tracker_ingest" +harness = false + +[profile.release] +lto = "thin" +codegen-units = 1 + +[profile.profiling] +inherits = "release" +debug = 2 +strip = false diff --git a/Cross.toml b/Cross.toml new file mode 100644 index 0000000..74cca66 --- /dev/null +++ b/Cross.toml @@ -0,0 +1,21 @@ +[target.aarch64-unknown-linux-gnu] +image = "ghcr.io/cross-rs/aarch64-unknown-linux-gnu:edge" +pre-build = [ + "dpkg --add-architecture $CROSS_DEB_ARCH", + "apt-get update -y", + "apt-get install -y libelf-dev:arm64 zlib1g-dev:arm64 libpcap-dev:arm64 gcc-aarch64-linux-gnu protobuf-compiler libseccomp-dev:arm64 libbpf-dev clang llvm" +] + +[target.armv7-unknown-linux-gnueabihf] +image = "ghcr.io/cross-rs/armv7-unknown-linux-gnueabihf:edge" +pre-build = [ + "dpkg --add-architecture $CROSS_DEB_ARCH", + "apt-get update -y", + "apt-get install -y libelf-dev:armhf zlib1g-dev:armhf libpcap-dev:armhf gcc-arm-linux-gnueabihf protobuf-compiler libseccomp-dev:armhf libbpf-dev clang llvm" +] + +[target.x86_64-unknown-freebsd] +# FreeBSD cross-compilation uses a Linux container with FreeBSD sysroot +# The sysroot should already contain libpcap +# No pre-build steps needed as we can't run FreeBSD package manager in Linux container + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e407a94 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,111 @@ +# Multi-stage Docker build for RustNet +# Base images are pinned by digest for reproducible, tamper-evident builds. +FROM rust:1.96-slim@sha256:31ee7fc65186be7e0e0ccb3f2ca305f14e4739e7642a1ae65753aa5d7b874523 AS builder + +# Install rustfmt component (required for eBPF compilation) +RUN rustup component add rustfmt + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + libpcap-dev \ + libelf-dev \ + zlib1g-dev \ + clang \ + llvm \ + make \ + pkg-config \ + && rm -rf /var/lib/apt/lists/* + +# Set working directory +WORKDIR /app + +# Copy Cargo files first for better caching +COPY Cargo.toml Cargo.lock ./ + +# Copy build script (manpage/completions; npcap on Windows) +COPY build.rs ./ + +# Copy source code +COPY src ./src +COPY benches ./benches +# Workspace member crates. rustnet-core holds the baked-in oui.gz / services +# assets (include_bytes!/include_str!); rustnet-host holds the eBPF programs and +# bundled vmlinux headers compiled by its own build.rs. +COPY crates ./crates + +# Build the application in release mode (eBPF is enabled by default on Linux). +# Optional features can be added with --build-arg CARGO_FEATURES=kubernetes +# (additive: default features stay on). The CI Kubernetes image variant passes +# CARGO_FEATURES=kubernetes; the default image leaves it empty. +ARG CARGO_FEATURES="" +RUN if [ -n "$CARGO_FEATURES" ]; then \ + cargo build --release --features "$CARGO_FEATURES"; \ + else \ + cargo build --release; \ + fi + +# Runtime stage - use trixie-slim to match GLIBC version from builder +# Pinned by digest for a reproducible, tamper-evident base image. +FROM debian:trixie-slim@sha256:28de0877c2189802884ccd20f15ee41c203573bd87bb6b883f5f46362d24c5c2 + +# Install runtime dependencies +# libcap2-bin provides setcap, used below to grant packet-capture capabilities +# to the binary so it can run as a non-root user. +RUN apt-get update && apt-get install -y \ + libpcap0.8 \ + libelf1 \ + zlib1g \ + ca-certificates \ + libcap2-bin \ + && rm -rf /var/lib/apt/lists/* + +# Create a non-root user. The container runs as this user (see USER below); +# packet-capture privileges are granted via file capabilities on the binary +# rather than by running as root. +RUN useradd -r -s /bin/false rustnet + +# Set working directory +WORKDIR /app + +# Copy the binary from builder stage +COPY --from=builder /app/target/release/rustnet /usr/local/bin/rustnet + +# Copy the services asset for reference (the binary already embeds it at build time) +COPY --from=builder /app/crates/rustnet-core/assets/services ./assets/services + +# Create logs directory +RUN mkdir -p /app/logs && chown rustnet:rustnet /app/logs + +# Set executable permissions and grant CAP_NET_RAW as a file capability so the +# binary can capture packets as a non-root user. NET_RAW is in Docker's default +# capability bounding set, so `docker run rustnet` works without extra flags. +# +# Only NET_RAW is baked in on purpose: a file capability that is NOT also in the +# container's bounding set makes execve() fail with EPERM. BPF/PERFMON are not +# in Docker's default set, so eBPF is handled at runtime instead (see below). +RUN chmod +x /usr/local/bin/rustnet \ + && setcap 'cap_net_raw=ep' /usr/local/bin/rustnet + +# Expose no ports by default (rustnet is for monitoring, not serving) +# Network access is handled via host networking or packet capture capabilities + +# Add labels for better image metadata +LABEL org.opencontainers.image.title="RustNet" +LABEL org.opencontainers.image.description="A cross-platform network monitoring tool with deep packet inspection" +LABEL org.opencontainers.image.source="https://github.com/domcyrus/rustnet" +LABEL org.opencontainers.image.licenses="Apache License, Version 2.0" + +# RustNet runs as the non-root 'rustnet' user. CAP_NET_RAW is baked into the +# binary as a file capability and is part of Docker's default bounding set, so +# basic packet capture works out of the box: +# docker run rustnet +# eBPF-based process attribution needs BPF+PERFMON, which are NOT in the default +# bounding set and can't be granted to a non-root user via file capabilities. +# Enable eBPF by running as root with the extra caps (modern kernels 5.8+): +# docker run --user root --cap-add=BPF --cap-add=PERFMON rustnet +# Legacy kernels require broad CAP_SYS_ADMIN for eBPF. RustNet does not +# recommend granting it by default; without eBPF caps, rustnet falls back to +# /proc-based process detection. +# CAP_NET_ADMIN is NOT required (read-only, non-promiscuous capture). +USER rustnet +ENTRYPOINT ["rustnet"] diff --git a/Dockerfile.static b/Dockerfile.static new file mode 100644 index 0000000..42bb80a --- /dev/null +++ b/Dockerfile.static @@ -0,0 +1,61 @@ +# Dockerfile for building static musl-linked RustNet binary +# +# Usage (with eBPF - default, recommended): +# docker build -f Dockerfile.static -t rustnet-static . +# docker run --rm -v $(pwd)/dist:/dist rustnet-static cp /build/target/release/rustnet /dist/ +# +# Usage (without eBPF - smaller binary, ~5.2MB vs ~6.5MB): +# docker build -f Dockerfile.static --build-arg FEATURES="--no-default-features" -t rustnet-static . + +FROM rust:alpine + +ARG FEATURES="" + +# Install build dependencies +# - musl-dev: musl C library headers +# - libpcap-dev: libpcap headers and static library (/usr/lib/libpcap.a) +# - pkgconfig: for finding library paths +# - build-base: basic build tools (make, gcc, etc.) +# - perl: required by some build scripts (ring crate) +# - elfutils-dev: libelf for eBPF (includes static library) +# - zlib-dev/zlib-static: compression library +# - zstd-dev/zstd-static: Zstandard compression (required by elfutils 0.189+) +# - clang/llvm: for eBPF compilation +# - linux-headers: kernel headers for eBPF +RUN apk add --no-cache \ + musl-dev \ + libpcap-dev \ + pkgconfig \ + build-base \ + perl \ + elfutils-dev \ + zlib-dev \ + zlib-static \ + zstd-dev \ + zstd-static \ + clang \ + llvm \ + linux-headers + +# Add rustfmt for eBPF skeleton generation +RUN rustup component add rustfmt + +WORKDIR /build + +# Copy source code +COPY . . + +# Configure static linking for zstd +# This fixes the elfutils 0.189+ undeclared dependency on zstd +# See: https://github.com/libbpf/bpftool/issues/152 +RUN mkdir -p .cargo && printf '[target.x86_64-unknown-linux-musl]\nrustflags = ["-C", "link-arg=-l:libzstd.a"]\n' > .cargo/config.toml + +# Build configuration: +# - In Alpine/musl, binaries are statically linked by default +# - FEATURES arg controls whether eBPF is included (default) or disabled +# - --release: Optimized build +RUN cargo build --release ${FEATURES} + +# Verify the binary is statically linked +RUN file target/release/rustnet && \ + ldd target/release/rustnet 2>&1 || echo "Binary is statically linked" diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..bd173ce --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,1160 @@ +

English | 简体中文

+ +# Installation Guide + +This guide covers all installation methods for RustNet across different platforms. + +> **Tip:** For an at-a-glance view of which distributions package RustNet and which version each carries, see [RustNet on Repology](https://repology.org/project/rustnet/versions). + +## Table of Contents + +- [Installing from Release Packages](#installing-from-release-packages) + - [macOS DMG Installation](#macos-dmg-installation) + - [Windows MSI Installation](#windows-msi-installation) + - [Windows Chocolatey Installation](#windows-chocolatey-installation) + - [Linux Package Installation](#linux-package-installation) + - [FreeBSD Installation](#freebsd-installation) + - [Android (Termux) Installation](#android-termux-installation) +- [Install via Cargo](#install-via-cargo) +- [Building from Source](#building-from-source) +- [Using Docker](#using-docker) +- [Prerequisites](#prerequisites) +- [Permissions Setup](#permissions-setup) +- [GeoIP Databases (Optional)](#geoip-databases-optional) +- [Troubleshooting](#troubleshooting) + +## Installing from Release Packages + +Pre-built packages are available for each release on the [GitHub Releases](https://github.com/domcyrus/rustnet/releases) page. + +### macOS DMG Installation + +> ** Prefer Homebrew?** If you have Homebrew installed, using `brew install` is easier and avoids Gatekeeper bypass steps. See [Homebrew Installation](#homebrew-installation) for instructions. + +1. **Download** the appropriate DMG for your architecture: + - `Rustnet_macOS_AppleSilicon.dmg` for Apple Silicon Macs (M1/M2/M3) + - `Rustnet_macOS_Intel.dmg` for Intel-based Macs + +2. **Open the DMG** and drag Rustnet.app to your Applications folder + +3. **Bypass Gatekeeper** (for unsigned builds): + - When you first try to open RustNet, macOS will block it because the app is not signed + - Go to **System Settings → Privacy & Security** + - Scroll down to find the message about RustNet being blocked + - Click **"Open Anyway"** to allow the application to run + - You may need to confirm this choice when launching the app again + +4. **Run RustNet**: + - Double-click Rustnet.app to launch it in a Terminal window with sudo + - Or run from command line: `sudo /Applications/Rustnet.app/Contents/MacOS/rustnet` + +5. **Optional: Create a symlink for shell access**: + ```bash + # Create a symlink so you can run 'rustnet' from anywhere + sudo ln -s /Applications/Rustnet.app/Contents/MacOS/rustnet /usr/local/bin/rustnet + + # Now you can run from any terminal: + sudo rustnet + ``` + +6. **Optional: Setup BPF permissions** (to avoid needing sudo): + - Install Wireshark's BPF permission helper: `brew install --cask wireshark-chmodbpf` + - Log out and back in for group changes to take effect + - See the [Permissions Setup](#permissions-setup) section for detailed instructions + +### Windows MSI Installation + +1. **Install Npcap Runtime** (required for packet capture): + - Download from https://npcap.com/dist/ + - Run the installer and select **"WinPcap API compatible mode"** + +2. **Download and install** the appropriate MSI package: + - `Rustnet_Windows_64-bit.msi` for 64-bit Windows + - `Rustnet_Windows_32-bit.msi` for 32-bit Windows + +3. **Run the installer** and follow the installation wizard + +4. **Run RustNet**: + - Open Command Prompt or PowerShell + - Run: `rustnet.exe` + - If Npcap is not installed or not in WinPcap compatible mode, RustNet will display a helpful error message with installation instructions + - Note: Depending on your Npcap installation settings, you may or may not need Administrator privileges + +### Windows Chocolatey Installation + +The easiest way to install RustNet on Windows is via [Chocolatey](https://community.chocolatey.org/packages/rustnet): + +```powershell +# Run in Administrator PowerShell +choco install rustnet +``` + +**Note:** You still need to install [Npcap](https://npcap.com) separately with "WinPcap API compatible mode" enabled. + +### Linux Package Installation + +#### Ubuntu PPA (Recommended for Ubuntu 25.10 Questing and 26.04 LTS Resolute) + +The easiest way to install RustNet on Ubuntu is via the official PPA. The PPA publishes builds for the following Ubuntu series: + +- Ubuntu 25.10 (Questing Quokka) +- Ubuntu 26.04 LTS (Resolute Raccoon) + +```bash +# Add the RustNet PPA +sudo add-apt-repository ppa:domcyrus/rustnet + +# Update package list +sudo apt update + +# Install rustnet +sudo apt install rustnet + +# Run with sudo +sudo rustnet + +# Optional: Grant capabilities to run without sudo (modern kernel 5.8+) +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' /usr/bin/rustnet +rustnet +``` + +**Important:** The PPA supports only the two series listed above (Ubuntu 25.10 Questing and 26.04 LTS Resolute) because the build requires Rust 1.88+ (used for let-chains in the project). Earlier Ubuntu versions don't ship a recent enough `rustc` in their repositories. For older Ubuntu versions, use the [.deb packages](#debianubuntu-deb-packages) from GitHub releases or [build from source](#building-from-source). + +#### Debian/Ubuntu (.deb packages) + +For manual installation or non-Ubuntu Debian-based distributions: + +```bash +# Download the appropriate package for your architecture: +# - Rustnet_LinuxDEB_amd64.deb (x86_64) +# - Rustnet_LinuxDEB_arm64.deb (ARM64) +# - Rustnet_LinuxDEB_armhf.deb (ARMv7) + +# Install the package (capabilities are automatically configured) +sudo dpkg -i Rustnet_LinuxDEB_amd64.deb + +# Install dependencies if needed +sudo apt-get install -f + +# Run without sudo (capabilities were set by post-install script) +rustnet + +# Verify capabilities +getcap /usr/bin/rustnet +``` + +**Note:** The .deb package automatically sets Linux capabilities via post-install script, so you can run RustNet without sudo. + +#### RedHat/Fedora/CentOS (.rpm packages) + +For manual installation or distributions not using COPR: + +```bash +# Download the appropriate package for your architecture: +# - Rustnet_LinuxRPM_x86_64.rpm +# - Rustnet_LinuxRPM_aarch64.rpm + +# Install the package (capabilities are automatically configured) +sudo rpm -i Rustnet_LinuxRPM_x86_64.rpm +# Or with dnf/yum: +sudo dnf install Rustnet_LinuxRPM_x86_64.rpm + +# Run without sudo (capabilities were set by post-install script) +rustnet + +# Verify capabilities +getcap /usr/bin/rustnet +``` + +**Note:** The .rpm package automatically sets Linux capabilities via post-install script, so you can run RustNet without sudo. + +#### Arch Linux + +This package is included in the Arch Linux Extra repository ([link](https://archlinux.org/packages/extra/x86_64/rustnet/)). It can be installed using pacman: +```bash +sudo pacman -S rustnet +``` + +Alternatively, two AUR packages are available: +- [`rustnet-bin`](https://aur.archlinux.org/packages/rustnet-bin) - Pre-compiled binary from GitHub Releases +- [`rustnet-git`](https://aur.archlinux.org/packages/rustnet-git) - Build from source and use the latest commit (maintained by [@DeepChirp](https://github.com/DeepChirp)) + +Install with your preferred AUR helper: +```bash +# Pre-compiled binary from GitHub Releases +yay -S rustnet-bin + +# OR source build from the latest commit +yay -S rustnet-git +``` + +#### Nix / NixOS + +RustNet is available in [nixpkgs](https://search.nixos.org/packages?query=rustnet), including the **stable** channels (as well as `nixpkgs-unstable`). + +**Try it without installing (ephemeral shell):** + +```bash +nix-shell -p rustnet +# Then inside the shell: +sudo rustnet +``` + +**Persistent install on NixOS** — add to `/etc/nixos/configuration.nix`: + +```nix +environment.systemPackages = with pkgs; [ + rustnet +]; +``` + +Then run `sudo nixos-rebuild switch`. + +**Note on permissions:** NixOS's `/nix/store` is read-only, so `sudo setcap` on the binary won't persist across rebuilds. The simplest option is `sudo rustnet`. For sudo-less operation, define a NixOS [security.wrappers](https://search.nixos.org/options?channel=unstable&query=security.wrappers) entry that carries the capabilities: + +```nix +security.wrappers.rustnet = { + source = "${pkgs.rustnet}/bin/rustnet"; + owner = "root"; + group = "root"; + capabilities = "cap_net_raw,cap_bpf,cap_perfmon+eip"; +}; +``` + +Then run `rustnet` via the wrapper path (`/run/wrappers/bin/rustnet`). + +> **Coming soon — dedicated NixOS module.** A [`programs.rustnet` +> module](https://github.com/NixOS/nixpkgs/pull/517620) is in review for +> nixpkgs. Once merged it wraps exactly the capabilities above for you, so the +> whole setup becomes: +> +> ```nix +> programs.rustnet.enable = true; +> ``` +> +> This grants `cap_net_raw`, `cap_bpf`, and `cap_perfmon` (but **not** +> `cap_net_admin`, since RustNet never needs promiscuous mode) through +> `security.wrappers` — the same pattern `programs.mtr` and +> `programs.wireshark` use — letting you run `rustnet` without sudo. + +#### Fedora (COPR - Recommended for Fedora 42+) + +The easiest way to install RustNet on Fedora is via the official COPR repository. + +```bash +# Enable the COPR repository +sudo dnf copr enable domcyrus/rustnet + +# Install rustnet +sudo dnf install rustnet + +# Run with sudo +sudo rustnet + +# Optional: Grant capabilities to run without sudo (modern kernel 5.8+) +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' /usr/bin/rustnet +rustnet +``` + +**Important:** The COPR only supports Fedora 42 and 43 due to the Rust 1.88+ requirement. CentOS and RHEL don't have recent enough Rust compilers in their repositories. For those distributions, use the [.rpm packages](#redhatfedoracentos-rpm-packages) from GitHub releases or [build from source](#building-from-source). + +#### openSUSE Tumbleweed (OBS) + +RustNet is built for openSUSE Tumbleweed (x86_64 and aarch64) via the [openSUSE Build Service](https://build.opensuse.org/package/show/home:domcyrus:rustnet/rustnet). + +```bash +sudo zypper addrepo https://download.opensuse.org/repositories/home:/domcyrus:/rustnet/openSUSE_Tumbleweed/home:domcyrus:rustnet.repo +sudo zypper refresh +sudo zypper install rustnet + +# Run with sudo +sudo rustnet + +# Optional: Grant capabilities to run without sudo (modern kernel 5.8+) +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' /usr/bin/rustnet +rustnet +``` + +#### Homebrew Installation + +**On macOS:** +```bash +brew install rustnet + +# Follow the caveats displayed after installation for permission setup +``` + +**On Linux:** +```bash +brew install rustnet + +# Grant capabilities to the Homebrew-installed binary (modern kernel 5.8+) +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' $(brew --prefix)/bin/rustnet + +# Run without sudo +rustnet +``` + +#### Static Binary (Portable - Any Linux Distribution) + +For maximum portability, static binaries are available that work on **any Linux distribution** regardless of GLIBC version. These are fully self-contained and require no system dependencies. + +```bash +# Download the static binary for your architecture: +# - rustnet-vX.Y.Z-x86_64-unknown-linux-musl.tar.gz (x86_64) +# - rustnet-vX.Y.Z-aarch64-unknown-linux-musl.tar.gz (ARM64) + +# Extract the archive +tar xzf rustnet-vX.Y.Z-x86_64-unknown-linux-musl.tar.gz + +# Move binary to PATH +sudo mv rustnet-vX.Y.Z-x86_64-unknown-linux-musl/rustnet /usr/local/bin/ + +# Grant capabilities (modern kernel 5.8+) +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' /usr/local/bin/rustnet + +# Run without sudo +rustnet +``` + +**When to use static binaries:** +- Older distributions with outdated GLIBC (e.g., CentOS 7, older Ubuntu) +- Minimal/containerized environments +- Air-gapped systems where installing dependencies is difficult +- When you want a single portable binary + +### FreeBSD Installation + +FreeBSD support is available starting from version 0.15.0. + +#### From Ports or Packages (Future) + +Once available in FreeBSD ports: +```bash +# Using pkg (binary packages) +pkg install rustnet + +# Or build from ports +cd /usr/ports/net/rustnet && make install clean +``` + +#### From GitHub Releases + +Download the FreeBSD binary from the [rustnet-bsd releases](https://github.com/domcyrus/rustnet-bsd/releases): + +```bash +# Download the appropriate package +fetch https://github.com/domcyrus/rustnet-bsd/releases/download/vX.Y.Z/rustnet-vX.Y.Z-x86_64-unknown-freebsd.tar.gz + +# Extract the archive +tar xzf rustnet-vX.Y.Z-x86_64-unknown-freebsd.tar.gz + +# Move binary to PATH +sudo mv rustnet-vX.Y.Z-x86_64-unknown-freebsd/rustnet /usr/local/bin/ + +# Make it executable +sudo chmod +x /usr/local/bin/rustnet + +# Run with sudo +sudo rustnet +``` + +#### Building from Source on FreeBSD + +```bash +# Install dependencies +pkg install rust libpcap + +# Clone the repository +git clone https://github.com/domcyrus/rustnet.git +cd rustnet + +# Build in release mode +cargo build --release + +# The executable will be in target/release/rustnet +sudo ./target/release/rustnet +``` + +#### Permission Setup for FreeBSD + +FreeBSD requires access to BPF (Berkeley Packet Filter) devices for packet capture. + +**Option 1: Run with sudo (Simplest)** +```bash +sudo rustnet +``` + +**Option 2: Add user to the bpf group (Recommended)** +```bash +# Add your user to the bpf group +sudo pw groupmod bpf -m $(whoami) + +# Log out and back in for group changes to take effect + +# Now run without sudo +rustnet +``` + +**Option 3: Change BPF device permissions (Temporary)** +```bash +# This will reset on reboot +sudo chmod o+rw /dev/bpf* + +# Now run without sudo +rustnet +``` + +**Verifying FreeBSD Permissions:** +```bash +# Check if you're in the bpf group +groups | grep bpf + +# Check BPF device permissions +ls -la /dev/bpf* + +# Test without sudo +rustnet --help +``` + +### Android (Termux) Installation + +RustNet can run on Android devices via [Termux](https://termux.dev/en/), provided the device is rooted. + +Because Android strictly controls network and process information, RustNet requires `root` access (`su`) to capture packets and identify processes. A specialized Android build is provided that statically links dependencies and disables Linux-specific features (like eBPF and Landlock) that are incompatible with Android's kernel environment. + +#### Prerequisites +1. **Rooted** Android device (e.g., via Magisk or KernelSU) +2. **Termux** installed (from F-Droid or GitHub, *not* Google Play) + +#### Installation Steps + +1. **Install required packages in Termux:** + ```bash + pkg update + pkg install tsu wget tar + ``` + +2. **Download the Android binary:** + ```bash + # Download the Android-specific static binary from GitHub Releases + wget https://github.com/domcyrus/rustnet/releases/download/vX.Y.Z/rustnet-vX.Y.Z-aarch64-linux-android-musl.tar.gz + ``` + +3. **Extract and install:** + ```bash + tar xzf rustnet-vX.Y.Z-aarch64-linux-android-musl.tar.gz + + # Move it to a directory in your PATH + mv rustnet-vX.Y.Z-aarch64-linux-android-musl/rustnet $PREFIX/bin/ + chmod +x $PREFIX/bin/rustnet + ``` + +4. **Run RustNet as root:** + ```bash + # You must run RustNet with root privileges for it to function on Android + sudo rustnet + ``` + *Note: On first run, your root manager (e.g., Magisk) will prompt you to grant Superuser access to Termux.* + +## Install via Cargo + +```bash +# Install directly from crates.io +cargo install rustnet-monitor + +# The binary will be installed to ~/.cargo/bin/rustnet +# Make sure ~/.cargo/bin is in your PATH +``` + +After installation, see the [Permissions Setup](#permissions-setup) section to configure permissions. + +## Building from Source + +### Prerequisites + +- Rust 2024 edition or later (install from [rustup.rs](https://rustup.rs/)) +- Platform-specific dependencies: + - **Linux (Debian/Ubuntu)**: + ```bash + sudo apt-get install build-essential pkg-config libpcap-dev libelf-dev zlib1g-dev clang llvm + ``` + - **Linux (RedHat/CentOS/Fedora)**: + ```bash + sudo yum install make pkgconfig libpcap-devel elfutils-libelf-devel zlib-devel clang llvm + ``` + - **macOS**: Install Xcode Command Line Tools: `xcode-select --install` + - **FreeBSD**: `pkg install rust libpcap` + - **Windows**: Install Npcap and Npcap SDK (see [Windows Build Setup](#windows-build-setup) below) + +### Basic Build + +```bash +# Clone the repository +git clone https://github.com/domcyrus/rustnet.git +cd rustnet + +# Build in release mode (eBPF is enabled by default on Linux) +cargo build --release + +# To build WITHOUT eBPF support (procfs-only mode on Linux) +cargo build --release --no-default-features + +# The executable will be in target/release/rustnet +``` + +To build without eBPF (procfs-only mode), use `cargo build --release --no-default-features`. + +### Windows Build Setup + +Building RustNet on Windows requires the Npcap SDK and proper environment configuration: + +#### Build Requirements + +1. **Download and Install Npcap SDK**: + - Download the Npcap SDK from https://npcap.com/dist/ + - Extract the SDK to a directory (e.g., `C:\npcap-sdk`) + +2. **Set Environment Variables**: + - Set the `LIB` environment variable to include the SDK's library path: + ```cmd + set LIB=%LIB%;C:\npcap-sdk\Lib\x64 + ``` + - For PowerShell: + ```powershell + $env:LIB = "$env:LIB;C:\npcap-sdk\Lib\x64" + ``` + - For permanent setup, add this to your system environment variables + +3. **Build RustNet**: + ```cmd + cargo build --release + ``` + +#### Runtime Requirements + +1. **Install Npcap Runtime**: + - Download the Npcap installer from https://npcap.com/dist/ + - Run the installer and **select "WinPcap API compatible mode"** during installation + - This ensures compatibility with the packet capture library + +2. **Run RustNet**: + ```cmd + rustnet.exe + ``` + +**Note**: Depending on your Npcap installation settings, you may or may not need Administrator privileges. If you didn't select the option to restrict packet capture to administrators during Npcap installation, RustNet can run with normal user privileges. + +## Using Docker + +RustNet is available as a Docker container from GitHub Container Registry. The +image runs as a **non-root** user by default, with `CAP_NET_RAW` baked into the +binary as a file capability — so basic packet capture works with no extra flags. + +```bash +# Pull the latest image +docker pull ghcr.io/domcyrus/rustnet:latest + +# Or pull a specific version +docker pull ghcr.io/domcyrus/rustnet:0.7.0 + +# Option A — basic monitoring (non-root, recommended) +# Captures packets via the built-in CAP_NET_RAW file capability. Process +# attribution uses /proc (eBPF disabled). No --cap-add needed. +docker run --rm -it --net=host ghcr.io/domcyrus/rustnet:latest + +# Option B — full eBPF process attribution (runs as root + extra caps) +# eBPF needs CAP_BPF + CAP_PERFMON. A non-root user can't make these effective +# from --cap-add alone, so enable them by also running as root. +docker run --rm -it --user root \ + --cap-add=NET_RAW --cap-add=BPF --cap-add=PERFMON --net=host \ + ghcr.io/domcyrus/rustnet:latest + +# Run with a specific interface (either option; -i flag at the end) +docker run --rm -it --net=host ghcr.io/domcyrus/rustnet:latest -i eth0 + +# Alternative: privileged mode (simplest, least secure) +docker run --rm -it --privileged --net=host \ + ghcr.io/domcyrus/rustnet:latest + +# View available options +docker run --rm ghcr.io/domcyrus/rustnet:latest --help +``` + +**Note:** Basic capture (Option A) needs no special flags — the image carries +`CAP_NET_RAW` as a file capability and runs non-root. eBPF-based process +attribution (Option B) additionally needs `CAP_BPF` + `CAP_PERFMON`; because +those can't be granted to a non-root user via file capabilities, enable them by +running as root (`--user root`) with the matching `--cap-add` flags. Either way, +rustnet drops these capabilities and sandboxes itself immediately after startup. +Host networking (`--net=host`) is recommended for monitoring all interfaces. + +## Permissions Setup + +RustNet requires elevated privileges to capture network packets because accessing network interfaces for packet capture is a privileged operation on all modern operating systems. This section explains how to properly grant these permissions on different platforms. + +> ### **Security Advantage: Read-Only Network Access on Linux** +> +> **RustNet uses read-only packet capture without promiscuous mode on all platforms.** This means: +> +> **Linux:** Requires only **`CAP_NET_RAW`** capability - **NOT** full root or `CAP_NET_ADMIN` +> **Principle of Least Privilege:** Minimal permissions needed for packet capture +> **No Promiscuous Mode:** Only captures packets to/from the host (not all network traffic) +> **Read-Only:** Cannot modify or inject packets +> **Enhanced Security:** Reduced attack surface compared to full root access +> +> **macOS Note:** PKTAP (for process metadata) requires root privileges, but you can run without sudo using the `lsof` fallback for basic packet capture. + +### Why Permissions Are Required + +Network packet capture requires access to: + +- **Raw sockets** for low-level network access (read-only, non-promiscuous mode) +- **Network interfaces** for packet capture +- **BPF (Berkeley Packet Filter) devices** on macOS/BSD systems +- **Network namespaces** on some Linux configurations + +These capabilities are restricted to prevent malicious software from intercepting network traffic. + +### macOS Permission Setup + +On macOS, packet capture requires access to BPF (Berkeley Packet Filter) devices located at `/dev/bpf*`. + +**Note:** macOS PKTAP (for extracting process metadata from packets) requires **root/sudo** privileges. Without sudo, RustNet uses `lsof` as a fallback for process detection (slower but works without root). + +#### Option 1: Run with sudo (Simplest) + +```bash +# Build and run with sudo +cargo build --release +sudo ./target/release/rustnet +``` + +#### Option 2: BPF Group Access (Recommended) + +Add your user to the `access_bpf` group for passwordless packet capture: + +**Using Wireshark's ChmodBPF (For basic packet capture):** + +```bash +# Install Wireshark's BPF permission helper +brew install --cask wireshark-chmodbpf + +# Log out and back in for group changes to take effect +# Then run rustnet without sudo: +rustnet # Uses lsof for process detection (slower) + +# For PKTAP support with process metadata from packet headers, use sudo: +sudo rustnet # Uses PKTAP for faster process detection +``` + +**Note**: `wireshark-chmodbpf` grants access to `/dev/bpf*` for packet capture, but **PKTAP** is a separate privileged kernel interface that requires root privileges regardless of BPF permissions. The TUI will display which detection method is active ("pktap" with sudo, or "lsof" without). + +**Manual BPF Group Setup:** + +```bash +# Create the access_bpf group (if it doesn't exist) +sudo dseditgroup -o create access_bpf + +# Add your user to the group +sudo dseditgroup -o edit -a $USER -t user access_bpf + +# Set permissions on BPF devices (this needs to be done after each reboot) +sudo chmod g+rw /dev/bpf* +sudo chgrp access_bpf /dev/bpf* + +# Log out and back in for group membership to take effect +``` + +#### Option 3: Homebrew Installation + +If installed via Homebrew, the formula will provide detailed setup instructions: + +```bash +brew install rustnet +# Follow the caveats displayed after installation +``` + +### Linux Permission Setup (Read-Only Access - No Root Required!) + +**Linux Advantage:** RustNet requires **only `CAP_NET_RAW`** for packet capture - far less than full root access! + +On Linux, packet capture requires only the `CAP_NET_RAW` capability for read-only, non-promiscuous packet capture. For eBPF-enhanced process tracking, additional capabilities (`CAP_BPF` and `CAP_PERFMON`) are needed, but **`CAP_NET_ADMIN` is NOT required**. + +#### Option 1: Run with sudo (Simplest) + +```bash +# Build and run with sudo +cargo build --release +sudo ./target/release/rustnet +``` + +#### Option 2: Grant Capabilities (Recommended) + +Grant specific network capabilities to the binary without full root privileges: + +**For source builds:** + +```bash +# Build the binary first +cargo build --release + +# Grant capabilities to the binary (modern kernel 5.8+, with eBPF support) +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' ./target/release/rustnet + +# Now run without sudo +./target/release/rustnet +``` + +**For cargo-installed binaries:** + +```bash +# If installed via cargo install rustnet-monitor (modern kernel 5.8+) +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' ~/.cargo/bin/rustnet + +# Now run without sudo +rustnet +``` + +**For eBPF-enabled builds (enhanced Linux performance - enabled by default):** + +eBPF is enabled by default on Linux and provides lower-overhead process identification using kernel probes: + +```bash +# Build in release mode (eBPF is enabled by default) +cargo build --release + +# Modern Linux (5.8+) - works with just these three capabilities: +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' ./target/release/rustnet +./target/release/rustnet + +# Packet capture only - eBPF falls back to procfs: +sudo setcap 'cap_net_raw+eip' ./target/release/rustnet +./target/release/rustnet + +# Check TUI Statistics panel - should show "Process Detection: eBPF + procfs" +``` + +**Capability requirements:** + +**Base capability (always required):** +- `CAP_NET_RAW` - Raw socket access for read-only packet capture (non-promiscuous mode) + +**eBPF-specific capabilities (Linux 5.8+):** +- `CAP_BPF` - BPF program loading and map operations +- `CAP_PERFMON` - Performance monitoring and tracing operations + +**Legacy Linux (pre-5.8):** +Older kernels require broad `CAP_SYS_ADMIN` for eBPF operations. RustNet does +not recommend or automatically grant it. Use `CAP_NET_RAW` only and let process +attribution fall back to procfs unless you explicitly accept the extra risk. + +**Note:** CAP_NET_ADMIN is NOT required. RustNet uses read-only packet capture without promiscuous mode. + +**Fallback behavior**: If eBPF cannot load (e.g., insufficient capabilities, incompatible kernel), the application automatically uses procfs-only mode. The TUI Statistics panel displays which detection method is active: +- `Process Detection: eBPF + procfs` - eBPF successfully loaded +- `Process Detection: procfs` - Using procfs fallback + +**Note:** eBPF is enabled by default on Linux builds and may have limitations with process name display. See [ARCHITECTURE.md](ARCHITECTURE.md) for details on eBPF implementation. To build without eBPF, use `cargo build --release --no-default-features`. + +**For system-wide installation:** + +```bash +# If installed via package manager or copied to /usr/local/bin (modern kernel 5.8+) +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' /usr/local/bin/rustnet +rustnet +``` + +### Windows Permission Setup + +Windows support is currently limited, but when available: + +- RustNet will require **Administrator privileges** +- Must install **WinPcap** or **Npcap** for packet capture +- Run Command Prompt or PowerShell "As Administrator" + +### Verifying Permissions + +To verify that permissions are set up correctly: + +#### macOS + +```bash +# Check BPF device permissions +ls -la /dev/bpf* + +# Check group membership +groups | grep access_bpf + +# Test without sudo +rustnet --help +``` + +#### Linux + +```bash +# Check capabilities on the binary +# For source builds: +getcap ./target/release/rustnet + +# For cargo-installed binaries: +getcap ~/.cargo/bin/rustnet + +# For system-wide installations: +getcap $(which rustnet) + +# eBPF enabled: Should show cap_net_raw,cap_bpf,cap_perfmon+eip +# Packet capture only: Should show cap_net_raw=eip + +# Test without sudo +rustnet --help +``` + +## GeoIP Databases (Optional) + +RustNet supports GeoIP lookups to show country codes, city names, and ASN information for remote IPs. To enable this, install the [GeoLite2](https://dev.maxmind.com/geoip/geolite2-free-geolocation-data) databases using MaxMind's `geoipupdate` tool (requires a free [MaxMind account](https://www.maxmind.com/en/geolite2/signup)). + +**Available databases:** + +| Database | Provides | Flag | +|---|---|---| +| `GeoLite2-Country.mmdb` | Country code and name | *(auto-discovered)* | +| `GeoLite2-ASN.mmdb` | ASN number and organization | *(auto-discovered)* | +| `GeoLite2-City.mmdb` | City name, postal code, **and** country | *(auto-discovered)* | + +> **Tip:** `GeoLite2-City` is a superset of `GeoLite2-Country`. If you install the City database you do not need to also install the Country database. + +### Configuring which databases to download + +In your `GeoIP.conf`, set `EditionIDs` to include the databases you want: + +``` +# Country + ASN only: +EditionIDs GeoLite2-Country GeoLite2-ASN + +# City + ASN (City includes country data): +EditionIDs GeoLite2-City GeoLite2-ASN + +# All three: +EditionIDs GeoLite2-Country GeoLite2-ASN GeoLite2-City +``` + +### macOS (Homebrew) + +```bash +brew install geoipupdate +# Edit the config with your MaxMind account credentials and EditionIDs: +# $(brew --prefix)/etc/GeoIP.conf +geoipupdate +``` + +Databases are installed to `$(brew --prefix)/share/GeoIP/`. + +### Ubuntu/Debian + +```bash +sudo apt-get install geoipupdate +# Edit /etc/GeoIP.conf with your MaxMind account credentials and EditionIDs +sudo geoipupdate +``` + +Databases are installed to `/usr/share/GeoIP/`. + +### Fedora/RHEL + +```bash +sudo dnf install geoipupdate +# Edit /etc/GeoIP.conf with your MaxMind account credentials and EditionIDs +sudo geoipupdate +``` + +Databases are installed to `/usr/share/GeoIP/`. + +### Arch Linux + +```bash +sudo pacman -S geoipupdate +# Edit /etc/GeoIP.conf with your MaxMind account credentials and EditionIDs +sudo geoipupdate +``` + +Databases are installed to `/usr/share/GeoIP/`. + +### FreeBSD + +```bash +pkg install geoipupdate +# Edit /usr/local/etc/GeoIP.conf with your MaxMind account credentials and EditionIDs +sudo geoipupdate +``` + +Databases are installed to `/usr/local/share/GeoIP/`. + +### Manual Specification + +If your databases are in a non-standard location, specify them directly: + +```bash +# Country + ASN: +rustnet --geoip-country /path/to/GeoLite2-Country.mmdb --geoip-asn /path/to/GeoLite2-ASN.mmdb + +# City + ASN (City includes country data): +rustnet --geoip-city /path/to/GeoLite2-City.mmdb --geoip-asn /path/to/GeoLite2-ASN.mmdb +``` + +RustNet auto-discovers databases from standard locations. Run `rustnet --help` to see the full search path list. + +## Troubleshooting + +### Common Installation Issues + +#### Permission Denied Errors + +**On macOS:** + +- Ensure you're in the `access_bpf` group: `groups | grep access_bpf` +- Check BPF device permissions: `ls -la /dev/bpf0` +- Try running with sudo to confirm it's a permission issue +- Log out and back in after group changes + +**On Linux:** + +- Check if capabilities are set: `getcap $(which rustnet)` or `getcap ~/.cargo/bin/rustnet` +- Verify libpcap is installed: `ldconfig -p | grep pcap` +- Try running with sudo to confirm it's a permission issue: `sudo $(which rustnet)` + +#### No Suitable Capture Interfaces Found + +- Check available interfaces: `ip link show` (Linux) or `ifconfig` (macOS) +- Try specifying an interface explicitly: `rustnet -i eth0` +- Ensure the interface is up and has an IP address +- Some virtual interfaces may not support packet capture + +#### Operation Not Permitted (with capabilities set) + +- Capabilities may have been removed by system updates +- Re-apply capabilities (modern): `sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' $(which rustnet)` +- Some filesystems don't support extended attributes (capabilities) +- Try copying the binary to a different filesystem (e.g., from NFS to local disk) + +#### eBPF Unavailable Despite Capabilities Being Set + +If RustNet shows `Process Detection: procfs` with a degradation message even +after running `setcap`, work through the following checks. The TUI surfaces +the actual reason on the second status line — use it to jump to the right +section below. + +**1. `file caps ignored: binary on a nosuid mount`** + +The kernel silently ignores file capabilities for binaries that live on a +filesystem mounted with the `nosuid` option. Common culprits: `/home` on +hardened distros, `/tmp`, removable media, some bind-mounts inside +containers. + +```bash +# Find the mount that holds the binary and check its options +findmnt -T $(realpath $(which rustnet)) -o TARGET,OPTIONS +# If the OPTIONS column contains "nosuid", caps will not work. + +# Fix: install or copy the binary to a mount without nosuid +sudo install -m 0755 $(which rustnet) /usr/local/bin/rustnet +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' /usr/local/bin/rustnet +/usr/local/bin/rustnet +``` + +**2. `BPF denied (check perf_event_paranoid / AppArmor / unprivileged_bpf_disabled)`** + +Caps were granted, but the kernel returned `EPERM` or `EACCES`. Three layers +can do that — check them in this order: + +```bash +# 2a. perf_event_paranoid (THE most common cause on Debian). +# Debian 13 ships with kernel.perf_event_paranoid=3, which blocks +# perf_event_open(2) — and therefore kprobe attach — for non-root +# users *even with CAP_PERFMON*. Upstream kernels only go up to 2, +# where CAP_PERFMON correctly bypasses the restriction. +# +# Ubuntu uses a different patch (paranoid=4) that was updated in +# late 2025 to honor CAP_PERFMON, so on recent Ubuntu kernels +# (Jammy 5.15.0-165+, Noble 6.8.0-91+, Plucky 6.14.0-37+, +# Questing 6.17.0-14+, Resolute 6.18.0-8+) `setcap` alone is +# enough — no sysctl change required. Debian's equivalent patch +# (bug #994044) was never updated and was archived in 2025 +# without a fix, so Debian users still need the workaround below. +sysctl kernel.perf_event_paranoid +# If the value is 3 (Debian) or 4 on an old Ubuntu kernel, drop it to 2: +sudo sysctl kernel.perf_event_paranoid=2 +# Make it persist across reboot: +echo 'kernel.perf_event_paranoid = 2' | \ + sudo tee /etc/sysctl.d/99-rustnet.conf + +# 2b. AppArmor confining rustnet (Debian/Ubuntu install AppArmor by default). +sudo aa-status | grep rustnet +# If listed, either disable the profile or add a rule allowing capability bpf, +# capability perfmon, and the bpf() syscall for this binary. + +# 2c. unprivileged_bpf_disabled (Debian sets =2; file caps should bypass). +sysctl kernel.unprivileged_bpf_disabled + +# Confirm caps actually became effective at exec: +grep ^Cap /proc/$(pgrep -n rustnet)/status +# CapEff must include CAP_BPF (bit 39) and CAP_PERFMON (bit 38). +``` + +**3. `kprobe attach failed: `** + +The kernel is missing the symbol the eBPF probe wants to attach to. This is +usually a kernel-config issue (e.g. CONFIG_IPV6 disabled, CONFIG_KPROBES +off, or the symbol was inlined). RustNet currently attaches to +`tcp_connect`, `inet_csk_accept`, `udp_sendmsg`, `tcp_v6_connect`, +`udpv6_sendmsg`, `ping_v4_sendmsg`, and `ping_v6_sendmsg`. + +```bash +# Check whether the failing symbol exists in the running kernel: +sudo grep '' /proc/kallsyms +``` + +If the symbol is genuinely missing, eBPF process detection will not work +on this kernel build; procfs fallback continues to function. + +**4. `kernel BTF unavailable`** + +CO-RE relocations require `/sys/kernel/btf/vmlinux`. On stripped-down +kernels (some embedded / minimal cloud images) this file is absent. + +```bash +ls /sys/kernel/btf/vmlinux +# If missing: install the kernel-debuginfo / linux-image package matching +# your running kernel, or rebuild the kernel with CONFIG_DEBUG_INFO_BTF=y. +``` + +**5. Inside Docker / Podman** + +Even with file capabilities, the *bounding* set inside the container must +contain `CAP_BPF` and `CAP_PERFMON`, or they get masked at exec: + +```bash +docker run --cap-add=NET_RAW --cap-add=BPF --cap-add=PERFMON \ + --net=host --pid=host rustnet +# Optionally, if your container's seccomp profile blocks bpf(2): +# --security-opt seccomp=unconfined +# Or if AppArmor mediates BPF: +# --security-opt apparmor=unconfined +``` + +**6. `eBPF load failed: ...`** + +The catch-all branch carries the raw libbpf error text. Re-run with +`RUST_LOG=debug rustnet 2>&1 | tee rustnet.log` and inspect the full +chain — it usually contains an `errno` name (`EPERM`, `EACCES`, `ENOSPC` +for memlock, etc.) that points at the root cause. + +#### Windows: Npcap Not Found + +- Ensure Npcap is installed from https://npcap.com/dist/ +- During Npcap installation, select **"WinPcap API compatible mode"** +- Verify Npcap service is running: `sc query npcap` +- Try reinstalling Npcap with administrator privileges + +#### Build Errors + +**Windows - Npcap SDK not found:** +- Ensure the `LIB` environment variable includes the Npcap SDK path +- Check that the SDK is extracted to a directory without spaces +- Use the correct architecture (x64 vs x86) for your Rust toolchain + +**Linux build fails:** +```bash +# Install all required dependencies +# Debian/Ubuntu +sudo apt-get install build-essential pkg-config libpcap-dev libelf-dev zlib1g-dev clang llvm + +# RedHat/CentOS/Fedora +sudo yum install make pkgconfig libpcap-devel elfutils-libelf-devel zlib-devel clang llvm +``` + +#### Windows: Graphs Display Incorrectly in PowerShell + +If graphs and sparklines appear corrupted (showing question marks or garbled characters) in PowerShell, this is a **font issue**, not a RustNet bug. The default console fonts (Consolas, Lucida Console) lack support for Unicode Braille characters used for graph rendering. + +**Solution:** Install a font with Unicode Braille support: + +1. Download and install [Iosevka](https://typeof.net/Iosevka/) or any [Nerd Font](https://www.nerdfonts.com/) +2. Open PowerShell Properties (right-click title bar → Properties) +3. Select the installed font in the Font tab +4. Restart PowerShell + +**Alternative:** Use [Windows Terminal](https://aka.ms/terminal) which has better Unicode support out of the box. + +See also: [ratatui#457](https://github.com/ratatui/ratatui/issues/457), [gtop#21](https://github.com/aksakalli/gtop/issues/21) + +#### macOS: High Terminal CPU or Gappy Graphs (iTerm2) + +RustNet's graphs are drawn with Unicode Braille characters. The classic +macOS monospace fonts (Monaco, Menlo) have no Braille glyphs, so iTerm2 +renders every graph cell through font fallback. This is slow (iTerm2 can +use 10-20% CPU just displaying the graphs) and the fallback glyphs often +leave visible gaps in the waves. + +**Solution:** Give iTerm2 a font with Braille coverage for non-ASCII text: + +1. Install a [Nerd Font](https://www.nerdfonts.com/), e.g.: + + ```bash + brew install --cask font-jetbrains-mono-nerd-font + ``` + +2. In iTerm2: **Settings → Profiles → Text**, enable **"Use a different + font for non-ASCII text"** and select the Nerd Font. Your regular + text keeps its current font; only symbols and graph glyphs use the + new one. +3. Optional: **Settings → General → Preferences → "Maximize throughput"** + caps iTerm2's redraw rate at 30 fps, which further reduces CPU with + frequently updating TUIs. + +**Alternative:** Use a GPU-accelerated terminal such as +[WezTerm](https://wezterm.org/), [Ghostty](https://ghostty.org/), or +[kitty](https://sw.kovidgoyal.net/kitty/). These render RustNet with +much lower CPU than iTerm2, and WezTerm ships JetBrains Mono with symbol +fallback built in, so the graphs render correctly with zero +configuration. + +### Getting Help + +If you encounter issues not covered here: + +1. Enable debug logging: `rustnet --log-level debug` +2. Check the log file in the `logs/` directory +3. Open an issue on [GitHub](https://github.com/domcyrus/rustnet/issues) with: + - Your operating system and version + - Installation method used + - Error messages from logs + - Output of permission verification commands + +### Security Best Practices + +1. **Use capabilities instead of sudo** when possible (Linux) +2. **Use group-based access** instead of running as root (macOS) +3. **Regularly audit** which users have packet capture privileges +4. **Consider network segmentation** if running on production systems +5. **Monitor log files** for unauthorized usage +6. **Remove capabilities** when RustNet is no longer needed: + + ```bash + # Linux: Remove capabilities + sudo setcap -r /path/to/rustnet + + # macOS: Remove from group + sudo dseditgroup -o edit -d $USER -t user access_bpf + ``` + +### Integration with System Monitoring + +For production environments, consider: + +- **Audit logging** of packet capture access +- **Network monitoring policies** and compliance requirements +- **User access reviews** for privileged network access +- **Automated capability management** in configuration management systems + +This permissions setup ensures RustNet can capture packets while maintaining security best practices and principle of least privilege. diff --git a/INSTALL.zh-CN.md b/INSTALL.zh-CN.md new file mode 100644 index 0000000..20555dc --- /dev/null +++ b/INSTALL.zh-CN.md @@ -0,0 +1,1123 @@ +

English | 简体中文

+ +# 安装指南 + +本文档涵盖 RustNet 在各平台上的所有安装方法。 + +> **提示:** 想一眼看清哪些发行版打包了 RustNet、各自分发的版本号是多少,请查看 [Repology 上的 RustNet 页面](https://repology.org/project/rustnet/versions)。 + +## 目录 + +- [从发布包安装](#installing-from-release-packages) + - [macOS DMG 安装](#macos-dmg-installation) + - [Windows MSI 安装](#windows-msi-installation) + - [Windows Chocolatey 安装](#windows-chocolatey-installation) + - [Linux 包安装](#linux-package-installation) + - [FreeBSD 安装](#freebsd-installation) + - [Android(Termux)安装](#android-termux-installation) +- [通过 Cargo 安装](#install-via-cargo) +- [从源码构建](#building-from-source) +- [使用 Docker](#using-docker) +- [前置要求](#prerequisites) +- [权限配置](#permissions-setup) +- [GeoIP 数据库(可选)](#geoip-databases-optional) +- [故障排查](#troubleshooting) + +## 从发布包安装 + +预构建包可在每个版本的 [GitHub Releases](https://github.com/domcyrus/rustnet/releases) 页面下载。 + +### macOS DMG 安装 + +> **更喜欢 Homebrew?** 如果你已安装 Homebrew,使用 `brew install` 更简单,且无需绕过 Gatekeeper 步骤。参见 [Homebrew 安装](#homebrew-installation)了解详情。 + +1. **下载**适合你架构的 DMG: + - Apple Silicon Mac(M1/M2/M3)使用 `Rustnet_macOS_AppleSilicon.dmg` + - Intel Mac 使用 `Rustnet_macOS_Intel.dmg` + +2. **打开 DMG** 并将 Rustnet.app 拖拽到 Applications 文件夹 + +3. **绕过 Gatekeeper**(针对未签名构建): + - 首次尝试打开 RustNet 时,macOS 会阻止它,因为应用未签名 + - 前往 **系统设置 → 隐私与安全性** + - 向下滚动找到 RustNet 被阻止的消息 + - 点击 **"仍要打开"** 以允许应用运行 + - 再次启动应用时可能需要确认此选择 + +4. **运行 RustNet**: + - 双击 Rustnet.app 以在带 sudo 的终端窗口中启动 + - 或从命令行运行:`sudo /Applications/Rustnet.app/Contents/MacOS/rustnet` + +5. **可选:创建 shell 访问的符号链接**: + ```bash + # 创建符号链接,以便在任何位置运行 'rustnet' + sudo ln -s /Applications/Rustnet.app/Contents/MacOS/rustnet /usr/local/bin/rustnet + + # 现在你可以从任何终端运行: + sudo rustnet + ``` + +6. **可选:配置 BPF 权限**(以避免需要 sudo): + - 安装 Wireshark 的 BPF 权限助手:`brew install --cask wireshark-chmodbpf` + - 注销并重新登录以使组变更生效 + - 详细说明参见[权限配置](#permissions-setup)章节 + +### Windows MSI 安装 + +1. **安装 Npcap Runtime**(包捕获必需): + - 从 https://npcap.com/dist/ 下载 + - 运行安装程序并选择 **"WinPcap API compatible mode"** + +2. **下载并安装**适合的 MSI 包: + - 64 位 Windows 使用 `Rustnet_Windows_64-bit.msi` + - 32 位 Windows 使用 `Rustnet_Windows_32-bit.msi` + +3. **运行安装程序**并按照安装向导操作 + +4. **运行 RustNet**: + - 打开命令提示符或 PowerShell + - 运行:`rustnet.exe` + - 如果未安装 Npcap 或未处于 WinPcap 兼容模式,RustNet 会显示一条有用的错误消息及安装说明 + - 注意:根据你的 Npcap 安装设置,你可能需要或不需要 Administrator 特权 + +### Windows Chocolatey 安装 + +在 Windows 上安装 RustNet 最简单的方式是通过 [Chocolatey](https://community.chocolatey.org/packages/rustnet): + +```powershell +# 在 Administrator PowerShell 中运行 +choco install rustnet +``` + +**注意:** 你仍需要单独安装 [Npcap](https://npcap.com),并启用 "WinPcap API compatible mode"。 + +### Linux 包安装 + +#### Ubuntu PPA(推荐用于 Ubuntu 25.10 Questing 和 26.04 LTS Resolute) + +在 Ubuntu 上安装 RustNet 最简单的方式是通过官方 PPA。该 PPA 为以下 Ubuntu 系列发布构建: + +- Ubuntu 25.10(Questing Quokka) +- Ubuntu 26.04 LTS(Resolute Raccoon) + +```bash +# 添加 RustNet PPA +sudo add-apt-repository ppa:domcyrus/rustnet + +# 更新包列表 +sudo apt update + +# 安装 rustnet +sudo apt install rustnet + +# 使用 sudo 运行 +sudo rustnet + +# 可选:授予 Linux capabilities 以无需 sudo 运行(现代内核 5.8+) +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' /usr/bin/rustnet +rustnet +``` + +**重要:** 该 PPA 仅支持上述两个系列(Ubuntu 25.10 Questing 和 26.04 LTS Resolute),因为构建需要 Rust 1.88+(项目中使用了 let-chains)。早期 Ubuntu 版本的仓库中没有足够新的 `rustc`。对于旧版 Ubuntu,请使用 GitHub releases 中的 [.deb 包](#debianubuntu-deb-packages)或[从源码构建](#building-from-source)。 + +#### Debian/Ubuntu(.deb 包) + +用于手动安装或非 Ubuntu 的 Debian 系发行版: + +```bash +# 下载适合你架构的包: +# - Rustnet_LinuxDEB_amd64.deb(x86_64) +# - Rustnet_LinuxDEB_arm64.deb(ARM64) +# - Rustnet_LinuxDEB_armhf.deb(ARMv7) + +# 安装包(Linux capabilities 会自动配置) +sudo dpkg -i Rustnet_LinuxDEB_amd64.deb + +# 如有需要安装依赖 +sudo apt-get install -f + +# 无需 sudo 运行(post-install 脚本已设置 Linux capabilities) +rustnet + +# 验证 Linux capabilities +getcap /usr/bin/rustnet +``` + +**注意:** .deb 包通过 post-install 脚本自动设置 Linux capabilities,因此你可以无需 sudo 运行 RustNet。 + +#### RedHat/Fedora/CentOS(.rpm 包) + +用于手动安装或不使用 COPR 的发行版: + +```bash +# 下载适合你架构的包: +# - Rustnet_LinuxRPM_x86_64.rpm +# - Rustnet_LinuxRPM_aarch64.rpm + +# 安装包(Linux capabilities 会自动配置) +sudo rpm -i Rustnet_LinuxRPM_x86_64.rpm +# 或使用 dnf/yum: +sudo dnf install Rustnet_LinuxRPM_x86_64.rpm + +# 无需 sudo 运行(post-install 脚本已设置 Linux capabilities) +rustnet + +# 验证 Linux capabilities +getcap /usr/bin/rustnet +``` + +**注意:** .rpm 包通过 post-install 脚本自动设置 Linux capabilities,因此你可以无需 sudo 运行 RustNet。 + +#### Arch Linux + +该包已包含在 Arch Linux Extra 仓库中([链接](https://archlinux.org/packages/extra/x86_64/rustnet/))。可使用 pacman 安装: +```bash +sudo pacman -S rustnet +``` + +此外,还有两个 AUR 包可用: +- [`rustnet-bin`](https://aur.archlinux.org/packages/rustnet-bin) —— 来自 GitHub Releases 的预编译二进制文件 +- [`rustnet-git`](https://aur.archlinux.org/packages/rustnet-git) —— 从源码构建并使用最新提交(由 [@DeepChirp](https://github.com/DeepChirp) 维护) + +使用你喜欢的 AUR 助手安装: +```bash +# 来自 GitHub Releases 的预编译二进制文件 +yay -S rustnet-bin + +# 或使用最新提交的源码构建 +yay -S rustnet-git +``` + +#### Nix / NixOS + +RustNet 已收录在 [nixpkgs](https://search.nixos.org/packages?query=rustnet) 中,**stable** 通道(以及 `nixpkgs-unstable`)均已提供。 + +**在不安装的情况下试用(临时 shell):** + +```bash +nix-shell -p rustnet +# 然后在 shell 中执行: +sudo rustnet +``` + +**在 NixOS 上持久安装** —— 在 `/etc/nixos/configuration.nix` 中添加: + +```nix +environment.systemPackages = with pkgs; [ + rustnet +]; +``` + +然后运行 `sudo nixos-rebuild switch`。 + +**关于权限:** NixOS 的 `/nix/store` 是只读的,因此对二进制文件执行 `sudo setcap` 不会在系统重建后保留。最简单的方式是 `sudo rustnet`。如果希望无需 sudo 运行,可以定义一个携带相应 Linux capabilities 的 NixOS [security.wrappers](https://search.nixos.org/options?channel=unstable&query=security.wrappers) 项: + +```nix +security.wrappers.rustnet = { + source = "${pkgs.rustnet}/bin/rustnet"; + owner = "root"; + group = "root"; + capabilities = "cap_net_raw,cap_bpf,cap_perfmon+eip"; +}; +``` + +然后通过 wrapper 路径执行 `rustnet`(`/run/wrappers/bin/rustnet`)。 + +> **即将推出 —— 专用 NixOS 模块。** nixpkgs 正在评审一个 +> [`programs.rustnet` 模块](https://github.com/NixOS/nixpkgs/pull/517620)。 +> 合并后它会自动为你封装上述 capabilities,整个配置即可简化为: +> +> ```nix +> programs.rustnet.enable = true; +> ``` +> +> 该模块通过 `security.wrappers` 授予 `cap_net_raw`、`cap_bpf` 和 +> `cap_perfmon`(但**不**授予 `cap_net_admin`,因为 RustNet 从不需要混杂模式), +> 采用与 `programs.mtr`、`programs.wireshark` 相同的模式,让你无需 sudo 即可运行 +> `rustnet`。 + +#### Fedora(COPR - 推荐用于 Fedora 42+) + +在 Fedora 上安装 RustNet 最简单的方式是通过官方 COPR 仓库。 + +```bash +# 启用 COPR 仓库 +sudo dnf copr enable domcyrus/rustnet + +# 安装 rustnet +sudo dnf install rustnet + +# 使用 sudo 运行 +sudo rustnet + +# 可选:授予 Linux capabilities 以无需 sudo 运行(现代内核 5.8+) +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' /usr/bin/rustnet +rustnet +``` + +**重要:** 由于 Rust 1.88+ 的要求,COPR 仅支持 Fedora 42 和 43。CentOS 和 RHEL 的仓库中没有足够新的 Rust 编译器。对于这些发行版,请使用 GitHub releases 中的 [.rpm 包](#redhatfedoracentos-rpm-packages)或[从源码构建](#building-from-source)。 + +#### openSUSE Tumbleweed(OBS) + +RustNet 通过 [openSUSE Build Service](https://build.opensuse.org/package/show/home:domcyrus:rustnet/rustnet) 为 openSUSE Tumbleweed(x86_64 和 aarch64)构建。 + +```bash +sudo zypper addrepo https://download.opensuse.org/repositories/home:/domcyrus:/rustnet/openSUSE_Tumbleweed/home:domcyrus:rustnet.repo +sudo zypper refresh +sudo zypper install rustnet + +# 使用 sudo 运行 +sudo rustnet + +# 可选:授予 Linux capabilities 以无需 sudo 运行(现代内核 5.8+) +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' /usr/bin/rustnet +rustnet +``` + +#### Homebrew 安装 + +**在 macOS 上:** +```bash +brew install rustnet + +# 按照安装后显示的提示进行权限配置 +``` + +**在 Linux 上:** +```bash +brew install rustnet + +# 为 Homebrew 安装的二进制文件授予 Linux capabilities(现代内核 5.8+) +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' $(brew --prefix)/bin/rustnet + +# 无需 sudo 运行 +rustnet +``` + +#### 静态二进制文件(可移植 - 任意 Linux 发行版) + +为获得最大可移植性,静态二进制文件可在**任意 Linux 发行版**上运行,不受 GLIBC 版本限制。它们完全自包含,不需要任何系统依赖。 + +```bash +# 下载适合你架构的静态二进制文件: +# - rustnet-vX.Y.Z-x86_64-unknown-linux-musl.tar.gz(x86_64) +# - rustnet-vX.Y.Z-aarch64-unknown-linux-musl.tar.gz(ARM64) + +# 解压归档 +tar xzf rustnet-vX.Y.Z-x86_64-unknown-linux-musl.tar.gz + +# 将二进制文件移动到 PATH +sudo mv rustnet-vX.Y.Z-x86_64-unknown-linux-musl/rustnet /usr/local/bin/ + +# 授予 Linux capabilities(现代内核 5.8+) +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' /usr/local/bin/rustnet + +# 无需 sudo 运行 +rustnet +``` + +**何时使用静态二进制文件:** +- GLIBC 过时的旧发行版(例如 CentOS 7、旧版 Ubuntu) +- 最小化/容器化环境 +- 难以安装依赖的气隙系统 +- 当你需要一个单一的可移植二进制文件时 + +### FreeBSD 安装 + +FreeBSD 支持从版本 0.15.0 开始提供。 + +#### 从 Ports 或 Packages(未来) + +一旦进入 FreeBSD ports: +```bash +# 使用 pkg(二进制包) +pkg install rustnet + +# 或从 ports 构建 +cd /usr/ports/net/rustnet && make install clean +``` + +#### 从 GitHub Releases + +从 [rustnet-bsd releases](https://github.com/domcyrus/rustnet-bsd/releases) 下载 FreeBSD 二进制文件: + +```bash +# 下载适合的包 +fetch https://github.com/domcyrus/rustnet-bsd/releases/download/vX.Y.Z/rustnet-vX.Y.Z-x86_64-unknown-freebsd.tar.gz + +# 解压归档 +tar xzf rustnet-vX.Y.Z-x86_64-unknown-freebsd.tar.gz + +# 将二进制文件移动到 PATH +sudo mv rustnet-vX.Y.Z-x86_64-unknown-freebsd/rustnet /usr/local/bin/ + +# 使其可执行 +sudo chmod +x /usr/local/bin/rustnet + +# 使用 sudo 运行 +sudo rustnet +``` + +#### 在 FreeBSD 上从源码构建 + +```bash +# 安装依赖 +pkg install rust libpcap + +# 克隆仓库 +git clone https://github.com/domcyrus/rustnet.git +cd rustnet + +# Release 模式构建 +cargo build --release + +# 可执行文件位于 target/release/rustnet +sudo ./target/release/rustnet +``` + +#### FreeBSD 权限配置 + +FreeBSD 需要访问 BPF(Berkeley Packet Filter)设备来进行数据包捕获。 + +**选项 1:使用 sudo 运行(最简单)** +```bash +sudo rustnet +``` + +**选项 2:将用户添加到 bpf 组(推荐)** +```bash +# 将你的用户添加到 bpf 组 +sudo pw groupmod bpf -m $(whoami) + +# 注销并重新登录以使组变更生效 + +# 现在无需 sudo 运行 +rustnet +``` + +**选项 3:更改 BPF 设备权限(临时)** +```bash +# 重启后会重置 +sudo chmod o+rw /dev/bpf* + +# 现在无需 sudo 运行 +rustnet +``` + +**验证 FreeBSD 权限:** +```bash +# 检查是否在 bpf 组中 +groups | grep bpf + +# 检查 BPF 设备权限 +ls -la /dev/bpf* + +# 不使用 sudo 测试 +rustnet --help +``` + +### Android(Termux)安装 + +RustNet 可以通过 [Termux](https://termux.dev/en/) 在 Android 设备上运行,前提是设备已 root。 + +由于 Android 严格控制网络和进程信息,RustNet 需要 `root` 访问权限(`su`)才能捕获数据包和识别进程。提供一个专门的 Android 构建,静态链接依赖并禁用与 Android 内核环境不兼容的 Linux 特定功能(如 eBPF 和 Landlock)。 + +#### 前置要求 +1. **已 Root** 的 Android 设备(例如通过 Magisk 或 KernelSU) +2. 已安装 **Termux**(从 F-Droid 或 GitHub 获取,*不要*从 Google Play 获取) + +#### 安装步骤 + +1. **在 Termux 中安装所需包:** + ```bash + pkg update + pkg install tsu wget tar + ``` + +2. **下载 Android 二进制文件:** + ```bash + # 从 GitHub Releases 下载 Android 专用静态二进制文件 + wget https://github.com/domcyrus/rustnet/releases/download/vX.Y.Z/rustnet-vX.Y.Z-aarch64-linux-android-musl.tar.gz + ``` + +3. **解压并安装:** + ```bash + tar xzf rustnet-vX.Y.Z-aarch64-linux-android-musl.tar.gz + + # 将其移动到 PATH 中的目录 + mv rustnet-vX.Y.Z-aarch64-linux-android-musl/rustnet $PREFIX/bin/ + chmod +x $PREFIX/bin/rustnet + ``` + +4. **以 root 身份运行 RustNet:** + ```bash + # 你必须以 root 权限运行 RustNet,才能在 Android 上正常工作 + sudo rustnet + ``` + *注意:首次运行时,你的 root 管理器(例如 Magisk)会提示你授予 Termux Superuser 访问权限。* + +## 通过 Cargo 安装 + +```bash +# 直接从 crates.io 安装 +cargo install rustnet-monitor + +# 二进制文件将安装到 ~/.cargo/bin/rustnet +# 确保 ~/.cargo/bin 在你的 PATH 中 +``` + +安装后,参见[权限配置](#permissions-setup)章节配置权限。 + +## 从源码构建 + +### 前置要求 + +- Rust 2024 edition 或更高版本(从 [rustup.rs](https://rustup.rs/) 安装) +- 平台特定依赖: + - **Linux(Debian/Ubuntu)**: + ```bash + sudo apt-get install build-essential pkg-config libpcap-dev libelf-dev zlib1g-dev clang llvm + ``` + - **Linux(RedHat/CentOS/Fedora)**: + ```bash + sudo yum install make pkgconfig libpcap-devel elfutils-libelf-devel zlib-devel clang llvm + ``` + - **macOS**:安装 Xcode Command Line Tools:`xcode-select --install` + - **FreeBSD**:`pkg install rust libpcap` + - **Windows**:安装 Npcap 和 Npcap SDK(参见下方的 [Windows 构建配置](#windows-build-setup)) + +### 基本构建 + +```bash +# 克隆仓库 +git clone https://github.com/domcyrus/rustnet.git +cd rustnet + +# Release 模式构建(Linux 上默认启用 eBPF) +cargo build --release + +# 构建不带 eBPF 支持(仅 Linux procfs 模式) +cargo build --release --no-default-features + +# 可执行文件位于 target/release/rustnet +``` + +不带 eBPF(仅 procfs 模式)构建时,使用 `cargo build --release --no-default-features`。 + +### Windows 构建配置 + +在 Windows 上构建 RustNet 需要 Npcap SDK 和正确的环境配置: + +#### 构建需求 + +1. **下载并安装 Npcap SDK**: + - 从 https://npcap.com/dist/ 下载 Npcap SDK + - 将 SDK 解压到一个目录(例如 `C:\npcap-sdk`) + +2. **设置环境变量**: + - 将 `LIB` 环境变量设置为包含 SDK 的库路径: + ```cmd + set LIB=%LIB%;C:\npcap-sdk\Lib\x64 + ``` + - 对于 PowerShell: + ```powershell + $env:LIB = "$env:LIB;C:\npcap-sdk\Lib\x64" + ``` + - 要永久设置,请添加到你的系统环境变量中 + +3. **构建 RustNet**: + ```cmd + cargo build --release + ``` + +#### 运行时需求 + +1. **安装 Npcap Runtime**: + - 从 https://npcap.com/dist/ 下载 Npcap 安装程序 + - 运行安装程序并在安装期间**选择 "WinPcap API compatible mode"** + - 这确保与包捕获库的兼容性 + +2. **运行 RustNet**: + ```cmd + rustnet.exe + ``` + +**注意**:根据你的 Npcap 安装设置,你可能需要或不需要 Administrator 特权。如果你在 Npcap 安装期间没有选择限制数据包捕获到管理员的选项,RustNet 可以用普通用户权限运行。 + +## 使用 Docker + +RustNet 可作为 Docker 容器从 GitHub Container Registry 获取: + +```bash +# 拉取最新镜像 +docker pull ghcr.io/domcyrus/rustnet:latest + +# 或拉取特定版本 +docker pull ghcr.io/domcyrus/rustnet:0.7.0 + +# 使用 eBPF 支持所需的 Linux capabilities 运行(latest) +docker run --rm -it --cap-add=NET_RAW --cap-add=BPF --cap-add=PERFMON --net=host \ + ghcr.io/domcyrus/rustnet:latest + +# 使用特定版本运行 +docker run --rm -it --cap-add=NET_RAW --cap-add=BPF --cap-add=PERFMON --net=host \ + ghcr.io/domcyrus/rustnet:0.7.0 + +# 使用指定接口运行 +docker run --rm -it --cap-add=NET_RAW --cap-add=BPF --cap-add=PERFMON --net=host \ + ghcr.io/domcyrus/rustnet:latest -i eth0 + +# 替代方案:使用 privileged 模式(安全性较低但更简单) +docker run --rm -it --privileged --net=host \ + ghcr.io/domcyrus/rustnet:latest + +# 查看可用选项 +docker run --rm ghcr.io/domcyrus/rustnet:latest --help +``` + +**注意:** 容器需要 Linux capabilities(`NET_RAW`、`BPF` 和 `PERFMON`)或 privileged 模式才能进行带 eBPF 支持的数据包捕获。推荐使用主机网络(`--net=host`)以监控所有网络接口。 + +## 权限配置 + +RustNet 需要提升的特权来捕获网络数据包,因为在所有现代操作系统上,访问网络接口进行数据包捕获是一项特权操作。本章节解释如何在不同平台上正确授予这些权限。 + +> ### **安全优势:Linux 上的只读网络访问** +> +> **RustNet 在所有平台上使用只读数据包捕获,不启用混杂模式。** 这意味着: +> +> **Linux:** 仅需要 **`CAP_NET_RAW`** 这项 Linux capability —— **不需要**完整的 root 或 `CAP_NET_ADMIN` +> **最小权限原则:** 数据包捕获所需的最小权限 +> **无混杂模式:** 仅捕获往返于主机的数据包(而非所有网络流量) +> **只读:** 不能修改或注入数据包 +> **增强安全性:** 与完整 root 访问相比,攻击面更小 +> +> **macOS 注意:** PKTAP(用于进程元数据)需要 root 特权,但你可以在不使用 sudo 的情况下运行,使用 `lsof` 回退进行基本数据包捕获。 + +### 为什么需要权限 + +网络数据包捕获需要访问: + +- **Raw socket** 用于低层网络访问(只读、非混杂模式) +- **网络接口** 用于数据包捕获 +- macOS/BSD 系统上的 **BPF(Berkeley Packet Filter)设备** +- 某些 Linux 配置上的 **网络命名空间** + +这些 Linux capabilities 受到限制,以防止恶意软件拦截网络流量。 + +### macOS 权限配置 + +在 macOS 上,数据包捕获需要访问位于 `/dev/bpf*` 的 BPF(Berkeley Packet Filter)设备。 + +**注意:** macOS PKTAP(用于从数据包中提取进程元数据)需要 **root/sudo** 特权。不使用 sudo 时,RustNet 使用 `lsof` 作为进程检测的回退(较慢,但无需 root)。 + +#### 选项 1:使用 sudo 运行(最简单) + +```bash +# 使用 sudo 构建并运行 +cargo build --release +sudo ./target/release/rustnet +``` + +#### 选项 2:BPF 组访问(推荐) + +将用户添加到 `access_bpf` 组以实现免密码数据包捕获: + +**使用 Wireshark 的 ChmodBPF(用于基本数据包捕获):** + +```bash +# 安装 Wireshark 的 BPF 权限助手 +brew install --cask wireshark-chmodbpf + +# 注销并重新登录以使组变更生效 +# 然后无需 sudo 运行 rustnet: +rustnet # 使用 lsof 进行进程检测(较慢) + +# 如需 PKTAP 支持以从包头部获取进程元数据,请使用 sudo: +sudo rustnet # 使用 PKTAP 进行更快的进程检测 +``` + +**注意**:`wireshark-chmodbpf` 授予对 `/dev/bpf*` 的数据包捕获访问权限,但 **PKTAP** 是一个独立的特权内核接口,无论 BPF 权限如何都需要 root 特权。TUI 会显示当前使用的检测方法(使用 sudo 时为 "pktap",不使用 sudo 时为 "lsof")。 + +**手动 BPF 组配置:** + +```bash +# 创建 access_bpf 组(如果不存在) +sudo dseditgroup -o create access_bpf + +# 将用户添加到组中 +sudo dseditgroup -o edit -a $USER -t user access_bpf + +# 设置 BPF 设备权限(每次重启后都需要执行) +sudo chmod g+rw /dev/bpf* +sudo chgrp access_bpf /dev/bpf* + +# 注销并重新登录以使组成员身份生效 +``` + +#### 选项 3:Homebrew 安装 + +如果通过 Homebrew 安装,formula 会提供详细的配置说明: + +```bash +brew install rustnet +# 按照安装后显示的提示操作 +``` + +### Linux 权限配置(只读访问 - 无需 Root!) + +**Linux 优势:** RustNet 进行数据包捕获**仅需要 `CAP_NET_RAW`** —— 远少于完整的 root 访问! + +在 Linux 上,数据包捕获仅需要 `CAP_NET_RAW` 这项 Linux capability,用于只读、非混杂数据包捕获。对于 eBPF 增强型进程追踪,需要额外的 Linux capabilities(`CAP_BPF` 和 `CAP_PERFMON`),但**不需要 `CAP_NET_ADMIN`**。 + +#### 选项 1:使用 sudo 运行(最简单) + +```bash +# 使用 sudo 构建并运行 +cargo build --release +sudo ./target/release/rustnet +``` + +#### 选项 2:授予 Linux capabilities(推荐) + +为二进制文件授予特定的 Linux capabilities,而无需完整的 root 特权: + +**对于源码构建:** + +```bash +# 先构建二进制文件 +cargo build --release + +# 为二进制文件授予 Linux capabilities(现代内核 5.8+,带 eBPF 支持) +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' ./target/release/rustnet + +# 现在无需 sudo 运行 +./target/release/rustnet +``` + +**对于 cargo 安装的二进制文件:** + +```bash +# 如果通过 cargo install rustnet-monitor 安装(现代内核 5.8+) +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' ~/.cargo/bin/rustnet + +# 现在无需 sudo 运行 +rustnet +``` + +**对于启用 eBPF 的构建(增强型 Linux 性能 - 默认启用):** + +eBPF 在 Linux 构建上默认启用,使用内核探针提供低开销的进程识别: + +```bash +# Release 模式构建(默认启用 eBPF) +cargo build --release + +# 现代 Linux(5.8+)- 仅需这三个 Linux capabilities: +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' ./target/release/rustnet +./target/release/rustnet + +# 仅包捕获(eBPF 会回退到 procfs) +sudo setcap 'cap_net_raw+eip' ./target/release/rustnet +./target/release/rustnet + +# 检查 TUI 统计面板 - 应显示 "Process Detection: eBPF + procfs" +``` + +**Linux capabilities 需求:** + +**基础 Linux capabilities(始终需要):** +- `CAP_NET_RAW` —— 用于只读数据包捕获的 raw socket 访问(非混杂模式) + +**eBPF 所需的 Linux capabilities(根据内核版本选择):** + +**现代 Linux(5.8+):** +- `CAP_BPF` —— BPF 程序加载和 map 操作 +- `CAP_PERFMON` —— 性能监控和追踪操作 + +**旧版 Linux(pre-5.8):** +- eBPF 操作需要宽泛的 `CAP_SYS_ADMIN`。不建议默认授予它;请使用 + `CAP_NET_RAW` 进行包捕获,并让 RustNet 回退到 procfs 进程检测,除非你明确接受该风险。 + +**注意:** 不需要 CAP_NET_ADMIN。RustNet 使用不带混杂模式的只读数据包捕获。 + +**回退行为**:如果 eBPF 无法加载(例如 Linux capabilities 不足、内核不兼容),应用会自动使用仅 procfs 模式。TUI 统计面板显示当前使用的检测方法: +- `Process Detection: eBPF + procfs` —— eBPF 成功加载 +- `Process Detection: procfs` —— 使用 procfs 回退 + +**注意:** eBPF 在 Linux 构建上默认启用,进程名显示可能存在局限性。有关 eBPF 实现的详情参见 [ARCHITECTURE.zh-CN.md](ARCHITECTURE.zh-CN.md)。要构建不带 eBPF 的版本,使用 `cargo build --release --no-default-features`。 + +**对于系统级安装:** + +```bash +# 如果通过包管理器安装或复制到 /usr/local/bin(现代内核 5.8+) +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' /usr/local/bin/rustnet +rustnet +``` + +### Windows 权限配置 + +Windows 支持目前有限,但可用时: + +- RustNet 需要 **Administrator 特权** +- 必须安装 **WinPcap** 或 **Npcap** 用于数据包捕获 +- 以 Administrator 身份运行命令提示符或 PowerShell + +### 验证权限 + +要验证权限是否正确配置: + +#### macOS + +```bash +# 检查 BPF 设备权限 +ls -la /dev/bpf* + +# 检查组成员身份 +groups | grep access_bpf + +# 不使用 sudo 测试 +rustnet --help +``` + +#### Linux + +```bash +# 检查二进制文件上的 Linux capabilities +# 对于源码构建: +getcap ./target/release/rustnet + +# 对于 cargo 安装的二进制文件: +getcap ~/.cargo/bin/rustnet + +# 对于系统级安装: +getcap $(which rustnet) + +# 现代(5.8+):应显示 cap_net_raw,cap_bpf,cap_perfmon+eip +# 如果 eBPF capability 不可用:应显示 cap_net_raw+eip + +# 不使用 sudo 测试 +rustnet --help +``` + +## GeoIP 数据库(可选) + +RustNet 支持 GeoIP 查询以显示远程 IP 的国家代码、城市名称和 ASN 信息。要启用此功能,请使用 MaxMind 的 `geoipupdate` 工具安装 [GeoLite2](https://dev.maxmind.com/geoip/geolite2-free-geolocation-data) 数据库(需要免费的 [MaxMind 账户](https://www.maxmind.com/en/geolite2/signup))。 + +**可用数据库:** + +| 数据库 | 提供内容 | 标志 | +|---|---|---| +| `GeoLite2-Country.mmdb` | 国家代码和名称 | *(自动发现)* | +| `GeoLite2-ASN.mmdb` | ASN 编号和组织 | *(自动发现)* | +| `GeoLite2-City.mmdb` | 城市名称、邮政编码、**以及**国家 | *(自动发现)* | + +> **提示:** `GeoLite2-City` 是 `GeoLite2-Country` 的超集。如果你安装了 City 数据库,就不需要再安装 Country 数据库。 + +### 配置要下载的数据库 + +在你的 `GeoIP.conf` 中,将 `EditionIDs` 设置为你想要的数据库: + +``` +# 仅 Country + ASN: +EditionIDs GeoLite2-Country GeoLite2-ASN + +# City + ASN(City 已包含国家数据): +EditionIDs GeoLite2-City GeoLite2-ASN + +# 全部三个: +EditionIDs GeoLite2-Country GeoLite2-ASN GeoLite2-City +``` + +### macOS(Homebrew) + +```bash +brew install geoipupdate +# 使用你的 MaxMind 账户凭据和 EditionIDs 编辑配置: +# $(brew --prefix)/etc/GeoIP.conf +geoipupdate +``` + +数据库安装到 `$(brew --prefix)/share/GeoIP/`。 + +### Ubuntu/Debian + +```bash +sudo apt-get install geoipupdate +# 使用你的 MaxMind 账户凭据和 EditionIDs 编辑 /etc/GeoIP.conf +sudo geoipupdate +``` + +数据库安装到 `/usr/share/GeoIP/`。 + +### Fedora/RHEL + +```bash +sudo dnf install geoipupdate +# 使用你的 MaxMind 账户凭据和 EditionIDs 编辑 /etc/GeoIP.conf +sudo geoipupdate +``` + +数据库安装到 `/usr/share/GeoIP/`。 + +### Arch Linux + +```bash +sudo pacman -S geoipupdate +# 使用你的 MaxMind 账户凭据和 EditionIDs 编辑 /etc/GeoIP.conf +sudo geoipupdate +``` + +数据库安装到 `/usr/share/GeoIP/`。 + +### FreeBSD + +```bash +pkg install geoipupdate +# 使用你的 MaxMind 账户凭据和 EditionIDs 编辑 /usr/local/etc/GeoIP.conf +sudo geoipupdate +``` + +数据库安装到 `/usr/local/share/GeoIP/`。 + +### 手动指定 + +如果你的数据库在非标准位置,请直接指定: + +```bash +# Country + ASN: +rustnet --geoip-country /path/to/GeoLite2-Country.mmdb --geoip-asn /path/to/GeoLite2-ASN.mmdb + +# City + ASN(City 已包含国家数据): +rustnet --geoip-city /path/to/GeoLite2-City.mmdb --geoip-asn /path/to/GeoLite2-ASN.mmdb +``` + +RustNet 从标准位置自动发现数据库。运行 `rustnet --help` 查看完整的搜索路径列表。 + +## 故障排查 + +### 常见安装问题 + +#### 权限被拒绝错误 + +**在 macOS 上:** + +- 确保你在 `access_bpf` 组中:`groups | grep access_bpf` +- 检查 BPF 设备权限:`ls -la /dev/bpf0` +- 尝试使用 sudo 运行以确认是权限问题 +- 组变更后注销并重新登录 + +**在 Linux 上:** + +- 检查 Linux capabilities 是否已设置:`getcap $(which rustnet)` 或 `getcap ~/.cargo/bin/rustnet` +- 验证 libpcap 是否已安装:`ldconfig -p | grep pcap` +- 尝试使用 sudo 运行以确认是权限问题:`sudo $(which rustnet)` + +#### 未找到合适的捕获接口 + +- 检查可用接口:`ip link show`(Linux)或 `ifconfig`(macOS) +- 尝试显式指定接口:`rustnet -i eth0` +- 确保接口已启动并拥有 IP 地址 +- 某些虚拟接口可能不支持数据包捕获 + +#### 操作不被允许(已设置 Linux capabilities) + +- Linux capabilities 可能已被系统更新移除 +- 重新应用 Linux capabilities(现代):`sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' $(which rustnet)` +- 某些文件系统不支持扩展属性(Linux capabilities) +- 尝试将二进制文件复制到不同的文件系统(例如从 NFS 复制到本地磁盘) + +#### 已设置 Linux capabilities 但 eBPF 不可用 + +如果 RustNet 在运行 `setcap` 后仍显示 `Process Detection: procfs` 并伴随降级消息,请按以下步骤排查。TUI 在第二行状态栏显示实际原因 —— 用它跳转到下方正确的章节。 + +**1. `file caps ignored: binary on a nosuid mount`** + +内核会静默忽略位于以 `nosuid` 选项挂载的文件系统上的二进制文件的 Linux capabilities。常见 culprit:`/home` 在加固发行版上、`/tmp`、可移动介质、容器内的某些 bind-mount。 + +```bash +# 查找持有该二进制文件的挂载点并检查其选项 +findmnt -T $(realpath $(which rustnet)) -o TARGET,OPTIONS +# 如果 OPTIONS 列包含 "nosuid",Linux capabilities 将不会生效。 + +# 修复:将二进制文件安装或复制到没有 nosuid 的挂载点 +sudo install -m 0755 $(which rustnet) /usr/local/bin/rustnet +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' /usr/local/bin/rustnet +/usr/local/bin/rustnet +``` + +**2. `BPF denied (check perf_event_paranoid / AppArmor / unprivileged_bpf_disabled)`** + +Linux capabilities 已授予,但内核返回了 `EPERM` 或 `EACCES`。三层可能阻止它 —— 按此顺序检查: + +```bash +# 2a. perf_event_paranoid(Debian 上最常见的原因)。 +# Debian 13 默认 kernel.perf_event_paranoid=3,这会阻止 +# 非 root 用户的 perf_event_open(2) —— 因此也阻止 kprobe attach —— +# *即使有 CAP_PERFMON*。上游内核最高只到 2, +# 其中 CAP_PERFMON 正确绕过限制。 +# +# Ubuntu 使用了不同的补丁(paranoid=4),该补丁在 +# 2025 年底更新以尊重 CAP_PERFMON,因此在较新的 Ubuntu 内核上 +# (Jammy 5.15.0-165+、Noble 6.8.0-91+、Plucky 6.14.0-37+、 +# Questing 6.17.0-14+、Resolute 6.18.0-8+)仅 `setcap` 就 +# 足够了 —— 无需 sysctl 更改。Debian 的等效补丁 +# (bug #994044)从未更新,并在 2025 年被归档 +# 且未修复,因此 Debian 用户仍需要下方的变通方法。 +sysctl kernel.perf_event_paranoid +# 如果值为 3(Debian)或旧 Ubuntu 内核上的 4,将其降到 2: +sudo sysctl kernel.perf_event_paranoid=2 +# 使其在重启后持久: +echo 'kernel.perf_event_paranoid = 2' | \ + sudo tee /etc/sysctl.d/99-rustnet.conf + +# 2b. AppArmor 限制 rustnet(Debian/Ubuntu 默认安装 AppArmor)。 +sudo aa-status | grep rustnet +# 如果已列出,要么禁用该配置文件,要么添加一条规则允许 +# Linux capability `bpf`、Linux capability `perfmon`,以及该二进制文件的 bpf() 系统调用。 + +# 2c. unprivileged_bpf_disabled(Debian 设置为 =2;文件 Linux capabilities 应能绕过)。 +sysctl kernel.unprivileged_bpf_disabled + +# 确认 Linux capabilities 在 exec 时实际生效: +grep ^Cap /proc/$(pgrep -n rustnet)/status +# CapEff 必须包含 CAP_BPF(位 39)和 CAP_PERFMON(位 38)。 +``` + +**3. `kprobe attach failed: `** + +内核缺少 eBPF 探针想要附加的符号。这通常是内核配置问题(例如 CONFIG_IPV6 禁用、CONFIG_KPROBES 关闭,或符号被内联)。RustNet 当前附加到 +`tcp_connect`、`inet_csk_accept`、`udp_sendmsg`、`tcp_v6_connect`、 +`udpv6_sendmsg`、`ping_v4_sendmsg` 和 `ping_v6_sendmsg`。 + +```bash +# 检查失败符号是否存在于运行中的内核中: +sudo grep '' /proc/kallsyms +``` + +如果符号确实缺失,eBPF 进程检测将无法在此内核构建上工作;procfs 回退继续正常工作。 + +**4. `kernel BTF unavailable`** + +CO-RE 重定位需要 `/sys/kernel/btf/vmlinux`。在精简内核上(某些嵌入式 / 最小云镜像)该文件不存在。 + +```bash +ls /sys/kernel/btf/vmlinux +# 如果缺失:安装与你的运行内核匹配的 kernel-debuginfo / linux-image 包, +# 或使用 CONFIG_DEBUG_INFO_BTF=y 重新构建内核。 +``` + +**5. 在 Docker / Podman 中** + +即使有文件 Linux capabilities,容器内的*bounding*集合也必须包含 `CAP_BPF` 和 `CAP_PERFMON`,否则它们在 exec 时会被屏蔽: + +```bash +docker run --cap-add=NET_RAW --cap-add=BPF --cap-add=PERFMON \ + --net=host --pid=host rustnet +# 可选,如果你的容器的 seccomp 配置文件阻止 bpf(2): +# --security-opt seccomp=unconfined +# 或如果 AppArmor 监管 BPF: +# --security-opt apparmor=unconfined +``` + +**6. `eBPF load failed: ...`** + +兜底分支携带原始 libbpf 错误文本。使用 +`RUST_LOG=debug rustnet 2>&1 | tee rustnet.log` 重新运行并检查完整 +链 —— 它通常包含一个 `errno` 名称(`EPERM`、`EACCES`、`ENOSPC` +表示 memlock 等),指向根本原因。 + +#### Windows:未找到 Npcap + +- 确保从 https://npcap.com/dist/ 安装了 Npcap +- 在 Npcap 安装期间,选择 **"WinPcap API compatible mode"** +- 验证 Npcap 服务正在运行:`sc query npcap` +- 尝试使用管理员权限重新安装 Npcap + +#### 构建错误 + +**Windows - 未找到 Npcap SDK:** +- 确保 `LIB` 环境变量包含 Npcap SDK 路径 +- 检查 SDK 是否解压到没有空格的目录 +- 为你的 Rust 工具链使用正确的架构(x64 与 x86) + +**Linux 构建失败:** +```bash +# 安装所有必需的依赖 +# Debian/Ubuntu +sudo apt-get install build-essential pkg-config libpcap-dev libelf-dev zlib1g-dev clang llvm + +# RedHat/CentOS/Fedora +sudo yum install make pkgconfig libpcap-devel elfutils-libelf-devel zlib-devel clang llvm +``` + +#### Windows:图表在 PowerShell 中显示不正确 + +如果图表和 sparkline 在 PowerShell 中显示损坏(显示问号或乱码字符),这是**字体问题**,不是 RustNet 的 bug。默认的控制台字体(Consolas、Lucida Console)缺少图表渲染使用的 Unicode Braille 字符支持。 + +**解决方案:** 安装支持 Unicode Braille 的字体: + +1. 下载并安装 [Iosevka](https://typeof.net/Iosevka/) 或任意 [Nerd Font](https://www.nerdfonts.com/) +2. 打开 PowerShell 属性(右键标题栏 → 属性) +3. 在字体选项卡中选择已安装的字体 +4. 重启 PowerShell + +**替代方案:** 使用 [Windows Terminal](https://aka.ms/terminal),它开箱即用地提供更好的 Unicode 支持。 + +参见:[ratatui#457](https://github.com/ratatui/ratatui/issues/457)、[gtop#21](https://github.com/aksakalli/gtop/issues/21) + +#### macOS:终端 CPU 占用高或图表有缺口(iTerm2) + +RustNet 的图表使用 Unicode Braille 字符绘制。macOS 经典的等宽字体(Monaco、Menlo)不包含 Braille 字形,因此 iTerm2 会对每个图表单元格进行字体回退渲染。这不仅很慢(仅显示图表就可能占用 iTerm2 10-20% 的 CPU),回退字形还常常在波形中留下明显的缺口。 + +**解决方案:** 为 iTerm2 的非 ASCII 文本指定一个包含 Braille 字形的字体: + +1. 安装一个 [Nerd Font](https://www.nerdfonts.com/),例如: + + ```bash + brew install --cask font-jetbrains-mono-nerd-font + ``` + +2. 在 iTerm2 中:**Settings → Profiles → Text**,启用 **"Use a different font for non-ASCII text"**(为非 ASCII 文本使用不同的字体)并选择该 Nerd Font。常规文本保持当前字体不变;只有符号和图表字形使用新字体。 +3. 可选:**Settings → General → Preferences → "Maximize throughput"**(最大化吞吐量)将 iTerm2 的重绘率限制在 30 fps,可进一步降低频繁刷新的 TUI 应用的 CPU 占用。 + +**替代方案:** 使用 GPU 加速的终端,如 [WezTerm](https://wezterm.org/)、[Ghostty](https://ghostty.org/) 或 [kitty](https://sw.kovidgoyal.net/kitty/)。它们渲染 RustNet 的 CPU 占用远低于 iTerm2,其中 WezTerm 内置 JetBrains Mono 及符号回退字体,图表开箱即用即可正确渲染。 + +### 获取帮助 + +如果你遇到此处未涵盖的问题: + +1. 启用调试日志:`rustnet --log-level debug` +2. 检查 `logs/` 目录中的日志文件 +3. 在 [GitHub](https://github.com/domcyrus/rustnet/issues) 上开 issue,并提供: + - 你的操作系统和版本 + - 使用的安装方法 + - 日志中的错误消息 + - 权限验证命令的输出 + +### 安全最佳实践 + +1. **尽可能使用 Linux capabilities,而不是 sudo**(Linux) +2. **使用基于组的访问而非以 root 运行**(macOS) +3. **定期审计**哪些用户拥有数据包捕获特权 +4. **考虑网络分段**如果在生产系统上运行 +5. **监控日志文件**以发现未授权的使用 +6. **在不再需要 RustNet 时移除相关的 Linux capabilities**: + + ```bash + # Linux:移除 Linux capabilities + sudo setcap -r /path/to/rustnet + + # macOS:从组中移除 + sudo dseditgroup -o edit -d $USER -t user access_bpf + ``` + +### 与系统监控集成 + +对于生产环境,请考虑: + +- 数据包捕获访问的**审计日志** +- **网络监控策略**和合规要求 +- 特权网络访问的**用户访问审查** +- 配置管理系统中的**自动化 Linux capabilities 管理** + +此权限配置确保 RustNet 能够在捕获数据包的同时遵循安全最佳实践和最小权限原则。 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/PROFILING.md b/PROFILING.md new file mode 100644 index 0000000..2a6f535 --- /dev/null +++ b/PROFILING.md @@ -0,0 +1,196 @@ +

English | 简体中文

+ +# RustNet Performance Profiling Guide + +This guide explains how to profile RustNet to identify performance bottlenecks. + +## Quick Start + +### CPU Profiling with perf + flamegraph + +The easiest way to profile CPU usage on Linux: + +```bash +# 1. Install flamegraph tools +cargo install flamegraph + +# 2. Build a release binary with debug symbols +# IMPORTANT: Debug symbols are required for meaningful flamegraphs! +CARGO_PROFILE_RELEASE_DEBUG=true cargo build --release + +# Or add this to Cargo.toml temporarily: +# [profile.release] +# debug = true + +# 3. Run with profiling (requires sudo for perf) +# Note: Use full path to flamegraph since sudo doesn't have your user's PATH +# IMPORTANT: Use -- before the command to profile +sudo -E ~/.cargo/bin/flamegraph -- ./target/release/rustnet + +# Or specify interface and other args after the binary +sudo -E ~/.cargo/bin/flamegraph -- ./target/release/rustnet -i eth0 + +# Alternatively, preserve PATH for cleaner commands: +sudo env "PATH=$PATH" flamegraph -- ./target/release/rustnet + +# 4. Open the generated flamegraph.svg in a browser +firefox flamegraph.svg +``` + +### Alternative: Using perf directly + +If you prefer to use `perf` directly: + +```bash +# Build with debug symbols +cargo build --release + +# Record performance data (run for 30-60 seconds, then Ctrl+C to stop) +sudo perf record -F 99 -g ./target/release/rustnet -i eth0 + +# Generate flamegraph (requires FlameGraph scripts) +# Install from: https://github.com/brendangregg/FlameGraph +perf script | stackcollapse-perf.pl | flamegraph.pl > flamegraph.svg + +# Or view in perf's TUI +sudo perf report +``` + +### Profiling a Running Instance + +If RustNet is already running: + +```bash +# Find the PID +ps aux | grep rustnet + +# Profile the running process for 60 seconds +sudo -E ~/.cargo/bin/flamegraph -p --output rustnet-live.svg + +# Or with perf directly +sudo perf record -F 99 -g -p sleep 60 +sudo perf report +``` + +## Interpreting Flamegraphs + +Look for: +- **Wide bars at the bottom**: Functions that consume a lot of total CPU time +- **Tall stacks**: Deep call chains (potential optimization targets) +- **Hot spots**: Functions with many samples (bright colors in some viewers) + +Common hot spots: +- `packet_parser::parse_packet`: Normal - this is the core packet processing +- `DashMap::iter` or `iter_mut`: If this is a large portion, consider reducing iteration frequency +- `clone`: If excessive, reduce unnecessary cloning +- System calls (`read`, `write`, `ioctl`): Filesystem or network I/O overhead + +## Criterion Benchmarks + +Microbenchmarks for core operations live in `benches/`. Run them with: + +| Benchmark | Command | +|-----------|---------| +| Packet parsing | `cargo bench --bench packet_parsing` | +| Connection merge | `cargo bench --bench connection_merge` | +| Snapshot creation | `cargo bench --bench snapshot` | +| All benchmarks | `cargo bench` | +| Struct sizes | `cargo test --lib struct_sizes -- --nocapture` | + +Criterion produces HTML reports in `target/criterion/` with statistical comparison between runs. + +## Ad-hoc Benchmarking + +For consistent benchmarks: + +```bash +# Run with consistent traffic +sudo ./target/release/rustnet --interface eth0 & +PID=$! + +# Monitor CPU usage +top -p $PID + +# Or use perf stat for detailed metrics +sudo perf stat -p $PID sleep 60 + +# Stop the application +sudo kill $PID +``` + +## Performance Regression Testing + +After making changes, compare before/after: + +```bash +# Baseline (before changes) +sudo perf stat -r 3 timeout 60s ./target/release/rustnet-before > /dev/null + +# After changes +sudo perf stat -r 3 timeout 60s ./target/release/rustnet > /dev/null +``` + +Key metrics to compare: +- CPU cycles +- Instructions per cycle (IPC) +- Cache misses +- Context switches + +## Troubleshooting Flamegraphs + +### Empty or Single-Entry Flamegraph + +If your flamegraph only shows "rustnet (100%)" with no details: + +**Problem**: Debug symbols are missing from the release build. + +**Solution**: +```bash +# Rebuild with debug symbols +CARGO_PROFILE_RELEASE_DEBUG=true cargo build --release + +# Or add to Cargo.toml: +[profile.release] +debug = true + +# Then re-profile +sudo -E ~/.cargo/bin/flamegraph -- ./target/release/rustnet +``` + +### Flamegraph Shows Only Kernel Functions + +**Problem**: Running with insufficient permissions or perf can't access user-space symbols. + +**Solution**: +```bash +# Check perf_event_paranoid setting +cat /proc/sys/kernel/perf_event_paranoid + +# If it's > 1, temporarily lower it (requires root): +sudo sysctl kernel.perf_event_paranoid=1 + +# Or run as root +sudo -E ~/.cargo/bin/flamegraph -- ./target/release/rustnet +``` + +### Very Short Flamegraph (< 1000 samples) + +**Problem**: Profiling session too short, not enough data collected. + +**Solution**: +```bash +# Let rustnet run for at least 30-60 seconds before stopping +# The more network traffic, the better the profile + +# For longer profiling: +timeout 60 sudo -E ~/.cargo/bin/flamegraph -- ./target/release/rustnet +``` + +## Debugging Slow TUI + +If the TUI feels sluggish: + +1. **Check refresh rate**: Default is 1000ms, can be adjusted with `--refresh-interval` +2. **Check connection count**: High connection counts increase sorting overhead +3. **Profile the UI loop**: Look for hot spots in `run_ui_loop`, `draw`, or `sort_connections` +4. **Monitor thread contention**: Check if packet processing threads are blocking the snapshot provider diff --git a/PROFILING.zh-CN.md b/PROFILING.zh-CN.md new file mode 100644 index 0000000..bc01393 --- /dev/null +++ b/PROFILING.zh-CN.md @@ -0,0 +1,196 @@ +

English | 简体中文

+ +# RustNet 性能分析指南 + +本指南介绍如何对 RustNet 进行性能分析,以定位性能瓶颈。 + +## 快速开始 + +### 使用 perf + flamegraph 进行 CPU 分析 + +在 Linux 上分析 CPU 占用最简单的方式: + +```bash +# 1. 安装 flamegraph 工具 +cargo install flamegraph + +# 2. 构建带调试符号的 release 二进制 +# 重要:要生成有意义的火焰图,必须包含调试符号! +CARGO_PROFILE_RELEASE_DEBUG=true cargo build --release + +# 或临时在 Cargo.toml 中加入: +# [profile.release] +# debug = true + +# 3. 在性能分析下运行(perf 需要 sudo) +# 注意:使用 flamegraph 的完整路径,因为 sudo 不会带上你用户的 PATH +# 重要:在要分析的命令前加 -- +sudo -E ~/.cargo/bin/flamegraph -- ./target/release/rustnet + +# 或在二进制之后指定接口及其他参数 +sudo -E ~/.cargo/bin/flamegraph -- ./target/release/rustnet -i eth0 + +# 或者保留 PATH 以使用更简洁的命令: +sudo env "PATH=$PATH" flamegraph -- ./target/release/rustnet + +# 4. 在浏览器中打开生成的 flamegraph.svg +firefox flamegraph.svg +``` + +### 备选方案:直接使用 perf + +如果你更习惯直接使用 `perf`: + +```bash +# 构建带调试符号的版本 +cargo build --release + +# 记录性能数据(运行 30-60 秒,然后按 Ctrl+C 停止) +sudo perf record -F 99 -g ./target/release/rustnet -i eth0 + +# 生成火焰图(需要 FlameGraph 脚本) +# 从以下地址安装:https://github.com/brendangregg/FlameGraph +perf script | stackcollapse-perf.pl | flamegraph.pl > flamegraph.svg + +# 或在 perf 的 TUI 中查看 +sudo perf report +``` + +### 分析正在运行的实例 + +如果 RustNet 已经在运行: + +```bash +# 查找 PID +ps aux | grep rustnet + +# 对运行中的进程分析 60 秒 +sudo -E ~/.cargo/bin/flamegraph -p --output rustnet-live.svg + +# 或直接使用 perf +sudo perf record -F 99 -g -p sleep 60 +sudo perf report +``` + +## 解读火焰图 + +重点关注: +- **底部的宽条**:消耗大量总 CPU 时间的函数 +- **高耸的栈**:很深的调用链(潜在的优化目标) +- **热点**:采样次数很多的函数(在某些查看器中显示为鲜亮的颜色) + +常见热点: +- `packet_parser::parse_packet`:正常——这是核心的数据包处理 +- `DashMap::iter` 或 `iter_mut`:如果占比很大,考虑降低迭代频率 +- `clone`:如果过多,减少不必要的克隆 +- 系统调用(`read`、`write`、`ioctl`):文件系统或网络 I/O 开销 + +## Criterion 基准测试 + +核心操作的微基准测试位于 `benches/`。运行方式: + +| 基准测试 | 命令 | +|-----------|---------| +| 数据包解析 | `cargo bench --bench packet_parsing` | +| 连接合并 | `cargo bench --bench connection_merge` | +| 快照创建 | `cargo bench --bench snapshot` | +| 全部基准测试 | `cargo bench` | +| 结构体大小 | `cargo test --lib struct_sizes -- --nocapture` | + +Criterion 会在 `target/criterion/` 中生成 HTML 报告,并对多次运行结果进行统计比较。 + +## 临时基准测试 + +要获得稳定一致的基准测试: + +```bash +# 在稳定的流量下运行 +sudo ./target/release/rustnet --interface eth0 & +PID=$! + +# 监控 CPU 占用 +top -p $PID + +# 或使用 perf stat 获取详细指标 +sudo perf stat -p $PID sleep 60 + +# 停止应用 +sudo kill $PID +``` + +## 性能回归测试 + +在改动之后,对比改动前后: + +```bash +# 基线(改动前) +sudo perf stat -r 3 timeout 60s ./target/release/rustnet-before > /dev/null + +# 改动后 +sudo perf stat -r 3 timeout 60s ./target/release/rustnet > /dev/null +``` + +需要对比的关键指标: +- CPU 周期数 +- 每周期指令数(IPC) +- 缓存未命中 +- 上下文切换 + +## 火焰图问题排查 + +### 火焰图为空或只有单个条目 + +如果你的火焰图只显示 “rustnet (100%)” 而没有任何细节: + +**问题**:release 构建缺少调试符号。 + +**解决方案**: +```bash +# 带调试符号重新构建 +CARGO_PROFILE_RELEASE_DEBUG=true cargo build --release + +# 或在 Cargo.toml 中加入: +[profile.release] +debug = true + +# 然后重新分析 +sudo -E ~/.cargo/bin/flamegraph -- ./target/release/rustnet +``` + +### 火焰图只显示内核函数 + +**问题**:运行权限不足,或 perf 无法访问用户态符号。 + +**解决方案**: +```bash +# 检查 perf_event_paranoid 设置 +cat /proc/sys/kernel/perf_event_paranoid + +# 如果它大于 1,临时调低(需要 root): +sudo sysctl kernel.perf_event_paranoid=1 + +# 或以 root 运行 +sudo -E ~/.cargo/bin/flamegraph -- ./target/release/rustnet +``` + +### 火焰图过短(采样少于 1000 个) + +**问题**:分析会话太短,采集的数据不足。 + +**解决方案**: +```bash +# 在停止前,让 rustnet 至少运行 30-60 秒 +# 网络流量越多,分析结果越好 + +# 如需更长时间的分析: +timeout 60 sudo -E ~/.cargo/bin/flamegraph -- ./target/release/rustnet +``` + +## 排查 TUI 卡顿 + +如果 TUI 感觉迟钝: + +1. **检查刷新频率**:默认是 1000ms,可通过 `--refresh-interval` 调整 +2. **检查连接数量**:连接数过高会增加排序开销 +3. **分析 UI 循环**:在 `run_ui_loop`、`draw` 或 `sort_connections` 中查找热点 +4. **监控线程争用**:检查数据包处理线程是否阻塞了快照提供者 diff --git a/README.md b/README.md new file mode 100644 index 0000000..ddeb371 --- /dev/null +++ b/README.md @@ -0,0 +1,340 @@ +

+

RustNet

+

+ Per-process network monitoring for your terminal: live TCP, UDP, and QUIC connections with deep packet inspection, sandboxed by default. +

+

+ Built With Ratatui + Build Status + Crates.io + GitHub Stars + License + GitHub release + Docker Image +

+

+ +

+ English | 简体中文 +

+ +

+ RustNet demo +

+ +

+ Real-time visibility into every connection your machine makes, who owns it, and what protocol it's speaking. No tcpdump, X11 forwarding, or root piping. +

+ +## Features + +- **Per-process attribution**: Every TCP, UDP, and QUIC connection mapped to its owning process, via eBPF on Linux, PKTAP on macOS, native APIs on Windows and FreeBSD. Wireshark and tcpdump can't do this; `netstat` / `ss` can't show live state. +- **Deep packet inspection**: Identify HTTP, HTTPS/TLS with SNI, DNS, SSH, FTP, QUIC, MQTT, BitTorrent, STUN, NTP, mDNS, LLMNR, DHCP, SNMP, SSDP, and NetBIOS, without external dissectors. +- **Annotated PCAPNG export**: `--pcapng-export` writes a Wireshark-ready capture with process, PID, direction, DPI/SNI, and GeoIP embedded as per-packet comments. Open it in Wireshark and every packet already names its owning process, with no post-processing. Classic `--pcap-export` with a JSONL sidecar for offline correlation is also available. +- **Security sandboxing**: Landlock (Linux 5.13+), Seatbelt (macOS), token privilege drop + job-object child-process block (Windows). Drops privileges immediately after libpcap initializes. See [SECURITY.md](SECURITY.md). +- **TCP network analytics**: Real-time retransmissions, out-of-order packets, and fast-retransmit detection, per-connection and aggregate. +- **Smart connection lifecycle**: Protocol-aware timeouts with white → yellow → red staleness indicators. Toggle `t` to keep historic (closed) connections visible for forensics. +- **Vim/fzf-style filtering**: `port:`, `src:`, `dst:`, `sni:`, `process:`, `state:`, `proto:`, plus regex via `/(?i)pattern/`. +- **GeoIP enrichment**: Country lookups via local MaxMind GeoLite2. No network calls. +- **Kubernetes attribution** (optional `kubernetes` feature): connections mapped to their pod, namespace, and container, shown in the details pane, JSON/PCAPNG exports, and the `pod:`, `ns:`, `container:` filters. Enabled in the official Docker image; on a cluster, use the [kubectl-rustnet](https://github.com/domcyrus/kubectl-rustnet) plugin to run it as an ephemeral debug pod. See [USAGE.md](USAGE.md#--kubernetes-mode-optional-feature). +- **Cross-platform**: Linux, macOS, Windows, FreeBSD. + +## Why RustNet? + +RustNet fills the gap between simple connection tools (`netstat`, `ss`) and packet analyzers (`Wireshark`, `tcpdump`): + +- **Process attribution**: See which application owns each connection. Wireshark cannot provide this because it only sees packets, not sockets. +- **Connection-centric view**: Track states, bandwidth, and protocols per connection in real-time +- **SSH-friendly**: TUI works over SSH so you can quickly see what's happening on a remote server without forwarding X11 or capturing traffic + +RustNet complements packet capture tools. Use RustNet to see *what's making connections*. For direct Wireshark inspection, `--pcapng-export` writes live best-effort packet comments with PID/process context. For cleanup-time correlation, use `--pcap-export` plus the JSONL sidecar and optional `scripts/pcap_enrich.py`. See [PCAP Export](USAGE.md#pcap-export) and [Comparison with Similar Tools](ARCHITECTURE.md#comparison-with-similar-tools) for details. + +Built on ratatui, libpcap, eBPF (libbpf-rs), DashMap, crossbeam, ring, MaxMind GeoLite2, and Landlock. See [ARCHITECTURE.md](ARCHITECTURE.md#dependencies) for the full dependency breakdown. + +
+eBPF Enhanced Process Identification (Linux Default) + +RustNet uses kernel eBPF programs by default on Linux 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 +- eBPF is enabled by default; no special build flags needed + +To disable eBPF and use procfs-only mode, build with: +```bash +cargo build --release --no-default-features +``` + +See [ARCHITECTURE.md](ARCHITECTURE.md) for technical information. + +
+ +
+Interface Statistics Monitoring + +RustNet provides real-time network interface statistics across all supported platforms: + +- **Overview Tab**: Shows active interfaces with current rates, errors, and drops +- **Interfaces Tab** (press `3`): Detailed table with comprehensive metrics for all interfaces +- **Cross-Platform**: Linux (sysfs), macOS/FreeBSD (getifaddrs), Windows (GetIfTable2 API) +- **Smart Filtering**: Windows automatically excludes virtual/filter adapters + +See [USAGE.md](USAGE.md#interface-statistics) for detailed documentation on interpreting interface statistics and platform-specific behavior. + +**Metrics Available:** +- Total bytes and packets (RX/TX) +- Error counters (receive and transmit) +- Packet drops (queue overflows) +- Collisions (legacy, rarely used on modern networks) + +Stats are collected every 2 seconds in a background thread with minimal performance impact. + +
+ +## Screenshots + + + + + + + + + + +
Overview
Connections table with live stats and sparklines
Details
Per-connection SNI, cipher, GeoIP, DPI
Graph
Traffic chart, app distribution, top processes
Interfaces
Per-interface RX/TX history with errors and drops
+ +## Quick Start + +### Installation + +**Homebrew (macOS / Linux):** +```bash +brew install rustnet +``` + +**Ubuntu (25.10+):** +```bash +sudo add-apt-repository ppa:domcyrus/rustnet +sudo apt update && sudo apt install rustnet +``` + +**Fedora (42+):** +```bash +sudo dnf copr enable domcyrus/rustnet +sudo dnf install rustnet +``` + +**openSUSE Tumbleweed:** +```bash +sudo zypper addrepo https://download.opensuse.org/repositories/home:/domcyrus:/rustnet/openSUSE_Tumbleweed/home:domcyrus:rustnet.repo +sudo zypper refresh +sudo zypper install rustnet +``` + +**Arch Linux:** +```bash +sudo pacman -S rustnet +``` + +**Nix / NixOS:** +```bash +nix-shell -p rustnet +# Then inside the shell: sudo rustnet +``` + +**From crates.io:** +```bash +cargo install rustnet-monitor +``` + +**Windows (Chocolatey):** +```powershell +# Run in Administrator PowerShell +# Requires Npcap (https://npcap.com) installed with "WinPcap API-compatible Mode" enabled +choco install rustnet +``` + +**Other platforms:** +- **FreeBSD**: Download from [rustnet-bsd releases](https://github.com/domcyrus/rustnet-bsd/releases) +- **Docker, source builds, other Linux distros**: See [INSTALL.md](INSTALL.md) for detailed instructions + +### Running RustNet + +Packet capture requires elevated privileges: + +```bash +# Quick start (all platforms) +sudo rustnet + +# Linux: Grant capabilities to run without sudo (recommended) +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' $(which rustnet) +rustnet +``` + +**Common options:** +```bash +rustnet -i eth0 # Specify network interface +rustnet --show-localhost # Show localhost connections +rustnet --no-resolve-dns # Disable reverse DNS lookups (enabled by default) +rustnet -r 500 # Set refresh interval (ms) +rustnet --theme classic # Original full-color palette (default: muted) +rustnet --pcapng-export capture.pcapng # Annotated PCAPNG for Wireshark +``` + +See [INSTALL.md](INSTALL.md) for detailed permission setup and [USAGE.md](USAGE.md) for complete options. + +> If you set capabilities but the TUI still shows `eBPF unavailable`, see +> [eBPF Unavailable Despite Capabilities Being Set](INSTALL.md#ebpf-unavailable-despite-capabilities-being-set) +> in the troubleshooting section. + +## Keyboard Controls + +| Key | Action | +|-----|--------| +| `q` | Quit (press twice to confirm) | +| `Ctrl+C` | Quit immediately | +| `x` | Clear all connections (press twice to confirm) | +| `Tab` or `]` | Next tab | +| `Shift+Tab` or `[` | Previous tab | +| `1`–`5` | Jump to Overview / Details / Interfaces / Graph / Help | +| `↑/k` `↓/j` | Navigate up/down | +| `g` `G` | Jump to first/last connection | +| `Enter` | View connection details | +| `Esc` | Go back or clear filter | +| `c` | Copy remote address | +| `p` | Toggle service names/ports | +| `d` | Toggle hostnames/IPs | +| `s` `S` | Cycle sort columns / toggle direction | +| `a` | Toggle process grouping | +| `Space` | Expand/collapse process group | +| `←/→` or `h/l` | Collapse/expand group | +| `PageUp/PageDown` or `Ctrl+B/F` | Page navigation | +| `t` | Toggle historic (closed) connections | +| `i` | Toggle the System info sidebar | +| `r` | Reset view (grouping, sort, filter) | +| `/` | Enter filter mode | +| `h` | Toggle help | + +See [USAGE.md](USAGE.md) for detailed keyboard controls and navigation tips. + +## Filtering & Sorting + +**Quick filtering examples:** +``` +/google # Search for "google" anywhere +/port:443 # Filter by port +/process:firefox # Filter by process +/state:established # Filter by connection state +/dport:443 sni:github.com # Combine filters +``` + +**Sorting:** +- Press `s` to cycle through sortable columns (Process, Addresses, Service, Application, State, Bandwidth) +- Press `S` (Shift+s) to toggle sort direction +- Find bandwidth hogs: Press `s` until "Bandwidth Total ↓" appears (sorts by combined up+down speed) + +See [USAGE.md](USAGE.md) for complete filtering syntax and sorting guide. + +
+Advanced Filtering Examples + +**Keyword filters:** +- `port:44` - Ports containing "44" (443, 8080, 4433) +- `sport:80` - Source ports containing "80" +- `dport:443` - Destination ports containing "443" +- `src:192.168` - Source IPs containing "192.168" +- `dst:github.com` - Destinations containing "github.com" +- `process:ssh` - Process names containing "ssh" +- `sni:api` - SNI hostnames containing "api" +- `app:openssh` - SSH connections using OpenSSH +- `state:established` - Filter by protocol state +- `proto:tcp` - Filter by protocol type + +**State filtering:** +- `state:syn_recv` - Half-open connections (SYN flood detection) +- `state:established` - Established connections only +- `state:quic_connected` - Active QUIC connections +- `state:dns_query` - DNS query connections + +**Combined examples:** +- `sport:80 process:nginx` - Nginx connections from port 80 +- `dport:443 sni:google.com` - HTTPS to Google +- `process:firefox state:quic_connected` - Firefox QUIC connections +- `dport:22 app:openssh state:established` - Established OpenSSH connections + +
+ +
+Connection Lifecycle & Visual Indicators + +RustNet uses smart timeouts and visual warnings before removing connections: + +**Visual staleness indicators:** +- **White**: Active (< 75% of timeout) +- **Yellow**: Stale (75-90% of timeout) +- **Red**: Critical (> 90% of timeout) + +**Protocol-aware timeouts:** +- **HTTP/HTTPS**: 10 minutes (supports keep-alive) +- **SSH**: 30 minutes (long sessions) +- **TCP active**: 10 minutes, idle: 5 minutes +- **QUIC connected**: 3 minutes (or peer's transport-param idle timeout, when present); `Initial`/`Handshaking`: 60 seconds +- **DNS**: 30 seconds +- **TCP CLOSED**: 5 seconds + +Example: An HTTP connection turns yellow at 7.5 min, red at 9 min, and is removed at 10 min. + +See [USAGE.md](USAGE.md) for complete timeout details. + +
+ +## Documentation + +- **[INSTALL.md](INSTALL.md)** - Detailed installation instructions for all platforms, permission setup, and troubleshooting +- **[USAGE.md](USAGE.md)** - Complete usage guide including command-line options, filtering, sorting, and logging +- **[SECURITY.md](SECURITY.md)** - Security features including Landlock sandboxing and privilege management +- **[ARCHITECTURE.md](ARCHITECTURE.md)** - Technical architecture, platform implementations, and performance details +- **[PROFILING.md](PROFILING.md)** - Performance profiling guide with flamegraph setup and optimization tips +- **[ROADMAP.md](ROADMAP.md)** - Planned features and future improvements +- **[RELEASE.md](RELEASE.md)** - Release process for maintainers + +## Contributing + +Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on how to contribute. + +See [CONTRIBUTORS.md](CONTRIBUTORS.md) for a list of people who have contributed to this project. + +## License + +This project is licensed under the Apache License, Version 2.0 - see the [LICENSE](LICENSE) file for details. + +## Acknowledgments + +- Built with [ratatui](https://github.com/ratatui-org/ratatui) for the terminal UI +- Packet capture powered by [libpcap](https://www.tcpdump.org/) +- Inspired by tools like `tshark/wireshark/tcpdump`, `sniffnet`, `netstat`, `ss`, `iftop`, and [bandwhich](https://github.com/imsnif/bandwhich) +- Some code is vibe coded (OMG) / may the LLM gods be with you + +--- + +## Documentation Moved + +Some sections have been moved to dedicated files for better organization: + +- **Permissions Setup**: Now in [INSTALL.md - Permissions Setup](INSTALL.md#permissions-setup) +- **Installation Instructions**: Now in [INSTALL.md](INSTALL.md) +- **Detailed Usage**: Now in [USAGE.md](USAGE.md) +- **Architecture Details**: Now in [ARCHITECTURE.md](ARCHITECTURE.md) diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..b9b625e --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,7 @@ +# WeHub 来源说明 + +- 原始项目:`domcyrus/rustnet` +- 原始仓库:https://github.com/domcyrus/rustnet +- 导入方式:上游默认分支的最新快照 +- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准 +- 本文件仅用于记录来源,不代表 WeHub 是原项目作者 diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000..0ed642f --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,337 @@ +

+

RustNet

+

+ 面向终端的进程级网络监控工具:实时呈现 TCP、UDP、QUIC 连接,自带深度包检测,默认沙箱隔离运行。 +

+

+ Built With Ratatui + Build Status + Crates.io + GitHub Stars + License + GitHub release + Docker Image +

+

+ +

+ English | 简体中文 +

+ +

+ RustNet demo +

+ +

+ 实时洞察机器对外发起的每一条连接:谁在使用它、走的是什么协议。无需 tcpdump,无需 X11 转发,也不必把 root 权限传递下去。 +

+ +## 功能特性 + +- **进程级归属识别**:每一条 TCP、UDP、QUIC 连接都能追溯到所属进程。Linux 使用 eBPF,macOS 使用 PKTAP,Windows 与 FreeBSD 则走原生 API。Wireshark 与 tcpdump 做不到这一点;`netstat` / `ss` 也无法展示实时状态。 +- **深度包检测**:无需外部解析器即可识别 HTTP、带 SNI 的 HTTPS/TLS、DNS、SSH、FTP、QUIC、MQTT、BitTorrent、STUN、NTP、mDNS、LLMNR、DHCP、SNMP、SSDP 及 NetBIOS。 +- **安全沙箱**:Linux 5.13+ 使用 Landlock,macOS 使用 Seatbelt,Windows 通过 token 降权 + job-object 阻止子进程创建。libpcap 初始化完成后立即丢弃特权。详见 [SECURITY.zh-CN.md](SECURITY.zh-CN.md)。 +- **TCP 网络分析**:实时统计重传、乱序包、快重传,既有逐连接视图也有汇总视图。 +- **智能连接生命周期**:按协议设置超时,以白 → 黄 → 红的颜色指示过期程度。按 `t` 可保留历史(已关闭)连接以便事后追溯。 +- **Vim / fzf 风格过滤**:支持 `port:`、`src:`、`dst:`、`sni:`、`process:`、`state:`、`proto:`,以及 `/(?i)pattern/` 形式的正则。 +- **GeoIP 增强**:基于本地 MaxMind GeoLite2 数据库查询国家信息,不发起任何网络请求。 +- **跨平台**:Linux、macOS、Windows、FreeBSD。 + +## 为什么选 RustNet? + +RustNet 填补了简单连接工具(`netstat`、`ss`)与数据包分析器(`Wireshark`、`tcpdump`)之间的空白: + +- **进程归属**:看清每条连接归哪个应用所有。Wireshark 看不到这一层,因为它只看包,不看 socket。 +- **以连接为中心的视图**:逐连接实时追踪状态、带宽与协议。 +- **SSH 友好**:TUI 可直接在 SSH 会话中运行,远端服务器上发生了什么一眼可见,不必转发 X11 或抓包再回传。 + +RustNet 与抓包工具是互补关系。用 RustNet 看清*谁在发起连接*;若要直接在 Wireshark 中查看,可用 `--pcapng-export` 写出带 RustNet 数据包注释的 PCAPNG;若更重视清理阶段的元数据完整性,可用 `--pcap-export` 加 JSONL sidecar,再借 `scripts/pcap_enrich.py` 富化。参见 [USAGE.zh-CN.md 的 PCAP 导出章节](USAGE.zh-CN.md#pcap-export) 与 [ARCHITECTURE.zh-CN.md 的同类工具对比章节](ARCHITECTURE.zh-CN.md#comparison-with-similar-tools)。 + +基于 ratatui、libpcap、eBPF(libbpf-rs)、DashMap、crossbeam、ring、MaxMind GeoLite2 与 Landlock 构建。完整依赖清单见 [ARCHITECTURE.zh-CN.md](ARCHITECTURE.zh-CN.md#dependencies)。 + +
+基于 eBPF 的增强型进程识别(Linux 默认) + +RustNet 在 Linux 上默认使用内核 eBPF 程序进行进程识别,从而获得更高的性能与更低的开销。但这种方式也有一些重要的限制需要了解: + +**进程名长度限制:** +- eBPF 使用内核的 `comm` 字段,该字段最多只有 16 个字符 +- 显示的是任务 / 线程的命令名,而非完整的可执行路径 +- 多线程应用往往展示线程名而非主进程名 + +**真实场景示例:** +- **Firefox**:可能显示为 "Socket Thread"、"Web Content"、"Isolated Web Co" 或 "MainThread" +- **Chrome**:可能显示为 "ThreadPoolForeg"、"Chrome_IOThread"、"BrokerProcess" 或 "SandboxHelper" +- **Electron 应用**:经常显示为 "electron"、"node" 或内部线程名 +- **系统进程**:展示截断后的名字,如 "systemd-resolve" → "systemd-resolve" + +**回退行为:** +- 当 eBPF 加载失败或权限不足时,RustNet 会自动回退到基于 procfs 的标准进程识别方式 +- 标准模式可以拿到完整进程名,但 CPU 开销更高 +- eBPF 默认启用,无需任何特殊编译参数 + +如需关闭 eBPF、仅使用 procfs 模式,请这样构建: +```bash +cargo build --release --no-default-features +``` + +技术细节见 [ARCHITECTURE.zh-CN.md](ARCHITECTURE.zh-CN.md)。 + +
+ +
+网络接口统计监控 + +RustNet 在所有支持的平台上提供实时的网络接口统计: + +- **概览标签页**:展示当前活跃的接口,包含速率、错误数与丢包数 +- **接口标签页**(按 `3`):以详细表格呈现各接口的完整指标 +- **跨平台**:Linux(sysfs)、macOS / FreeBSD(getifaddrs)、Windows(GetIfTable2 API) +- **智能过滤**:Windows 上自动剔除虚拟 / 过滤类适配器 + +如何解读接口统计以及各平台的差异,详见 [USAGE.zh-CN.md](USAGE.zh-CN.md#interface-statistics)。 + +**可用指标:** +- 总字节数与包数(RX / TX) +- 错误计数(收 / 发) +- 丢包数(队列溢出) +- 冲突数(传统指标,现代网络中很少出现) + +数据由后台线程每 2 秒采集一次,对性能影响极小。 + +
+ +## 截图 + + + + + + + + + + +
概览
连接列表与实时统计、迷你折线图
详情
逐连接展示 SNI、加密套件、GeoIP、DPI
图表
流量曲线、应用分布、Top 进程
接口
各接口 RX / TX 历史曲线、错误与丢包
+ +## 快速上手 + +### 安装 + +**Homebrew(macOS / Linux):** +```bash +brew install rustnet +``` + +**Ubuntu(25.10+):** +```bash +sudo add-apt-repository ppa:domcyrus/rustnet +sudo apt update && sudo apt install rustnet +``` + +**Fedora(42+):** +```bash +sudo dnf copr enable domcyrus/rustnet +sudo dnf install rustnet +``` + +**openSUSE Tumbleweed:** +```bash +sudo zypper addrepo https://download.opensuse.org/repositories/home:/domcyrus:/rustnet/openSUSE_Tumbleweed/home:domcyrus:rustnet.repo +sudo zypper refresh +sudo zypper install rustnet +``` + +**Arch Linux:** +```bash +sudo pacman -S rustnet +``` + +**Nix / NixOS:** +```bash +nix-shell -p rustnet +# 然后在 shell 中执行: sudo rustnet +``` + +**通过 crates.io:** +```bash +cargo install rustnet-monitor +``` + +**Windows(Chocolatey):** +```powershell +# 需在管理员权限的 PowerShell 中执行 +# 需要先安装 Npcap(https://npcap.com),并启用 "WinPcap API-compatible Mode" +choco install rustnet +``` + +**其他平台:** +- **FreeBSD**:从 [rustnet-bsd releases](https://github.com/domcyrus/rustnet-bsd/releases) 下载 +- **Docker、源码构建、其他 Linux 发行版**:详见 [INSTALL.zh-CN.md](INSTALL.zh-CN.md) + +### 运行 RustNet + +抓包需要更高的权限: + +```bash +# 快速启动(所有平台) +sudo rustnet + +# Linux:为可执行文件赋予 Linux capabilities,即可免 sudo 运行(推荐) +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' $(which rustnet) +rustnet +``` + +**常用参数:** +```bash +rustnet -i eth0 # 指定网络接口 +rustnet --show-localhost # 显示 localhost 上的连接 +rustnet --no-resolve-dns # 关闭反向 DNS 解析(默认开启) +rustnet -r 500 # 设置刷新间隔(毫秒) +rustnet --theme classic # 原始全彩调色板(默认:muted) +rustnet --pcapng-export capture.pcapng # 导出带注释的 PCAPNG +``` + +权限配置详情见 [INSTALL.zh-CN.md](INSTALL.zh-CN.md),完整参数说明见 [USAGE.zh-CN.md](USAGE.zh-CN.md)。 + +> 如果已经设置了 Linux capabilities,但 TUI 仍然提示 `eBPF unavailable`,请参阅 [INSTALL.zh-CN.md 的排障章节](INSTALL.zh-CN.md#ebpf-unavailable-despite-capabilities-being-set)。 + +## 键盘控制 + +| 按键 | 作用 | +|-----|--------| +| `q` | 退出(连按两次确认) | +| `Ctrl+C` | 立即退出 | +| `x` | 清空所有连接(连按两次确认) | +| `Tab` 或 `]` | 下一个标签页 | +| `Shift+Tab` 或 `[` | 上一个标签页 | +| `1`–`5` | 直接跳转到 Overview / Details / Interfaces / Graph / Help | +| `↑/k` `↓/j` | 上下移动 | +| `g` `G` | 跳到第一条 / 最后一条连接 | +| `Enter` | 查看连接详情 | +| `Esc` | 返回或清除过滤器 | +| `c` | 复制远端地址 | +| `p` | 在服务名与端口之间切换 | +| `d` | 在主机名与 IP 之间切换 | +| `s` `S` | 切换排序列 / 切换排序方向 | +| `a` | 切换按进程分组 | +| `Space` | 展开 / 折叠进程分组 | +| `←/→` 或 `h/l` | 折叠 / 展开当前分组 | +| `PageUp/PageDown` 或 `Ctrl+B/F` | 翻页 | +| `t` | 切换是否显示历史(已关闭)连接 | +| `i` | 切换 System 信息侧边栏 | +| `r` | 重置视图(分组、排序、过滤) | +| `/` | 进入过滤模式 | +| `h` | 切换帮助 | + +完整键位说明与导航技巧见 [USAGE.zh-CN.md](USAGE.zh-CN.md)。 + +## 过滤与排序 + +**快速过滤示例:** +``` +/google # 全局搜索 "google" +/port:443 # 按端口过滤 +/process:firefox # 按进程过滤 +/state:established # 按连接状态过滤 +/dport:443 sni:github.com # 组合多个过滤条件 +``` + +**排序:** +- 按 `s` 在可排序的列之间循环切换(进程、地址、服务、应用、状态、带宽) +- 按 `S`(Shift+s)切换升序 / 降序 +- 想抓出带宽大户:连续按 `s` 直到显示 "Bandwidth Total ↓"(按上下行合计速度排序) + +完整的过滤语法与排序说明见 [USAGE.zh-CN.md](USAGE.zh-CN.md)。 + +
+高级过滤示例 + +**关键字过滤:** +- `port:44` —— 端口号包含 "44" 的连接(443、8080、4433) +- `sport:80` —— 源端口包含 "80" +- `dport:443` —— 目的端口包含 "443" +- `src:192.168` —— 源 IP 包含 "192.168" +- `dst:github.com` —— 目的地址包含 "github.com" +- `process:ssh` —— 进程名包含 "ssh" +- `sni:api` —— SNI 主机名包含 "api" +- `app:openssh` —— 使用 OpenSSH 的 SSH 连接 +- `state:established` —— 按协议状态过滤 +- `proto:tcp` —— 按协议类型过滤 + +**状态过滤:** +- `state:syn_recv` —— 半开连接(可用于发现 SYN flood) +- `state:established` —— 仅显示已建立的连接 +- `state:quic_connected` —— 活跃的 QUIC 连接 +- `state:dns_query` —— DNS 查询连接 + +**组合示例:** +- `sport:80 process:nginx` —— Nginx 从 80 端口发出的连接 +- `dport:443 sni:google.com` —— 到 Google 的 HTTPS +- `process:firefox state:quic_connected` —— Firefox 的 QUIC 连接 +- `dport:22 app:openssh state:established` —— 已建立的 OpenSSH 连接 + +
+ +
+连接生命周期与可视化指示 + +RustNet 在移除连接前会先用智能超时机制与颜色给出预警: + +**过期程度的颜色指示:** +- **白色**:活跃(< 75% 的超时时间) +- **黄色**:开始过期(75% – 90% 的超时时间) +- **红色**:即将过期(> 90% 的超时时间) + +**按协议设定的超时:** +- **HTTP / HTTPS**:10 分钟(支持 keep-alive) +- **SSH**:30 分钟(适配长会话) +- **TCP 活跃**:10 分钟;**TCP 空闲**:5 分钟 +- **QUIC 已连接**:3 分钟(若对端通过 transport 参数声明了 idle timeout,则以对端为准);`Initial` / `Handshaking` 阶段:60 秒 +- **DNS**:30 秒 +- **TCP CLOSED**:5 秒 + +举例:一条 HTTP 连接会在第 7.5 分钟变黄,第 9 分钟变红,第 10 分钟被移除。 + +完整超时说明见 [USAGE.zh-CN.md](USAGE.zh-CN.md)。 + +
+ +## 文档 + +- **[INSTALL.zh-CN.md](INSTALL.zh-CN.md)** —— 各平台的详细安装说明、权限配置与排障 +- **[USAGE.zh-CN.md](USAGE.zh-CN.md)** —— 完整使用手册,涵盖命令行参数、过滤、排序与日志 +- **[SECURITY.zh-CN.md](SECURITY.zh-CN.md)** —— 安全特性,包括 Landlock 沙箱与权限管理 +- **[ARCHITECTURE.zh-CN.md](ARCHITECTURE.zh-CN.md)** —— 技术架构、各平台实现与性能细节 +- **[CONTRIBUTING.zh-CN.md](CONTRIBUTING.zh-CN.md)** —— 贡献指南,包括工作流、质量要求与 AI 辅助贡献规范 +- **[PROFILING.zh-CN.md](PROFILING.zh-CN.md)** —— 性能分析指南,含 flamegraph 配置与优化建议 +- **[ROADMAP.md](ROADMAP.md)** —— 已规划的功能与后续改进 +- **[RELEASE.md](RELEASE.md)** —— 维护者发布流程 + +## 参与贡献 + +欢迎贡献!请阅读 [CONTRIBUTING.zh-CN.md](CONTRIBUTING.zh-CN.md) 了解贡献流程。 + +历来的贡献者名单见 [CONTRIBUTORS.md](CONTRIBUTORS.md)。 + +## 许可证 + +本项目采用 Apache License 2.0 许可证,详见 [LICENSE](LICENSE) 文件。 + +## 致谢 + +- 终端 UI 基于 [ratatui](https://github.com/ratatui-org/ratatui) 构建 +- 抓包能力由 [libpcap](https://www.tcpdump.org/) 提供 +- 灵感来自 `tshark/wireshark/tcpdump`、`sniffnet`、`netstat`、`ss`、`iftop`,以及 [bandwhich](https://github.com/imsnif/bandwhich) +- 部分代码靠手感写出(OMG)/ 愿 LLM 之神与你同在 + +--- + +## 已迁移的文档 + +部分章节已迁移到独立文件,以便更好地组织内容: + +- **权限配置**:迁移至 [INSTALL.zh-CN.md 的权限配置章节](INSTALL.zh-CN.md#permissions-setup) +- **安装说明**:迁移至 [INSTALL.zh-CN.md](INSTALL.zh-CN.md) +- **详细用法**:迁移至 [USAGE.zh-CN.md](USAGE.zh-CN.md) +- **架构细节**:迁移至 [ARCHITECTURE.zh-CN.md](ARCHITECTURE.zh-CN.md) diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..254e1d0 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,218 @@ +# Release Process + +This document is for maintainers releasing new versions of RustNet. + +## Changelog Maintenance (ongoing, not at release time) + +Notable changes are added to the `## [Unreleased]` section of `CHANGELOG.md` in +the same PR that makes them — don't wait until release time and reconstruct the +list from git history. Cutting a release then just renames that section (see +step 3 below). `pre-release-check.sh` warns if `[Unreleased]` still has content +after the rename, and the release workflow's notes extraction ignores it. + +## Creating a New Release + +### 1. Run Pre-Release Checks + +After updating versions and changelog, run the pre-release validation script: + +```bash +./scripts/pre-release-check.sh 1.2.0 +``` + +This validates version consistency, changelog entries, code quality (fmt/clippy/test), +Dockerfile correctness, and git status. Fix any errors before proceeding. + +### 2. Test Platform Builds + +Before tagging, verify all platform builds succeed on the current main branch: + +```bash +# Ensure you're on the main branch with latest changes +git checkout main +git pull origin main +``` + +1. Go to [Actions > Test Platform Builds](../../actions/workflows/test-platform-builds.yml) +2. Click "Run workflow" +3. Select `all` to test all platforms (including static Linux builds) +4. Wait for the workflow to complete successfully + +This catches cross-platform and static linking issues before you invest time in release prep. + +### 3. Prepare the Release + +> **Two version tracks since the workspace split.** `Cargo.toml` carries two +> versions: the binary's `[package] version` (line ~30, the user-facing `1.x` +> line that tags and packages follow) and `[workspace.package] version` (line +> ~9, the `0.x` library crates `rustnet-core`/`-capture`/`-host`, single source +> of truth also referenced from `[workspace.dependencies]`). A normal feature +> release bumps **only the binary `[package] version`** and `rpm/rustnet.spec`. +> Bump the library version separately, and only when the libraries actually +> change in a release-worthy way. + +Update the binary version in `Cargo.toml` and `rpm/rustnet.spec`, and turn the +accumulated `[Unreleased]` changelog section into the release entry: + +```bash +# Update Cargo.toml [package] version (e.g., version = "1.4.0") — NOT the +# [workspace.package] version unless you intend to bump the library crates. +# Update rpm/rustnet.spec Version field (e.g., Version: 1.4.0) + +# In CHANGELOG.md: +# 1. Rename "## [Unreleased]" to "## [0.3.0] - YYYY-MM-DD" (review/polish the entries) +# 2. Add a fresh, empty "## [Unreleased]" section above it +# 3. Update the comparison links at the bottom: +# [Unreleased]: https://github.com/domcyrus/rustnet/compare/v0.3.0...HEAD +# [0.3.0]: https://github.com/domcyrus/rustnet/compare/v0.2.0...v0.3.0 + +# Update Cargo.lock and test the build +cargo build --release +cargo test +``` + +### 4. Commit Release Changes + +```bash +# Stage and commit the version and changelog changes +git add Cargo.toml Cargo.lock CHANGELOG.md rpm/rustnet.spec +git commit -m "Release v0.3.0 + +- Feature or fix summary here +- Another change here +- And more changes" +``` + +### 5. Create and Push Git Tag + +```bash +# Create an annotated tag matching the version in Cargo.toml +git tag -a v0.3.0 -m "Release v0.3.0 + +- Feature or fix summary here +- Another change here +- And more changes" + +# Push both the commit and the tag +git push origin main +git push origin v0.3.0 +``` + +**That's it!** The GitHub Actions workflow will automatically: +- Build binaries for all platforms (Linux, macOS, Windows - multiple architectures) +- Create installer packages (DEB, RPM, DMG, MSI) +- Extract release notes from CHANGELOG.md +- Create a draft GitHub release with all artifacts attached +- Upload all binaries and installers to the release +- **Publish the release** (un-draft) once all assets are uploaded +- Trigger downstream package updates (Homebrew, Chocolatey, FreeBSD, PPA, COPR, AUR, Docker, crates.io) + +### 6. Verify the Release + +Once the GitHub Actions workflow completes (~15-20 minutes): + +1. Go to the [GitHub repository releases page](https://github.com/domcyrus/rustnet/releases) +2. Verify the release is published (no longer a draft) with all assets +3. Review the automatically extracted release notes +4. Check downstream package updates completed (Homebrew, Chocolatey, etc.) + +## Automated Release Workflow + +The release process is fully automated via [`.github/workflows/release.yml`](.github/workflows/release.yml): + +**Triggers:** +- Pushing a tag matching `v[0-9]+.[0-9]+.[0-9]+` (e.g., `v0.3.0`, `v1.2.3`) +- Manual workflow dispatch + +**What it does:** +1. **Builds cross-platform binaries:** + - Linux: x64, ARM64, ARMv7 (with eBPF support) + - macOS: Intel (x64) and Apple Silicon (ARM64) + - Windows: 64-bit and 32-bit + +2. **Creates installer packages:** + - **Linux:** DEB packages (amd64, arm64, armhf) and RPM packages (x86_64, aarch64) + - **macOS:** DMG installers with app bundles (supports code signing/notarization if secrets configured) + - **Windows:** MSI installers (64-bit and 32-bit) + +3. **Extracts release notes:** + - Automatically parses `CHANGELOG.md` to extract the version-specific section + - Falls back to auto-generated notes if no changelog entry is found + +4. **Creates GitHub release:** + - Creates a draft release with the tag name as title + - Attaches all binaries and installer packages + - Uses extracted changelog content as release notes + +5. **Publishes the workspace to crates.io** (via + [`.github/workflows/publish.yml`](.github/workflows/publish.yml), after the + GitHub release is published): the four crates are published in dependency + order — `rustnet-core` → `rustnet-capture` → `rustnet-host` → + `rustnet-monitor` — waiting for each to appear in the index before publishing + a dependent. The step is idempotent (it skips any `crate@version` already on + crates.io), so a re-run after a partial failure is safe. The library crates + use the `[workspace.package]` version; the binary uses its `[package]` + version. + +## Important: Never Move a Tag After Release + +**Never force-push or move a tag after the release pipeline has started.** Moving a tag +causes GitHub to regenerate source tarballs with different SHA checksums, which breaks +every downstream package manager that already cached the original checksums: + +- **AUR/Homebrew/Chocolatey**: checksum verification failures for end users +- **Launchpad PPA**: rejects uploads with the same version but different file contents +- **crates.io**: already published and cannot be re-published with the same version + +If a fix is needed after tagging, **create a patch release** (e.g., `v1.1.1`) instead. + +## Release Checklist + +Before pushing the tag, ensure: + +- [ ] Pre-release checks pass: `./scripts/pre-release-check.sh x.y.z` +- [ ] Test Platform Builds workflow passes for all platforms (including static) +- [ ] Binary version updated in `Cargo.toml` `[package]` (not `[workspace.package]` unless bumping the library crates) +- [ ] Version number updated in `rpm/rustnet.spec` (line 5: `Version: x.y.z`) +- [ ] `Cargo.lock` updated (via `cargo build`) +- [ ] `CHANGELOG.md`: `[Unreleased]` renamed to `## [x.y.z] - YYYY-MM-DD`, a fresh empty `[Unreleased]` added, comparison links updated +- [ ] All tests pass (`cargo test`) +- [ ] Changes committed to main branch +- [ ] Git tag created and pushed + +After GitHub Actions completes: + +- [ ] Verify release is published (automatically un-drafted after all assets uploaded) +- [ ] Verify all platform binaries built successfully +- [ ] Verify all installer packages created (DEB, RPM, DMG, MSI) +- [ ] Verify Docker image pushed to ghcr.io +- [ ] Verify all four crates published to crates.io (`rustnet-monitor`, `rustnet-core`, `rustnet-capture`, `rustnet-host`) and docs.rs built +- [ ] Review automatically extracted release notes +- [ ] Verify Homebrew formula updated at https://github.com/domcyrus/homebrew-rustnet +- [ ] Verify Chocolatey package updated at https://github.com/domcyrus/rustnet-chocolatey +- [ ] Verify FreeBSD build at https://github.com/domcyrus/rustnet-bsd +- [ ] Announce release (if applicable) + +## Maintenance: New Ubuntu and Fedora Releases + +This is an occasional task, not part of every release. When a new Ubuntu interim or LTS, or a new Fedora release, is published and we want RustNet packages to ship for it: + +1. **Ubuntu PPA**: add the new codename to the matrix in [`.github/workflows/ppa-release.yml`](.github/workflows/ppa-release.yml) (the `set-matrix` job's `releases=[...]` list and the `workflow_dispatch` choice options). Confirm the new series ships `rustc-1.88` (or whatever the current `rust-version` floor in `Cargo.toml` is). Reference: issue [#254](https://github.com/domcyrus/rustnet/issues/254) added Ubuntu 26.04 (Resolute) support. +2. **Fedora COPR**: add the new chroot in the COPR project settings at [https://copr.fedorainfracloud.org/coprs/domcyrus/rustnet/edit/](https://copr.fedorainfracloud.org/coprs/domcyrus/rustnet/edit/). The chroot list is managed in the COPR UI, not in this repo. +3. Trigger a `workflow_dispatch` of `Release to Ubuntu PPA` for the new codename to verify the build before relying on it from the next tagged release. + +Conversely, when an older Ubuntu series is no longer worth supporting (no `rustc-1.88` in archive, or end of life), remove it from the same two locations and update [INSTALL.md](INSTALL.md) and [debian/README.md](debian/README.md) to match. + +## Versioning + +RustNet follows [Semantic Versioning (SemVer)](https://semver.org/): + +- **MAJOR** version for incompatible API changes +- **MINOR** version for backward-compatible functionality additions +- **PATCH** version for backward-compatible bug fixes + +Examples: + +- `v0.1.0` → `v0.1.1` (bug fixes) +- `v0.1.1` → `v0.2.0` (new features) +- `v0.2.0` → `v1.0.0` (major changes, API stability) diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..cb49d80 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,270 @@ +# RustNet Roadmap + +This document outlines the planned features and improvements for RustNet. + +## Platform Support + +- [x] **macOS Support**: Full support including: + - BPF device access and permissions setup + - PKTAP (Packet Tap) headers for process identification from packet metadata + - Fallback to `lsof` system commands for process-socket associations + - DMG installation packages for Apple Silicon and Intel + - Homebrew installation support +- [x] **Windows Support**: Full functionality working with: + - Npcap SDK and runtime integration + - MSI installation packages for 64-bit and 32-bit + - Process identification via Windows IP Helper API (GetExtendedTcpTable/GetExtendedUdpTable) +- [x] **FreeBSD Support**: Full support including: + - Process identification via `sockstat` command parsing + - BPF device access and permissions setup + - Native libpcap packet capture + - Cross-compilation support from Linux +- [ ] **FreeBSD Capsicum Full Sandbox** (`cap_enter()`): Replace per-FD `cap_rights_limit()` with full capability mode to prevent file access and data exfiltration. Requires: + - Switch from `sockstat` subprocess to `libprocstat(3)` library calls for process lookup (eliminates `fork()`/`execve()` dependency) + - Integrate `libcasper` for privileged sysctl access from inside capability mode (`kern.proc.filedesc` is blocked in `cap_enter()`) + - Architecture: pre-fork a Casper service before `cap_enter()`, communicate over socket pair at runtime + - Write FFI bindings for `libprocstat` and `libcasper` (no Rust crate exists) + - Link against `-lprocstat -lcasper -lcap_sysctl` (system libraries on FreeBSD 10+) +- [ ] **Windows Sandbox Hardening**: Strengthen the current privilege-drop + Job Object setup with process mitigation policies (`SetProcessMitigationPolicy`), low-integrity execution, and evaluation of `CreateRestrictedToken` / AppContainer. +- [ ] **macOS Seatbelt Hardening**: The current Seatbelt profile is allow-default with targeted denies (user homes, system credential stores, outbound TCP/UDP, `process-exec` except `lsof`). Tighten further: + - **Deny-by-default writes**: rustnet only writes its log/PCAP/JSONL output, so flip `file-write*` to deny-by-default with a small allowlist. This blocks root-level persistence (`/Library/LaunchDaemons`, `/Library/LaunchAgents`, `/private/etc` cron/launchd, etc.) that the current allow-default write policy leaves open. Needs on-host validation that the TUI's writes to the already-open tty and the `lsof` child still work. + - **More credential read denies**: system TCC database (`/Library/Application Support/com.apple.TCC`), Kerberos keytabs, `master.passwd`/`sudoers`, saved network/Wi-Fi configuration (`/Library/Preferences/SystemConfiguration`). + - **Eventual deny-by-default reads**: whitelist the dyld shared cache, system frameworks, `/dev/bpf*`, resolver/locale/timezone data, and the GeoIP paths. Strongest containment, but fragile across macOS releases — requires a multi-version on-host test pass before shipping. +- [ ] **Linux Sandbox Hardening (capabilities + Landlock network)**: Landlock already enforces deny-by-default filesystem access on the post-sandbox worker threads, but two gaps remain when rustnet runs as root: + - **Drop all non-essential capabilities** (or clear the bounding set via `PR_CAPBSET_DROP`) before spawning the worker threads. Today only `CAP_NET_RAW`/`CAP_BPF`/`CAP_PERFMON` are dropped, so a root-launched process retains `CAP_DAC_OVERRIDE`, `CAP_SYS_ADMIN`, `CAP_SYS_MODULE`, etc. — which Landlock does not cover (non-filesystem/non-TCP abuse such as loading kernel modules). Running non-root with a `cap_net_raw` file capability already avoids this; the hardening is for the common `sudo rustnet` case. + - **UDP egress is not blocked**: Landlock ABI v4 only governs TCP `bind`/`connect`, so UDP exfiltration remains possible (accepted as low risk today, since filesystem reads are tightly contained). Revisit when a newer Landlock ABI adds UDP support. Note macOS Seatbelt already blocks both TCP and UDP. +- [ ] **OpenBSD and NetBSD Support**: Future platforms to support +- [x] **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 + +### 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 +- **Enhanced BTF Support**: Better compatibility across different kernel versions and distributions +- **Performance Optimizations**: Reduce eBPF map lookups and improve connection-to-process matching efficiency +- **Switch from kprobes to fentry/fexit or kprobe.multi**: Today we attach 7 kprobes via `perf_event_open(2)`, which is gated by `kernel.perf_event_paranoid`. On Debian 13 the default is `=3` (an out-of-tree patch that predates CAP_PERFMON and only honors CAP_SYS_ADMIN), so even with `cap_bpf,cap_perfmon+ep` set via `setcap`, attach fails with `-EACCES` until the user lowers the sysctl globally (issue #255). Debian bug #994044 was archived in 2025 without a fix, so this is unlikely to change in Forky (Debian 14, ~2027) without renewed pressure. Ubuntu has a parallel patch at `=4` that *was* updated to honor CAP_PERFMON in late 2025 (Jammy 5.15.0-165+, Noble 6.8.0-91+, Plucky 6.14.0-37+, Questing 6.17.0-14+, Resolute 6.18.0-8+), so Ubuntu users already work out of the box — Debian is the laggard. Both `BPF_PROG_TYPE_TRACING` (fentry/fexit, kernel ≥ 5.5) and `kprobe.multi` (fprobe-based, kernel ≥ 5.18) attach via `BPF_LINK_CREATE` and never call `perf_event_open` — they're gated only by CAP_BPF + CAP_PERFMON, so they would work out of the box on Debian regardless of the broken patch. fentry also gives entry args + return value in a single program (cleaner than kprobe + kretprobe). Mainline / Fedora / Arch / RHEL all default to paranoid=2 and the upstream direction (CAP_PERFMON, fprobe, BTF trampolines) is toward more privilege separation, not less, so this conversion is also future-aligned. Trade-off: raises minimum supported kernel from current (kprobes work back to ~4.x) to 5.5+ for fentry or 5.18+ for kprobe.multi — need to decide whether to keep a kprobe fallback for older kernels. + +### 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.) +- **Cross-Namespace Attribution for Kubernetes**: The current procfs fallback reads `/proc/net/tcp` from the reader's network namespace, so under `hostNetwork: true` (as used by kubectl-rustnet) it never sees sockets owned by pods in their own netns. The kubernetes feature ships a scoped per-PID `/proc//net/{tcp,tcp6,udp,udp6}` walker that covers TCP+UDP for kubepods PIDs, but it ticks at the enrichment interval and so misses sub-tick ephemeral flows. The complete fix lives in the eBPF layer: kprobes/fentry are netns-agnostic and fire at `connect()`/`accept()` time, but the current socket-tracker map is being pruned more aggressively than userspace can consume. Plan: extend map retention (or switch to a ring buffer of close events that userspace drains opportunistically), debug the "Map Lookup Miss" path under Kubernetes traffic patterns, and verify cross-namespace coverage end-to-end in a kind cluster. This work also benefits ICMP and raw-socket attribution, which procfs cannot reach. + +## Features + +### Monitoring & Protocol Support + +- [x] **Real-time Network Monitoring**: Monitor active TCP, UDP, ICMP, and ARP connections +- [x] **Connection States**: Comprehensive state tracking for: + - TCP states (ESTABLISHED, SYN_SENT, TIME_WAIT, CLOSED, etc.) + - QUIC states (QUIC_INITIAL, QUIC_HANDSHAKE, QUIC_CONNECTED, QUIC_DRAINING) + - DNS states (DNS_QUERY, DNS_RESPONSE) + - SSH states (BANNER, KEYEXCHANGE, AUTHENTICATION, ESTABLISHED) + - Activity states (UDP_ACTIVE, UDP_IDLE, UDP_STALE) +- [x] **Deep Packet Inspection (DPI)**: Application protocol detection: + - HTTP with host information + - HTTPS/TLS with SNI (Server Name Indication) + - DNS queries and responses + - SSH connections with version detection, software identification, and state tracking + - QUIC protocol with CONNECTION_CLOSE frame detection and RFC 9000 compliance +- [ ] **DPI Enhancements**: Improve deep packet inspection capabilities: + - Support more protocols (e.g. FTP, SMTP, IMAP, etc.) + - CDP/LLDP (network device discovery protocols) + - LACP (Link Aggregation Control Protocol) + - More accurate SNI detection for QUIC/HTTPS +- [x] **Connection Lifecycle Management**: Smart protocol-aware timeouts with visual staleness indicators (yellow at 75%, red at 90%) +- [x] **Process Identification**: Associate network connections with running processes (with experimental eBPF support on Linux) +- [x] **Service Name Resolution**: Identify well-known services using port numbers +- [x] **Cross-platform Support**: Works on Linux, macOS, Windows, and FreeBSD +- [x] **DNS Reverse Lookup**: Add optional hostname resolution (toggle between IP and hostname display) - `--resolve-dns` flag with `d` key toggle +- [ ] **IPv6 Support**: Full IPv6 connection tracking and display, including DNS resolution (needs testing) +- [ ] **VLAN Tag Detection**: Parse 802.1Q VLAN tags from packet headers to identify VLAN configurations +- [ ] **Passive Host Discovery**: Infer local network hosts from observed ARP requests/replies and other broadcast traffic without active scanning +- [ ] **MAC Vendor Lookup (OUI)**: Resolve MAC addresses to hardware vendor names using a local OUI database (e.g. "Apple", "Intel", "Ubiquiti") + +### Filtering & Search + +- [x] **Advanced Filtering**: Real-time vim/fzf-style filtering with: + - Navigate while typing filters + - Fuzzy search across all connection fields including DPI data + - Keyword filters: `port:`, `src:`, `dst:`, `sni:`, `process:`, `sport:`, `dport:`, `ssh:`, `state:` + - State filtering for all protocol states + - Exact port matching by default (`port:22` matches only port 22) + - Regular expression support via `/pattern/` syntax on any filter value + +### Sorting & Display + +- [x] **Sorting**: Comprehensive table sorting with: + - Sort by all columns: Protocol, Local/Remote Address, State, Service, Application, Bandwidth (Down/Up), Process + - Intuitive left-to-right column cycling with `s` key + - Direction toggle with `S` (Shift+s) for ascending/descending + - Visual indicators: cyan/underlined active column, arrows showing direction + - Smart defaults: bandwidth descending (show hogs), text ascending (alphabetical) + - Bandwidth sorting: sorts by combined up+down bandwidth total + - Seamless integration with filtering + +### Performance & Architecture + +- [x] **Multi-threaded Processing**: Concurrent packet processing across multiple threads +- [x] **Optional Logging**: Detailed logging with configurable log levels (disabled by default) + +### Packaging & Distribution + +- [x] **Package Distribution**: Pre-built packages available: + - [x] **macOS DMG packages**: Apple Silicon and Intel (via GitHub Actions release workflow) + - [x] **Windows MSI packages**: 64-bit and 32-bit (via cargo-wix) + - [x] **Linux DEB packages**: amd64, arm64, armhf (via cargo-deb) + - [x] **Linux RPM packages**: x86_64, aarch64 (via cargo-generate-rpm) + - [x] **Cargo crates.io**: Published as `rustnet-monitor` (version 0.10.0+) + - [x] **Docker images**: Available on GitHub Container Registry with eBPF support + - [x] **Homebrew formula**: Available in separate tap repository (domcyrus/rustnet) + +### Future Enhancements + +- [ ] **Internationalization (i18n)**: Support for multiple languages in the UI +- [x] **Connection History**: Store and display historical connection data (toggle with `t` key, up to 5,000 archived connections) +- [x] **PCAP Export**: Export packets to PCAP file with process attribution sidecar (`--pcap-export`) + - Standard PCAP format compatible with Wireshark/tcpdump + - Streaming JSONL sidecar with PID, process name, timestamps + - Python enrichment script to create annotated PCAPNG +- [x] **Native Annotated PCAPNG Export**: Export a Wireshark-ready PCAPNG file with live best-effort RustNet packet comments (`--pcapng-export`) + - Per-packet comments include process/PID, direction, DPI/SNI, and GeoIP/ASN when available + - Uses true capture timestamps and bounded attribution retry +- [ ] **Enhanced PCAP Metadata**: Richer process information in sidecar file + - Process executable full path (not just name) + - Command line arguments + - Working directory + - User/UID information + - Parent process information +- [ ] **Configuration File**: Support for persistent configuration: + - Custom color themes and UI styling + - Default filters and sort preferences + - Default process grouping (start with `group: true` in config) + - Color mode preference (disable colors via config, complementing `--no-color` flag) + - Per-interface settings + - Keybinding customization +- [ ] **Connection Alerts**: Notifications for new connections or suspicious activity +- [x] **GeoIP Integration**: Geographical location of remote IPs +- [x] **GeoIP City-Level Resolution**: Extend GeoIP to include city-level location data using GeoLite2-City database +- [ ] **Protocol Statistics**: Summary view of protocol distribution +- [ ] **Rate Limiting Detection**: Identify connections with unusual traffic patterns +- [ ] **Bufferbloat Detection**: Measure latency under load to identify bufferbloat issues on the network +- [ ] **PCAP Import/Replay**: Load a PCAP file (with optional JSON process attribution sidecar) and replay it in the TUI for offline analysis. Enables remote monitoring workflows: capture on a remote host with `--pcap-export`, transfer files, and replay locally with full process-attributed view +- [ ] **Route Table Display**: Show the system routing table in a user-friendly view within the TUI +- [ ] **Privacy/Redact Mode**: Obfuscate sensitive information (IPs, MACs, hostnames) in the TUI for safe screenshots and sharing. Include option to export connection details from the details view to a text file with privacy redaction applied + +## UI Improvements + +- [x] **Terminal User Interface**: TUI built with ratatui with adjustable column widths +- [x] **Sortable Columns**: Keyboard-based sorting by all table columns +- [x] **Keyboard Controls**: Comprehensive keyboard navigation (q, Ctrl+C, x, Tab, arrows, j/k, g/G, PageUp/Down, Enter, Esc, c, p, s, S, h, /, a, r, Space) +- [x] **Connection Details View**: Detailed information about selected connections (Enter key) +- [x] **Help Screen**: Toggle help screen with keyboard shortcuts (h key) +- [x] **Clipboard Support**: Copy remote address to clipboard (c key) +- [x] **Service/Port Toggle**: Toggle between service names and port numbers (p key) +- [x] **Platform-Specific CLI Help**: Show only relevant options per platform (hide Linux sandbox options on macOS, hide PKTAP notes on Linux) +- [x] **Connection Grouping**: Group connections by process with expandable tree view (press `a` to toggle, aggregated stats, Space/arrows to expand/collapse) +- [x] **Reset View**: Reset all view settings (grouping, sort, filter) with `r` key +- [ ] **Resizable Columns**: Dynamic column width adjustment +- [ ] **ASCII Graphs**: Terminal-based graphs for bandwidth/packet visualization +- [ ] **Mouse Support**: Click to select connections +- [ ] **Split Pane View**: Show multiple views simultaneously + +## Architecture + +### Workspace Split + +Restructure the single crate into a Cargo workspace (same GitHub repo) with clear separation of concerns: + +- [x] **rustnet-monitor** (binary, bin name `rustnet`): CLI, TUI, app event + loop, sandboxing (Landlock/Seatbelt), and interface statistics -- the + user-facing application; process attribution is delegated to `rustnet-host`. + (Package stays `rustnet-monitor` because the `rustnet` crate name is taken on + crates.io; the installed binary is `rustnet`.) +- [x] **rustnet-core** (library): Packet parsing, protocol types, DPI, + link-layer parsers, connection merging, and DNS/GeoIP/OUI lookups -- the + reusable, platform-independent, capture-independent analysis core. Lives at + `crates/rustnet-core`. (Named `rustnet-core` rather than `rustnet-net` to + avoid the redundant "net-net"; verified available on crates.io.) +- [x] **rustnet-capture** (library): the libpcap/Npcap-based capture backend -- + device selection, BPF filters, macOS PKTAP, TUN/TAP, and a raw-frame + `PacketReader`. Lives at `crates/rustnet-capture`. This is the **existing** + pcap code moved into its own crate (not a libpcap-free rewrite): the point of + the split is composability — a headless front-end (e.g. a Prometheus exporter) + can pair `rustnet-capture` + `rustnet-core` without the TUI, and a platform + wanting a bespoke capture path (e.g. the macOS pktap helper) can swap it out. + The macOS `DegradationReason` coupling was untangled by giving capture its own + `PktapUnavailable` enum, which the binary maps to its UI `DegradationReason`. +- [x] **rustnet-host** (library): Per-connection process attribution behind one + `ProcessLookup` trait -- eBPF/procfs on Linux, PKTAP/lsof on macOS, the IP + Helper API on Windows, and `sockstat` on FreeBSD. Lives at `crates/rustnet-host` + and owns the eBPF build tooling (the `socket_tracker.bpf.c` program and bundled + `vmlinux.h`). The binary injects PKTAP availability via `report_pktap_degradation`, + so the crate needs no dependency on `rustnet-capture`. +- [ ] **rustnet-helper** (binary): Minimal suid helper for macOS pktap privilege + separation (~100 lines, zero C deps — just `libc`). **Future work, not yet a + crate.** The root-gated pktap interface creation (`SIOCIFCREATE`) can only be + written and validated on real macOS hardware, so this is deferred until it can + be done for real rather than scaffolded. See "macOS Privilege Separation" below. + +Benefits: +- Clean dependency boundaries (helper has zero C dependencies) +- `rustnet-core` becomes independently useful as a Rust network analysis library +- Compile times improve (parallel crate compilation) +- `cargo install rustnet-monitor` continues to work unchanged + +**Status:** The workspace exists with `rustnet-monitor` (binary) depending on +`rustnet-core`, `rustnet-capture`, and `rustnet-host`. The binary's `src/network` +module re-exports `rustnet_core::network::*` and `rustnet_capture` (as `capture`) +so existing `crate::network::*` paths, integration tests, and benches are +unchanged. Net-only dependencies (`dns-lookup`, `ring`, `aes`, `flate2`, +`maxminddb`, `pnet_datalink`) and the baked-in `oui.gz` / `services` assets live +in `rustnet-core`; all pcap usage lives in `rustnet-capture`; and `procfs` / +`libbpf-rs` plus the eBPF programs and `vmlinux.h` live in `rustnet-host`. +`rustnet-core` also exposes a `ConnectionTracker` so headless tools can fold +captured packets into a live, lifecycle-managed connection table without the +TUI. Remaining work: the `rustnet-helper` macOS pktap suid helper (needs real +hardware). + +### macOS Privilege Separation (pktap without root) + +Currently pktap requires root because the macOS kernel enforces a root check (`SIOCIFCREATE` ioctl) when creating the pktap pseudo-interface. This is independent of BPF device permissions (ChmodBPF). The goal is to run the main RustNet process as a regular user while only the minimal helper runs privileged. + +**Approach**: Small suid helper binary that: +1. Opens `/dev/bpf*` and creates the pktap interface (requires root) +2. Configures BPF device (bind interface, set buffer size, immediate mode) +3. Locks the device with `BIOCLOCK` (prevents further configuration changes) +4. Passes the BPF file descriptor to the unprivileged RustNet process via Unix socket (`SCM_RIGHTS`) +5. Drops privileges and exits + +The main RustNet process reads packets directly from the received BPF fd using `read()` -- no libpcap needed on this path. The existing pktap header parser (`link_layer/pktap.rs`) already handles the packet format. BPF filter compilation is not needed since BPF filters are already incompatible with pktap. + +On Linux/Windows/FreeBSD, nothing changes -- libpcap is used as today, with the existing capability-based privilege model on Linux. + +Security properties: +- Helper is tiny (~100 lines of Rust, no C code) -- minimal attack surface as root +- `BIOCLOCK` prevents the unprivileged process from reconfiguring the capture device +- Seatbelt sandbox can still be applied to the main process after fd handoff +- Similar pattern to Wireshark's `dumpcap` but with a smaller privileged surface (no libpcap in the helper) + +## Development + +- [x] **Unit Tests**: Basic unit tests in 12+ source modules (DPI protocols, filtering, services, network capture, etc.) +- [x] **Integration Tests**: Platform-specific integration tests for Linux and macOS (tests/integration_tests.rs) +- [ ] **Comprehensive Test Coverage**: Expand test coverage across all modules +- [x] **CI/CD Pipeline**: Automated builds and releases for all platforms (GitHub Actions) + - [x] **Release workflow**: Multi-platform builds with cross-compilation + - [x] **Docker workflow**: Automated Docker image builds + - [x] **Rust workflow**: Basic CI checks +- [x] **Documentation**: Comprehensive README with usage guides, architecture overview, and troubleshooting +- [x] **Packaging/Distribution**: Create packages for easy installation on Linux, macOS, and Windows + - DMG packages with code signing + - MSI packages with code signing for Windows diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..0d19489 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,333 @@ +

English | 简体中文

+ +# Security + +RustNet processes untrusted network data, making defense-in-depth security critical. This document describes the security measures implemented. + +## Table of Contents + +- [Landlock Sandboxing (Linux)](#landlock-sandboxing-linux) +- [Seatbelt Sandboxing (macOS)](#seatbelt-sandboxing-macos) +- [FreeBSD Sandboxing](#freebsd-sandboxing) +- [Privilege Drop and Job Object Sandboxing (Windows)](#privilege-drop-and-job-object-sandboxing-windows) +- [Privilege Requirements](#privilege-requirements) +- [Read-Only Operation](#read-only-operation) +- [No External Communication](#no-external-communication) +- [Log File Privacy](#log-file-privacy) +- [eBPF Security](#ebpf-security) +- [Threat Model](#threat-model) +- [Supply Chain Security](#supply-chain-security) +- [Audit and Compliance](#audit-and-compliance) +- [Reporting Security Issues](#reporting-security-issues) + +## Landlock Sandboxing (Linux) + +On Linux 5.13+, RustNet uses [Landlock](https://landlock.io/) to restrict its own capabilities after initialization. This limits the damage if a vulnerability in packet parsing is exploited. + +### What Gets Restricted + +| Restriction | Kernel Version | Description | +|-------------|----------------|-------------| +| Filesystem | 5.13+ | Only `/proc` readable (for process identification) | +| Network | 6.4+ | TCP bind/connect blocked (RustNet is passive) | +| Capabilities | Any | `CAP_NET_RAW` dropped after pcap socket opened | +| Capabilities | Any | `CAP_BPF`, `CAP_PERFMON` dropped after eBPF programs loaded | +| Root uid | Any | When started as root (e.g. `sudo rustnet`), the process drops to the invoking user (`SUDO_UID`/`SUDO_GID`) or `nobody` after initialization | +| Privileges | 3.5+ | `PR_SET_NO_NEW_PRIVS` set by RustNet itself — always, even with `--no-sandbox` — prevents privilege escalation via setuid binaries | + +### How It Works + +1. **Initialization phase**: RustNet loads eBPF programs, opens packet capture handles, and creates log files +2. **Privilege lock**: `PR_SET_NO_NEW_PRIVS` is set (applied even when the sandbox is disabled) +3. **Capability drop**: `CAP_NET_RAW`, `CAP_BPF`, and `CAP_PERFMON` are removed from the process +4. **Root uid drop**: when running as root, the process switches to the invoking sudo user (or `nobody`) via `setresuid`/`setresgid`. Already-open capture sockets, eBPF programs, and log/export files keep working. This matters most on kernels without Landlock, where the uid drop is the main containment +5. **Landlock**: Restricts filesystem and network access + +### Security Benefits + +If an attacker exploits a vulnerability in DPI/packet parsing: +- Cannot read arbitrary files (credentials, configs, etc.) +- Cannot write to filesystem (except configured log paths) +- Cannot make outbound TCP connections (data exfiltration blocked) +- Cannot bind TCP ports (reverse shell blocked) +- Cannot create new raw sockets (capability dropped) +- Cannot escalate privileges via setuid binaries (`PR_SET_NO_NEW_PRIVS`, set even with `--no-sandbox`) +- Does not run as root: under `sudo rustnet` the process continues as the invoking user, so even on kernels without Landlock a compromise does not yield root + +### CLI Options + +``` +--no-sandbox Disable Landlock sandboxing, capability dropping, and the + root uid drop (PR_SET_NO_NEW_PRIVS is still set) +--sandbox-strict Require full sandbox enforcement or exit +--no-uid-drop Keep running as root instead of dropping to + SUDO_UID/SUDO_GID (or nobody) after initialization +``` + +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`. + +### Graceful Degradation + +- **Kernel < 5.13**: Sandboxing skipped, warning logged +- **Kernel 5.13-6.3**: Filesystem restrictions only +- **Kernel 6.4+**: Full filesystem + network restrictions +- **Docker**: Landlock may be restricted; app continues normally + +## Seatbelt Sandboxing (macOS) + +On macOS 10.5+, RustNet uses [Seatbelt](https://theapplewiki.com/wiki/Dev:Seatbelt) (`sandbox_init_with_parameters`) to restrict its own capabilities after initialization. This limits the damage if a vulnerability in packet parsing is exploited. + +### What Gets Restricted + +| Restriction | Description | +|-------------|-------------| +| Outbound network | TCP/UDP outbound blocked; Unix sockets (Mach IPC) allowed | +| Filesystem reads | User home directories blocked (`/Users`, `/var/root`); GeoIP paths explicitly allowed | +| Filesystem writes | All user home directories blocked (`/Users`, `/var/root`) | +| Filesystem writes | Only configured log, PCAP, and PCAPNG export paths writable | +| Process execution | All binaries blocked except `/usr/sbin/lsof` | +| Root uid | When started as root (e.g. `sudo rustnet`), the process drops to the invoking user (`SUDO_UID`/`SUDO_GID`) or `nobody` after initialization | + +### How It Works + +1. **Initialization phase**: RustNet opens packet capture handles (BPF/PKTAP) and creates log files +2. **Pre-create**: PCAP sidecar (`.connections.jsonl`) and PCAPNG export files are created before the sandbox so their paths are already valid allow targets, and are handed over to the uid-drop target so they stay writable after the drop +3. **Root uid drop**: when running as root, the process switches to the invoking sudo user (or `nobody`) via `setgid`/`setuid`. Already-open capture and log/export descriptors keep working +4. **Sandbox application**: `sandbox_init_with_parameters` is called; already-open file descriptors survive unchanged, only future operations are restricted + +### Profile Strategy + +RustNet uses an **allow-default** SBPL profile with targeted denies. A deny-default profile would require explicitly whitelisting all system libraries, Mach ports, locale data, fonts, and other OS internals — fragile and error-prone. Allow-default with targeted denies covers the primary threats (credential theft, data exfiltration, shell escapes) without operational risk. Specific deny rules block file reads/writes under user home directories, outbound network connections, and execution of all binaries except `/usr/sbin/lsof`. + +### Output File Support + +`--json-log`, `--pcap-export`, and `--pcapng-export` paths are passed to the SBPL profile as runtime parameters (`JSON_LOG_PATH`, `PCAP_PATH`, `PCAP_JSONL_PATH`, `PCAPNG_PATH`). The profile grants an explicit `allow file-write*` rule on each path, which takes precedence over the broader `/Users` deny rule via SBPL specificity. Unused parameters default to `/dev/null`. + +All three flags work normally within the sandbox. + +### Security Benefits + +If an attacker exploits a vulnerability in DPI/packet parsing: +- Cannot read SSH keys, AWS credentials, browser profiles, or other credential files under `/Users` +- Cannot write to SSH keys, AWS credentials, browser profiles, or other credential files +- Cannot make outbound TCP/UDP connections (data exfiltration blocked) +- Cannot open new raw network sockets +- Cannot execute binaries (no shell escapes via `/bin/sh`, `/usr/bin/curl`, etc.) +- Does not run as root: under `sudo rustnet` the process continues as the invoking user + +### CLI Options + +``` +--no-sandbox Disable Seatbelt sandboxing and the root uid drop +--sandbox-strict Require full sandbox enforcement or exit +--no-uid-drop Keep running as root instead of dropping to + SUDO_UID/SUDO_GID (or nobody) after initialization +``` + +Trade-off of the root uid drop: the default PKTAP attribution path is +unaffected (process metadata arrives in-band on the already-open capture fd), +but the lsof fallback (active when PKTAP is unavailable, e.g. with an explicit +`--interface`) then only sees the target user's processes. Use `--no-uid-drop` +if you rely on lsof attribution for other users' processes. + +### Why BestEffort is Default + +`sandbox_init_with_parameters` is a private (undocumented) macOS API. It has been stable since macOS 10.5 and is used by Chromium, Firefox, and Safari for process sandboxing, but it could theoretically change without notice. BestEffort degrades gracefully if the API behaves unexpectedly rather than preventing the app from running. Use `--sandbox-strict` to require sandboxing or abort. + +### Clipboard Behavior + +Unlike Linux Landlock, clipboard copy (`c` key) works normally under Seatbelt. macOS clipboard uses NSPasteboard, which communicates via Mach IPC over Unix domain sockets — the SBPL profile explicitly allows `(network-outbound (remote unix-socket))`. + +On Linux, clipboard requires access to Wayland sockets (`/run/user/UID/wayland-0`) or X11 sockets (`/tmp/.X11-unix/`). Landlock's deny-default model blocks these because they are not in the write-path allowlist, so clipboard is unavailable when Landlock is active. + +## FreeBSD Sandboxing + +FreeBSD does not currently have sandboxing enabled. A full Capsicum sandbox using `cap_enter()` with `libcasper` for privileged process lookup is planned — see [ROADMAP.md](ROADMAP.md) for details. + +### Root Uid Drop + +Until Capsicum lands, the primary containment on FreeBSD is a root privilege drop: when started as root (e.g. `sudo rustnet`), the process drops to the invoking user (`SUDO_UID`/`SUDO_GID`) or `nobody` after the BPF capture devices are open, via `setresuid`/`setresgid`. Already-open capture and log/export descriptors keep working, and pre-created export files are handed over to the target user. Note that `doas` does not set `SUDO_UID`, so doas users get the `nobody` fallback. + +Trade-off: process attribution uses `sockstat`, which as a non-root user only sees the target user's sockets. Use `--no-uid-drop` if you need attribution for other users' processes. + +``` +--no-uid-drop Keep running as root instead of dropping to + SUDO_UID/SUDO_GID (or nobody) after initialization +``` + +## Privilege Drop and Job Object Sandboxing (Windows) + +On Windows, RustNet removes dangerous privileges from the process token and applies a Job Object to prevent child process creation after initialization. + +### What Gets Restricted + +| Restriction | Description | +|-------------|-------------| +| Privilege removal | SeDebugPrivilege, SeTakeOwnershipPrivilege, SeBackupPrivilege, SeRestorePrivilege, and other dangerous privileges permanently removed | +| Child processes | Job Object blocks creation of child processes (reverse shell, exec-based exfiltration) | + +### How It Works + +1. **Initialization phase**: RustNet opens Npcap handles and creates log files +2. **Privilege removal**: `AdjustTokenPrivileges` with `SE_PRIVILEGE_REMOVED` permanently strips dangerous privileges from the process token +3. **Job Object**: A Job Object with `JOB_OBJECT_LIMIT_ACTIVE_PROCESS = 1` is applied, preventing any child process creation + +### Security Benefits + +If an attacker exploits a vulnerability in DPI/packet parsing: +- Cannot debug other processes (SeDebugPrivilege removed) +- Cannot take ownership of arbitrary files (SeTakeOwnershipPrivilege removed) +- Cannot bypass ACLs to read files (SeBackupPrivilege removed) +- Cannot spawn child processes (cmd.exe, powershell.exe, curl.exe — blocked by Job Object) +- Cannot load kernel drivers (SeLoadDriverPrivilege removed) + +### Limitations + +Windows sandboxing is weaker than Linux/macOS/FreeBSD: +- No filesystem restriction — Windows lacks a process-wide filesystem sandbox equivalent to Landlock or Seatbelt +- No network restriction — blocking outbound would break Npcap packet capture +- Privilege removal only affects privileges the elevated process held + +### CLI Options + +``` +--no-sandbox Disable privilege removal and job object +--sandbox-strict Require full sandbox enforcement or exit +``` + +## Privilege Requirements + +RustNet requires privileged access for packet capture: + +| Platform | Requirement | +|----------|-------------| +| Linux | `CAP_NET_RAW` capability or root | +| macOS | Root or BPF group membership (`access_bpf` group) | +| Windows | Administrator (for Npcap) | +| FreeBSD | Root or BPF device access | + +### Why Privileges Are Needed + +- **Raw socket access** - Intercept network traffic at low level (read-only, non-promiscuous mode) +- **BPF device access** - Load packet filters into kernel +- **eBPF programs** - Optional kernel probes for enhanced process tracking (Linux only) + +### Recommended: Capability-based Execution (Linux) + +Instead of running as root, grant only the required capabilities: + +```bash +# Modern Linux (5.8+): packet capture + eBPF +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' $(which rustnet) + +# Packet capture only (no eBPF process detection) +sudo setcap cap_net_raw+eip $(which rustnet) +``` + +Legacy pre-5.8 kernels required `CAP_SYS_ADMIN` for eBPF operations. RustNet +does not grant this broad capability automatically; use `CAP_NET_RAW` only and +let eBPF fall back to procfs unless you explicitly accept the extra risk. + +After sandbox application, `CAP_NET_RAW` and eBPF-loading capabilities are +dropped - the process retains only the minimum privileges needed. + +## Read-Only Operation + +RustNet only monitors traffic; it does not: +- Modify packets +- Block connections +- Inject traffic +- Alter routing tables +- Change firewall rules + +The packet capture is opened in non-promiscuous, read-only mode. + +## No External Communication + +RustNet operates entirely locally: +- No telemetry or analytics +- No network requests (except monitored traffic) +- No cloud services or remote APIs +- All data stays on your system + +## Log File Privacy + +Log files may contain sensitive information: +- IP addresses and ports +- Hostnames and SNI data +- Process names and PIDs +- DNS queries and responses + +**Best Practices:** +- Disable logging by default (no `--log-level` flag) +- Secure log directory permissions +- Implement log rotation and retention policies +- Review logs for sensitive data before sharing + +## eBPF Security + +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) +- Automatically falls back to procfs if eBPF fails + +## Threat Model + +**What RustNet protects against:** +- Unauthorized users cannot capture packets without proper permissions +- Capability-based permissions limit blast radius of compromise +- Landlock (Linux) and Seatbelt (macOS) sandboxes contain potential exploitation + +**What RustNet does NOT protect against:** +- Users with packet capture permissions can see all unencrypted traffic +- Root/Administrator users can modify RustNet or capture packets directly +- Physical access to the machine enables packet capture +- Network-level attacks (RustNet is a monitoring tool, not a security appliance) + +### Sandboxing as Root + +Both Landlock (Linux) and Seatbelt (macOS) enforce restrictions even when RustNet runs as root (UID 0). Once applied, the sandbox cannot be reversed from within the process — on Linux, RustNet sets `PR_SET_NO_NEW_PRIVS` directly before applying any restrictions (Landlock requires and would set it as well), which is irreversible per-process and applied even with `--no-sandbox`. + +However, sandboxing does **not** protect against supply chain attacks. A compromised binary would simply not apply the sandbox. Root can also: +- Pass `--no-sandbox` to skip sandboxing entirely (except `PR_SET_NO_NEW_PRIVS`) +- Unload the Landlock LSM kernel module +- Disable SIP on macOS (which controls sandbox enforcement) +- Use `ptrace` to modify a running process + +For this reason, running with fine-grained capabilities (`setcap cap_net_raw=eip`) is strongly preferred over running as root. + +## Supply Chain Security + +RustNet takes the following measures to protect against supply chain attacks: + +- **Dependency lockfile**: `Cargo.lock` is committed to the repository, pinning all transitive dependency versions and recording source checksums. This prevents silent version upgrades. +- **Security audit**: `cargo deny check` runs in CI on every push and pull request, checking dependencies against the RustSec Advisory Database and enforcing license, source, and wildcard-version policies (`deny.toml`). A scheduled daily workflow re-checks advisories against the committed `Cargo.lock`, so newly published advisories surface without requiring a push. +- **CI action pinning**: All GitHub Actions are pinned by commit SHA (not tags), preventing tag-rewriting attacks on upstream actions. +- **Conservative dependency policy**: New dependencies require justification and are reviewed for maintenance status and security track record (see `CONTRIBUTING.md`). +- **Build-time integrity**: The Windows Npcap SDK download in `build.rs` is verified against a hardcoded SHA256 checksum. +- **Code signing**: macOS releases are signed with an Apple Developer certificate and notarized. +- **Checksum verification**: All packaging workflows (Homebrew, Chocolatey, AUR) calculate and double-verify SHA256 checksums before publishing. + +### Limitations + +- `cargo install rustnet` fetches the latest compatible versions from crates.io and does **not** use `Cargo.lock`. Users building from source should verify the source tarball checksum. +- Build scripts (`build.rs`) and proc-macros execute arbitrary code at compile time. While all current dependencies are well-established crates, this is an inherent risk of the Rust build model. + +## Audit and Compliance + +For production environments: +- **Audit logging** of who runs RustNet with packet capture privileges +- **Network monitoring policies** and compliance with data protection regulations +- **User access reviews** for privileged network access +- **Automated capability management** via configuration management systems + +## Reporting Security Issues + +Please report security vulnerabilities via GitHub Issues or contact the maintainers directly. diff --git a/SECURITY.zh-CN.md b/SECURITY.zh-CN.md new file mode 100644 index 0000000..945ad23 --- /dev/null +++ b/SECURITY.zh-CN.md @@ -0,0 +1,328 @@ +

English | 简体中文

+ +# 安全 + +RustNet 处理不受信任的网络数据,因此纵深防御至关重要。本文档描述了已实现的安全措施。 + +## 目录 + +- [Landlock 沙箱(Linux)](#landlock-sandboxing-linux) +- [Seatbelt 沙箱(macOS)](#seatbelt-sandboxing-macos) +- [FreeBSD 沙箱](#freebsd-sandboxing) +- [权限剥离与 Job Object 沙箱(Windows)](#privilege-drop-and-job-object-sandboxing-windows) +- [权限需求](#privilege-requirements) +- [只读操作](#read-only-operation) +- [不主动对外通信](#no-external-communication) +- [日志文件隐私](#log-file-privacy) +- [eBPF 安全](#ebpf-security) +- [威胁模型](#threat-model) +- [供应链安全](#supply-chain-security) +- [审计与合规](#audit-and-compliance) +- [报告安全问题](#reporting-security-issues) + +## Landlock 沙箱(Linux) + +在 Linux 5.13+ 上,RustNet 使用 [Landlock](https://landlock.io/) 在初始化后限制自身的 Linux capabilities。这样即使包解析存在漏洞被利用,也能限制损害范围。 + +### 受限制的内容 + +| 限制项 | 内核版本 | 描述 | +|--------|----------|------| +| 文件系统 | 5.13+ | 仅 `/proc` 可读(用于进程识别) | +| 网络 | 6.4+ | 禁止 TCP bind/connect(RustNet 为被动模式) | +| Linux capabilities | 任意 | pcap socket 打开后丢弃 `CAP_NET_RAW` | +| Linux capabilities | 任意 | eBPF 程序加载后丢弃 `CAP_BPF`、`CAP_PERFMON`、`CAP_SYS_ADMIN` | +| root uid | 任意 | 以 root 启动时(如 `sudo rustnet`),初始化完成后降权到调用用户(`SUDO_UID`/`SUDO_GID`)或 `nobody` | +| 特权 | 3.5+ | `PR_SET_NO_NEW_PRIVS` 由 RustNet 自身设置——始终生效,即使使用 `--no-sandbox`——防止通过 setuid 二进制文件提升特权 | + +### 工作原理 + +1. **初始化阶段**:RustNet 加载 eBPF 程序、打开包捕获句柄、创建日志文件 +2. **特权锁定**:设置 `PR_SET_NO_NEW_PRIVS`(即使禁用沙箱也会应用) +3. **Linux capabilities 剥离**:移除 `CAP_NET_RAW`、`CAP_BPF`、`CAP_PERFMON` 和 `CAP_SYS_ADMIN` +4. **root uid 降权**:以 root 运行时,通过 `setresuid`/`setresgid` 切换到调用 sudo 的用户(或 `nobody`)。已打开的捕获 socket、eBPF 程序和日志/导出文件继续有效。在没有 Landlock 的内核上,这是主要的隔离手段 +5. **Landlock**:限制文件系统和网络访问 + +### 安全收益 + +如果攻击者利用 DPI/包解析中的漏洞: +- 无法读取任意文件(凭据、配置等) +- 无法写入文件系统(除配置的日志路径外) +- 无法建立出站 TCP 连接(阻止数据外泄) +- 无法绑定 TCP 端口(阻止反向 shell) +- 无法创建新的 raw socket(Linux capabilities 已剥离) +- 无法通过 setuid 二进制文件提升特权(`PR_SET_NO_NEW_PRIVS`,即使使用 `--no-sandbox` 也会设置) +- 不以 root 运行:使用 `sudo rustnet` 时进程会切换为调用用户,即使在没有 Landlock 的内核上,攻击者也无法获得 root + +### CLI 选项 + +``` +--no-sandbox 禁用 Landlock 沙箱、Linux capabilities 剥离和 root uid 降权 + (仍会设置 PR_SET_NO_NEW_PRIVS) +--sandbox-strict 要求完整沙箱强制生效,否则退出 +--no-uid-drop 初始化后保持 root 运行, + 不降权到 SUDO_UID/SUDO_GID(或 nobody) +``` + +root uid 降权的权衡:降权后,procfs 回退路径的进程归属只能检查目标用户拥有的进程, +`/var/log/pods` 下的 Kubernetes 日志目录也可能不可读。eBPF 快速路径(默认)不受影响。 +如果依赖纯 procfs 归属(如未启用 eBPF 的构建)且需要归属其他用户的进程,请使用 +`--no-uid-drop`。 + +### 优雅降级 + +- **Kernel < 5.13**:跳过沙箱,记录警告 +- **Kernel 5.13-6.3**:仅文件系统限制 +- **Kernel 6.4+**:完整的文件系统 + 网络限制 +- **Docker**:Landlock 可能受限;应用正常运行 + +## Seatbelt 沙箱(macOS) + +在 macOS 10.5+ 上,RustNet 使用 [Seatbelt](https://theapplewiki.com/wiki/Dev:Seatbelt)(`sandbox_init_with_parameters`)在初始化后限制自身能力。这样即使包解析存在漏洞被利用,也能限制损害范围。 + +### 受限制的内容 + +| 限制项 | 描述 | +|--------|------| +| 出站网络 | TCP/UDP 出站被阻止;Unix socket(Mach IPC)允许 | +| 文件系统读取 | 禁止读取用户主目录(`/Users`、`/var/root`);GeoIP 路径显式允许 | +| 文件系统写入 | 禁止写入所有用户主目录(`/Users`、`/var/root`) | +| 文件系统写入 | 仅配置的日志、JSON log、PCAP 和 PCAPNG 导出路径可写 | +| 进程执行 | 除 `/usr/sbin/lsof` 外,禁止执行所有二进制文件 | +| root uid | 以 root 启动时(如 `sudo rustnet`),初始化完成后降权到调用用户(`SUDO_UID`/`SUDO_GID`)或 `nobody` | + +### 工作原理 + +1. **初始化阶段**:RustNet 打开包捕获句柄(BPF/PKTAP)并创建日志文件 +2. **预创建**:PCAP sidecar 文件(`.connections.jsonl`)和 PCAPNG 输出文件在沙箱应用前创建,因此其路径已经是有效的允许目标;同时移交给降权目标用户,确保降权后仍可写入 +3. **root uid 降权**:以 root 运行时,通过 `setgid`/`setuid` 切换到调用 sudo 的用户(或 `nobody`)。已打开的捕获和日志/导出文件描述符继续有效 +4. **沙箱应用**:调用 `sandbox_init_with_parameters`;已打开的文件描述符保持不变,仅限制未来的操作 + +### 配置文件策略 + +RustNet 使用 **默认允许** 的 SBPL 配置文件配合针对性拒绝。拒绝默认的配置文件需要显式将所有系统库、Mach 端口、区域设置数据、字体和其他 OS 内部组件加入白名单——脆弱且容易出错。默认允许配合针对性拒绝覆盖了主要威胁(凭据窃取、数据外泄、shell 逃逸),同时避免操作风险。具体的拒绝规则阻止对用户主目录下的文件读/写、出站网络连接,以及除 `/usr/sbin/lsof` 外所有二进制文件的执行。 + +### 输出文件支持 + +`--json-log`、`--pcap-export` 和 `--pcapng-export` 路径通过运行时参数(`JSON_LOG_PATH`、`PCAP_PATH`、`PCAP_JSONL_PATH`、`PCAPNG_PATH`)传递给 SBPL 配置文件。配置文件为每个路径授予显式的 `allow file-write*` 规则,该规则通过 SBPL 的特异性优先于更宽泛的 `/Users` 拒绝规则。未使用的参数默认为 `/dev/null`。 + +三个标志在沙箱内均可正常工作。 + +### 安全收益 + +如果攻击者利用 DPI/包解析中的漏洞: +- 无法读取 `/Users` 下的 SSH 密钥、AWS 凭据、浏览器配置文件或其他凭据文件 +- 无法写入 `/Users` 下的 SSH 密钥、AWS 凭据、浏览器配置文件或其他凭据文件 +- 无法建立出站 TCP/UDP 连接(阻止数据外泄) +- 无法打开新的 raw network socket +- 无法执行二进制文件(不能通过 `/bin/sh`、`/usr/bin/curl` 等逃逸 shell) +- 不以 root 运行:使用 `sudo rustnet` 时进程会切换为调用用户 + +### CLI 选项 + +``` +--no-sandbox 禁用 Seatbelt 沙箱和 root uid 降权 +--sandbox-strict 要求完整沙箱强制生效,否则退出 +--no-uid-drop 初始化后保持 root 运行, + 不降权到 SUDO_UID/SUDO_GID(或 nobody) +``` + +root uid 降权的权衡:默认的 PKTAP 归属路径不受影响(进程元数据随已打开的捕获 +fd 带内到达),但 lsof 回退路径(PKTAP 不可用时启用,例如显式指定 `--interface`) +降权后只能看到目标用户的进程。如果依赖 lsof 归属其他用户的进程,请使用 +`--no-uid-drop`。 + +### 为什么默认使用 BestEffort + +`sandbox_init_with_parameters` 是 macOS 的私有(未公开)API。自 macOS 10.5 以来一直保持稳定,Chromium、Firefox 和 Safari 都使用它进行进程沙箱,但理论上可能在没有通知的情况下发生变化。BestEffort 在 API 行为异常时优雅降级,而不是阻止应用运行。使用 `--sandbox-strict` 可要求沙箱生效,否则中止。 + +### 剪贴板行为 + +与 Linux Landlock 不同,在 Seatbelt 下剪贴板复制(`c` 键)正常工作。macOS 剪贴板使用 NSPasteboard,通过 Mach IPC 在 Unix domain socket 上通信——SBPL 配置文件显式允许 `(network-outbound (remote unix-socket))`。 + +在 Linux 上,剪贴板需要访问 Wayland socket(`/run/user/UID/wayland-0`)或 X11 socket(`/tmp/.X11-unix/`)。Landlock 的拒绝默认模型会阻止这些,因为它们不在写路径的允许列表中,因此当 Landlock 激活时剪贴板不可用。 + +## FreeBSD 沙箱 + +FreeBSD 当前未启用沙箱。计划使用 `cap_enter()` 配合 `libcasper` 实现完整的 Capsicum 沙箱,用于特权进程查找——详见 [ROADMAP.md](ROADMAP.md)。 + +### root uid 降权 + +在 Capsicum 落地之前,FreeBSD 上的主要隔离手段是 root 降权:以 root 启动时(如 `sudo rustnet`),BPF 捕获设备打开后,进程通过 `setresuid`/`setresgid` 降权到调用用户(`SUDO_UID`/`SUDO_GID`)或 `nobody`。已打开的捕获和日志/导出文件描述符继续有效,预创建的导出文件会移交给目标用户。注意 `doas` 不设置 `SUDO_UID`,因此 doas 用户会回退到 `nobody`。 + +权衡:进程归属使用 `sockstat`,非 root 用户只能看到目标用户的 socket。如需归属其他用户的进程,请使用 `--no-uid-drop`。 + +``` +--no-uid-drop 初始化后保持 root 运行, + 不降权到 SUDO_UID/SUDO_GID(或 nobody) +``` + +## 权限剥离与 Job Object 沙箱(Windows) + +在 Windows 上,RustNet 在初始化后从进程令牌中移除危险特权,并应用 Job Object 阻止子进程创建。 + +### 受限制的内容 + +| 限制项 | 描述 | +|--------|------| +| 特权移除 | 永久移除 SeDebugPrivilege、SeTakeOwnershipPrivilege、SeBackupPrivilege、SeRestorePrivilege 等危险特权 | +| 子进程 | Job Object 阻止创建子进程(反向 shell、基于 exec 的数据外泄) | + +### 工作原理 + +1. **初始化阶段**:RustNet 打开 Npcap 句柄并创建日志文件 +2. **特权移除**:`AdjustTokenPrivileges` 配合 `SE_PRIVILEGE_REMOVED` 永久从进程令牌中剥离危险特权 +3. **Job Object**:应用 `JOB_OBJECT_LIMIT_ACTIVE_PROCESS = 1` 的 Job Object,阻止任何子进程创建 + +### 安全收益 + +如果攻击者利用 DPI/包解析中的漏洞: +- 无法调试其他进程(SeDebugPrivilege 已移除) +- 无法取得任意文件的所有权(SeTakeOwnershipPrivilege 已移除) +- 无法通过 ACL 绕过读取文件(SeBackupPrivilege 已移除) +- 无法生成子进程(cmd.exe、powershell.exe、curl.exe —— 被 Job Object 阻止) +- 无法加载内核驱动(SeLoadDriverPrivilege 已移除) + +### 局限性 + +Windows 沙箱弱于 Linux/macOS/FreeBSD: +- 无文件系统限制 —— Windows 缺少与 Landlock 或 Seatbelt 等效的进程级文件系统沙箱 +- 无网络限制 —— 阻止出站会中断 Npcap 包捕获 +- 特权移除仅影响提升进程已拥有的特权 + +### CLI 选项 + +``` +--no-sandbox 禁用特权移除和 Job Object +--sandbox-strict 要求完整沙箱强制生效,否则退出 +``` + +## 权限需求 + +RustNet 需要特权访问来捕获网络数据包: + +| 平台 | 需求 | +|------|------| +| Linux | `CAP_NET_RAW` 这项 Linux capability 或 root | +| macOS | Root 或 BPF 组成员(`access_bpf` 组) | +| Windows | Administrator(用于 Npcap) | +| FreeBSD | Root 或 BPF 设备访问 | + +### 为什么需要特权 + +- **Raw socket 访问** —— 在低层拦截网络流量(只读、非混杂模式) +- **BPF 设备访问** —— 将包过滤器加载到内核 +- **eBPF 程序** —— 可选的内核探针,用于增强进程追踪(仅限 Linux) + +### 推荐:基于 Linux capabilities 的执行(Linux) + +与其以 root 运行,不如仅授予所需的 Linux capabilities: + +```bash +# 现代 Linux(5.8+):包捕获 + eBPF +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' $(which rustnet) + +# 仅包捕获(eBPF 会回退到 procfs) +sudo setcap cap_net_raw+eip $(which rustnet) +``` + +旧版 pre-5.8 内核需要宽泛的 `CAP_SYS_ADMIN` 才能执行 eBPF 操作。RustNet +的安装包不会自动授予该 capability;除非你明确接受该风险,否则请只授予 +`CAP_NET_RAW` 并使用 procfs 回退。沙箱应用后,`CAP_NET_RAW` 和 eBPF +加载相关 capabilities 会被丢弃——进程仅保留所需的最小特权。 + +## 只读操作 + +RustNet 仅监控流量,不会: +- 修改数据包 +- 阻断连接 +- 注入流量 +- 更改路由表 +- 更改防火墙规则 + +包捕获以非混杂、只读模式打开。 + +## 不主动对外通信 + +RustNet 完全在本地运行: +- 无遥测或分析 +- 无网络请求(除监控的流量外) +- 无云服务或远程 API +- 所有数据保留在你的系统上 + +## 日志文件隐私 + +日志文件可能包含敏感信息: +- IP 地址和端口 +- 主机名和 SNI 数据 +- 进程名和 PID +- DNS 查询和响应 + +**最佳实践:** +- 默认禁用日志记录(不使用 `--log-level` 标志) +- 保护日志目录权限 +- 实施日志轮转和保留策略 +- 分享前检查日志中的敏感数据 + +## eBPF 安全 + +使用 eBPF 进行增强型进程检测时(Linux 默认): + +- 现代内核需要额外的 Linux capabilities(`CAP_BPF`、`CAP_PERFMON`) +- eBPF 程序在加载前由内核验证 +- 仅限只读操作(不修改数据包) +- 如果 eBPF 失败,自动回退到 procfs + +## 威胁模型 + +**RustNet 防护的内容:** +- 未经授权的用户无法在没有适当权限的情况下捕获数据包 +- 基于 Linux capabilities 的权限限制了被入侵后的影响范围 +- Landlock(Linux)和 Seatbelt(macOS)沙箱限制潜在的漏洞利用 + +**RustNet 不防护的内容:** +- 拥有包捕获权限的用户可以看到所有未加密的流量 +- Root/Administrator 用户可以直接修改 RustNet 或捕获数据包 +- 对机器的物理访问可以捕获数据包 +- 网络级攻击(RustNet 是监控工具,不是安全设备) + +### 以 Root 身份运行时的沙箱 + +Landlock(Linux)和 Seatbelt(macOS)即使在 RustNet 以 root(UID 0)运行时也会强制执行限制。沙箱一旦应用就无法从进程内部撤销——在 Linux 上,RustNet 会在应用任何限制之前直接设置 `PR_SET_NO_NEW_PRIVS`(Landlock 也需要并会同样设置它),该设置对每个进程不可逆,并且即使使用 `--no-sandbox` 也会应用。 + +然而,沙箱**不能**防护供应链攻击。被入侵的二进制文件可以直接不应用沙箱。Root 也可以: +- 传递 `--no-sandbox` 完全跳过沙箱(`PR_SET_NO_NEW_PRIVS` 除外) +- 卸载 Landlock LSM 内核模块 +- 在 macOS 上禁用 SIP(控制沙箱强制执行) +- 使用 `ptrace` 修改运行中的进程 + +因此,强烈建议使用细粒度 Linux capabilities(`setcap cap_net_raw=eip`)运行,而不是以 root 运行。 + +## 供应链安全 + +RustNet 采取以下措施防护供应链攻击: + +- **依赖锁文件**:`Cargo.lock` 已提交到仓库,固定所有传递依赖版本并记录源校验和。这防止静默版本升级。 +- **安全审计**:`cargo deny check` 在每次 push 和 pull request 时于 CI 中运行,对照 RustSec Advisory Database 检查依赖,并强制执行许可证、来源和通配符版本策略(`deny.toml`)。一个每日定时工作流会针对已提交的 `Cargo.lock` 重新检查安全公告,因此新发布的公告无需 push 即可被发现。 +- **CI action 固定**:所有 GitHub Actions 均通过 commit SHA(而非标签)固定,防止对上游 action 的标签重写攻击。 +- **保守的依赖策略**:新依赖需要说明理由,并审查其维护状态和安全记录(参见 [CONTRIBUTING.zh-CN.md](CONTRIBUTING.zh-CN.md))。 +- **构建时完整性**:Windows Npcap SDK 下载在 `build.rs` 中对照硬编码的 SHA256 校验和进行验证。 +- **代码签名**:macOS 发布版本使用 Apple Developer 证书签名并进行公证。 +- **校验和验证**:所有打包工作流(Homebrew、Chocolatey、AUR)在发布前计算并双重验证 SHA256 校验和。 + +### 局限性 + +- `cargo install rustnet` 从 crates.io 获取最新兼容版本,并**不**使用 `Cargo.lock`。从源码构建的用户应验证源 tarball 校验和。 +- 构建脚本(`build.rs`)和 proc-macros 在编译时执行任意代码。虽然所有当前依赖都是久经考验的 crate,但这是 Rust 构建模型的固有风险。 + +## 审计与合规 + +对于生产环境: +- 审计记录谁以包捕获权限运行 RustNet +- 网络监控策略和数据保护法规合规 +- 对特权网络访问进行用户访问审查 +- 通过配置管理系统实现自动化 Linux capabilities 管理 + +## 报告安全问题 + +请通过 GitHub Issues 报告安全漏洞,或直接与维护者联系。 diff --git a/USAGE.md b/USAGE.md new file mode 100644 index 0000000..9cc0307 --- /dev/null +++ b/USAGE.md @@ -0,0 +1,1189 @@ +

English | 简体中文

+ +# Usage Guide + +This guide covers detailed usage of RustNet, including command-line options, keyboard controls, filtering, sorting, and understanding connection lifecycle. + +## Table of Contents + +- [Running RustNet](#running-rustnet) +- [Command-line Options](#command-line-options) +- [Keyboard Controls](#keyboard-controls) +- [Mouse Controls](#mouse-controls) +- [Filtering](#filtering) +- [Sorting](#sorting) +- [Process Grouping](#process-grouping) +- [Network Statistics Panel](#network-statistics-panel) +- [Interface Statistics](#interface-statistics) +- [Connection Lifecycle & Visual Indicators](#connection-lifecycle--visual-indicators) +- [Logging](#logging) + +## Running RustNet + +Packet capture requires elevated privileges on most systems. See [INSTALL.md](INSTALL.md) for detailed permission setup instructions. + +**Quick start:** + +```bash +# Run with sudo (works on all platforms) +sudo rustnet + +# Or grant capabilities to run without sudo (see INSTALL.md for details) +# Linux example (modern kernel 5.8+): +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' /path/to/rustnet +rustnet +``` + +**Basic usage examples:** + +```bash +# Run with default settings +# macOS: Uses PKTAP for process metadata +# Linux/Other: Auto-detects active interface +rustnet + +# Specify network interface +rustnet -i eth0 +rustnet --interface wlan0 + +# Linux: Monitor all interfaces simultaneously +rustnet -i any + +# Filter out localhost connections (already filtered by default) +rustnet --no-localhost + +# Show localhost connections (override default filtering) +rustnet --show-localhost + +# Set UI refresh interval (in milliseconds) +rustnet -r 500 +rustnet --refresh-interval 2000 + +# Disable deep packet inspection +rustnet --no-dpi + +# Restore the original full-color palette +rustnet --theme classic + +# Disable reverse DNS lookups (enabled by default) +rustnet --no-resolve-dns + +# Enable logging with specific level (options: error, warn, info, debug, trace) +rustnet -l debug +rustnet --log-level info + +# View help and all options +rustnet --help +``` + +## Command-line Options + +``` +Usage: rustnet [OPTIONS] + +Options: + -i, --interface Network interface to monitor + --no-localhost Filter out localhost connections (default: filtered) + --show-localhost Show localhost connections (overrides default filtering) + -r, --refresh-interval UI refresh interval in milliseconds [default: 1000] + --no-dpi Disable deep packet inspection + --no-resolve-dns Disable reverse DNS lookups (enabled by default) + --show-ptr-lookups Show PTR lookup connections (hidden by default) + -l, --log-level Set the log level (if not provided, no logging will be enabled) + --json-log Enable JSON logging of connection events to specified file + --pcap-export Export captured packets to PCAP file for Wireshark analysis + --pcapng-export Export captured packets to annotated PCAPNG file for Wireshark analysis + --no-color Disable all colors in the UI (also respects NO_COLOR env var) + --theme Color theme preset: "muted" (single accent, color reserved + for signals, default) or "classic" (original full-color palette) + --geoip-country Path to GeoLite2-Country.mmdb (auto-discovered if not specified) + --geoip-asn Path to GeoLite2-ASN.mmdb (auto-discovered if not specified) + --geoip-city Path to GeoLite2-City.mmdb (auto-discovered if not specified) + --no-geoip Disable GeoIP lookups entirely + -f, --bpf-filter BPF filter expression for packet capture + --no-sandbox Disable Landlock sandboxing (Linux only) + --sandbox-strict Require full sandbox enforcement or exit (Linux only) + --no-uid-drop Keep running as root instead of dropping to + SUDO_UID/SUDO_GID (or nobody) after initialization (Linux, macOS, and FreeBSD) + -h, --help Print help + -V, --version Print version +``` + +Builds compiled with the optional `kubernetes` feature (including the official Docker image) additionally expose `--kubernetes `. See [`--kubernetes`](#--kubernetes-mode-optional-feature) below. + +### Option Details + +#### `-i, --interface ` + +Specify which network interface to monitor. + +**Default behavior (no `-i` flag):** +- **macOS**: Automatically uses PKTAP for enhanced process metadata (requires sudo) +- **Linux/Other**: Auto-detects the first available non-loopback interface + +**Examples:** +```bash +# Default: Auto-detect interface (PKTAP on macOS) +rustnet + +# Linux: Monitor all interfaces using the special "any" pseudo-interface +rustnet -i any + +# Monitor specific interfaces +rustnet -i eth0 # Monitor Ethernet interface +rustnet -i wlan0 # Monitor WiFi interface +rustnet -i en0 # Monitor macOS primary interface + +# Monitor VPN and tunnel interfaces (TUN/TAP support) +rustnet -i utun0 # macOS VPN tunnel (TUN, Layer 3) +rustnet -i tun0 # Linux/BSD VPN tunnel (TUN, Layer 3) +rustnet -i tap0 # TAP interface (Layer 2, includes Ethernet) +``` + +**TUN/TAP Interface Support:** + +RustNet fully supports monitoring VPN and virtual network interfaces: + +- **TUN interfaces** (Layer 3): Carry IP packets directly without Ethernet headers + - Common on VPNs: WireGuard, OpenVPN (tun mode), Tailscale + - Examples: `utun0-utun9` (macOS), `tun0-tun9` (Linux/BSD) + +- **TAP interfaces** (Layer 2): Include full Ethernet frames + - Used by: OpenVPN (tap mode), QEMU/KVM virtual networks, Docker + - Examples: `tap0-tap9` (Linux/BSD) + +RustNet automatically detects TUN/TAP interfaces and adjusts packet parsing accordingly. The interface type is displayed in the UI status area. + +**Platform-specific notes:** +- **macOS**: Without `-i`, PKTAP is used automatically for better process detection. Use `-i ` to monitor a specific interface instead +- **Linux**: Use `-i any` to capture on all interfaces simultaneously (not available on other platforms) +- **TUN/TAP**: Fully supported on all platforms - RustNet detects interface type by name and adjusts parsing +- **All platforms**: If you specify a non-existent interface, an error will show available interfaces + +**Finding your interfaces:** +- Linux: `ip link show` or `ifconfig` +- macOS: `ifconfig` or `networksetup -listallhardwareports` +- Windows: `ipconfig /all` + +#### `--no-localhost` / `--show-localhost` + +Control whether localhost (127.0.0.1/::1) connections are displayed. + +- **Default**: Localhost connections are filtered out (`--no-localhost`) +- **Override**: Use `--show-localhost` to see localhost connections + +This is useful for reducing noise in the connection list, as most users don't need to monitor local IPC connections. + +#### `-r, --refresh-interval ` + +Set the UI refresh rate in milliseconds. Lower values provide more responsive updates but increase CPU usage. + +**Recommendations:** +- **Default (1000ms)**: Good balance for most users +- **High-traffic networks (2000ms)**: Reduce CPU usage on busy networks +- **Real-time monitoring (500ms)**: More responsive updates for quick analysis +- **Low-end systems (2000-3000ms)**: Reduce load on resource-constrained machines + +#### `--no-dpi` + +Disable Deep Packet Inspection (DPI). This reduces CPU usage by 20-40% on high-traffic networks but disables: +- HTTP host detection +- HTTPS/TLS SNI extraction +- DNS query/response detection +- SSH version identification +- QUIC protocol detection + +Useful for performance-constrained environments or when application-level details aren't needed. + +#### `--no-resolve-dns` / `--show-ptr-lookups` + +Reverse DNS lookups are **enabled by default**: IP addresses are resolved to hostnames in the background and shown in the connection list (toggle with the `d` key) and in the Details tab. + +- **`--no-resolve-dns`**: Disable reverse DNS resolution entirely. The connection list shows IP addresses only and no PTR queries are issued. +- **`--show-ptr-lookups`**: PTR lookup traffic is hidden by default. Use this flag to show the DNS PTR queries generated by the resolver. + +**Note**: Resolved hostnames are also included in JSON logs (`destination_hostname`, `source_hostname` fields). + +#### `--theme ` + +Select the color theme preset: + +- **`muted`** (default): A restrained palette with one cyan accent. Addresses + keep calm colors (remote = blue, local = cyan); everything else uses color + only for *signals* — transitional connection states, staleness (yellow/red + rows), and live bandwidth. +- **`classic`**: The original full-color palette from earlier releases, with a + distinct color per column. + +```bash +# Bring back the original full-color look +rustnet --theme classic +``` + +Related: `--no-color` disables all colors entirely (also honors the `NO_COLOR` +environment variable). + +#### `-f, --bpf-filter ` + +Apply a BPF (Berkeley Packet Filter) expression to filter packets at capture time. This is more efficient than application-level filtering as packets are filtered in the kernel before reaching RustNet. + +**Common filter expressions:** + +```bash +# Filter by port (matches source OR destination) +rustnet --bpf-filter "port 443" +rustnet --bpf-filter "port 80 or port 8080" + +# Filter by destination port specifically +rustnet --bpf-filter "dst port 443" +rustnet --bpf-filter "tcp dst port 80" + +# Filter by source port specifically +rustnet --bpf-filter "src port 443" + +# Filter by host +rustnet --bpf-filter "host 192.168.1.1" +rustnet --bpf-filter "net 10.0.0.0/8" + +# Filter by protocol +rustnet --bpf-filter "tcp" +rustnet --bpf-filter "udp port 53" + +# Combine filters +rustnet --bpf-filter "tcp port 443 and host github.com" + +# Exclude traffic +rustnet --bpf-filter "not port 22" +``` + +**Notes:** +- BPF filter syntax follows the pcap-filter(7) format. Invalid filters will cause RustNet to exit with an error. Use `man pcap-filter` for complete syntax documentation. +- **macOS limitation:** BPF filters are incompatible with PKTAP (linktype 149). When you specify a BPF filter on macOS, RustNet automatically falls back to regular interface capture. This means process identification uses `lsof` instead of PKTAP's direct process metadata, which may be slightly less accurate for short-lived connections. + +#### `-l, --log-level ` + +Enable logging with the specified level. Logging is **disabled by default**. + +**Available levels:** +- `error` - Only errors (minimal logging) +- `warn` - Warnings and errors +- `info` - General information (recommended for normal debugging) +- `debug` - Detailed debugging information +- `trace` - Very verbose output (includes packet-level details) + +Log files are created in the `logs/` directory with timestamp: `rustnet_YYYY-MM-DD_HH-MM-SS.log` + +#### `--kubernetes ` (optional feature) + +Attribute connections to their owning Kubernetes pod and container. This flag only exists in builds compiled with the `kubernetes` cargo feature: the official Docker image (`ghcr.io/domcyrus/rustnet`) ships with it enabled, while native installs (cargo, Homebrew, deb/rpm) leave it off. + +**Modes:** + +- `auto` (default): Enable attribution only when RustNet itself is running inside a pod +- `on`: Always enable (e.g. when running directly on a node) +- `off`: Disable attribution + +When active, RustNet maps each connection's PID to its pod UID and container ID via cgroups (`/proc//cgroup`) and resolves human-readable pod and container names from the kubelet log directories (`/var/log/containers`, `/var/log/pods`). This is runtime-agnostic and needs no CRI socket or kubelet credentials. Pod-owned sockets are attributed even when RustNet runs with `hostNetwork: true`, because the per-PID socket tables are network-namespace aware. + +Attribution is surfaced in: + +- The **Details tab**, as a "Kubernetes" section showing pod name, namespace, pod UID, container name, and container ID +- **JSON logs** (`--json-log`) and the `--pcap-export` sidecar JSONL, as a `kubernetes` object per connection event +- **PCAPNG packet comments** (`--pcapng-export`), as `pod=`, `ns=`, `pod_uid=`, `container=`, and `container_id=` fields +- The `pod:`, `ns:`, and `container:` [filter keywords](#keyword-filters) + +**Running on a cluster:** the easiest way to use this is the [kubectl-rustnet](https://github.com/domcyrus/kubectl-rustnet) plugin (`kubectl krew install rustnet`). It launches RustNet as an ephemeral debug pod on a node using the official image, mounts the kubelet log directories read-only for name resolution, and cleans up the pod on exit. Since the plugin runs RustNet inside a pod, the default `auto` mode enables attribution without any flags. + +```bash +# On a Kubernetes cluster: run as an ephemeral debug pod via the plugin +kubectl rustnet --node worker-3 + +# Native build with the feature enabled +cargo build --release --features kubernetes + +# Force attribution on outside a pod (e.g. directly on a node) +rustnet --kubernetes on +``` + +## Keyboard Controls + +### Navigation + +- `↑` or `k` - Navigate up in connection list +- `↓` or `j` - Navigate down in connection list +- `g` - Jump to first connection (vim-style) +- `G` (Shift+g) - Jump to last connection (vim-style) +- `PageUp` or `Ctrl+B` - Move up by one page +- `PageDown` or `Ctrl+F` - Move down by one page + +### Views and Tabs + +- `Tab` or `]` - Next tab +- `Shift+Tab` or `[` - Previous tab +- `1` / `2` / `3` / `4` / `5` - Jump directly to Overview / Details / Interfaces / Graph / Help +- `Enter` - View detailed information about selected connection +- `Esc` - Go back to previous view or clear active filter +- `h` - Toggle help screen + +### Actions + +- `c` - Copy remote address to clipboard +- `p` - Toggle between service names and port numbers +- `d` - Toggle between hostnames and IP addresses (disabled by `--no-resolve-dns`) +- `/` - Enter filter mode (vim-style search with real-time results) +- `x` - Clear all connections and reset statistics (press twice to confirm) +- `t` - Toggle display of historic (closed) connections +- `i` - Toggle the System info sidebar +- `r` - Reset view to defaults (clears grouping, sort, filter, and historic) + +### Process Grouping + +- `a` - Toggle process grouping mode (aggregate connections by process) +- `Space` - Expand/collapse selected process group +- `←` or `h` - Collapse selected group +- `→` or `l` - Expand selected group + +### Sorting + +- `s` - Cycle through sort columns (left-to-right order) +- `S` (Shift+s) - Toggle sort direction (ascending/descending) + +### Exit + +- `q` - Quit the application (press twice to confirm) +- `Ctrl+C` - Quit immediately + +## Mouse Controls + +RustNet has full mouse support. Mouse capture is enabled automatically — all interactions described below work out of the box. + +### Overview Tab + +| Action | Effect | +|--------|--------| +| **Click** on a connection row | Select that connection | +| **Double-click** a connection row | Open the Details tab for that connection | +| **Scroll wheel** over the connection list | Navigate up/down through connections | +| **Click** on a tab name | Switch to that tab | + +### Grouped View (press `a` to enable) + +| Action | Effect | +|--------|--------| +| **Click** on a group header (`▸`/`▾`) | Select the group | +| **Double-click** a group header | Expand or collapse the process group | +| **Click** on a connection within an expanded group | Select that connection | +| **Double-click** a connection within an expanded group | Open the Details tab for that connection | +| **Scroll wheel** | Navigate through groups and connections | + +### Details Tab + +| Action | Effect | +|--------|--------| +| **Click** on any field line | Copy the field value to the system clipboard | + +Clicking a field copies just the value (not the label). For example, clicking the "Remote Address: 142.250.80.46:443" line copies `142.250.80.46:443` to your clipboard. A confirmation message appears in the status bar for 3 seconds. + +Both the "Connection Information" and "Traffic Statistics" panels support click-to-copy. + +## Filtering + +Press `/` to enter filter mode. Type to filter connections in real-time, navigate with arrow keys while typing. + +### Basic Search + +Simply type any text to search across all connection fields: + +``` +/google # Find connections containing "google" +/firefox # Find Firefox connections +/192.168 # Find connections with IP starting with 192.168 +``` + +### Keyword Filters + +Use keyword filters for targeted searches: + +| Keyword | Aliases | Description | Example | +|---------|---------|-------------|---------| +| `port:` | | Exact port match; use `/pattern/` for regex | `port:22` matches only 22; `port:/22/` matches 22, 220, 5522 | +| `sport:` | `srcport:`, `source-port:` | Source port (exact or regex) | `sport:80` matches only source port 80 | +| `dport:` | `dstport:`, `dest-port:`, `destination-port:` | Destination port (exact or regex) | `dport:443` matches only destination port 443 | +| `src:` | `source:` | Source IPs/hostnames | `src:192.168` matches 192.168.x.x | +| `dst:` | `dest:`, `destination:` | Destinations | `dst:github.com` matches github.com | +| `process:` | `proc:` | Process names | `process:ssh` matches ssh, sshd | +| `sni:` | `host:`, `hostname:` | SNI hostnames (HTTPS) | `sni:api` matches api.example.com | +| `service:` | `svc:` | Service names | `service:https` matches HTTPS service | +| `app:` | `application:` | Detected application protocol | `app:ssh` matches SSH connections | +| `state:` | | Protocol states | `state:established` matches established connections | +| `proto:` | `protocol:` | Protocol type | `proto:tcp` matches TCP connections | +| `pod:` | | Kubernetes pod name or UID * | `pod:nginx` matches nginx-86644db9cc-mf5lx | +| `ns:` | `namespace:` | Kubernetes pod namespace * | `ns:kube-system` matches pods in kube-system | +| `container:` | `cont:` | Kubernetes container name or ID * | `container:nginx` matches the nginx container | + +\* Requires a build with the `kubernetes` feature and active pod attribution. See [`--kubernetes`](#--kubernetes-mode-optional-feature). + +### State Filtering + +Filter connections by their current protocol state (case-insensitive): + +⚠️ **Note:** State tracking accuracy varies by protocol. TCP states are most reliable, while UDP, QUIC, and other protocol states are derived from packet inspection and may not always reflect the true connection state. + +**Examples:** +``` +state:syn_recv # Show half-open connections (useful for detecting SYN floods) +state:established # Show only established connections +state:fin_wait # Show connections in closing states +state:quic_handshake # Show QUIC connections during handshake +state:dns_query # Show DNS query connections +state:udp_active # Show active UDP connections +``` + +**Available states:** + +| Protocol | States | +|----------|--------| +| **TCP** | `SYN_SENT`, `SYN_RECV`, `ESTABLISHED`, `FIN_WAIT1`, `FIN_WAIT2`, `TIME_WAIT`, `CLOSE_WAIT`, `LAST_ACK`, `CLOSING`, `CLOSED` | +| **QUIC** | `QUIC_INITIAL`, `QUIC_HANDSHAKE`, `QUIC_CONNECTED`, `QUIC_DRAINING`, `QUIC_CLOSED` ⚠️ *Note: May be incomplete due to encrypted handshakes* | +| **UDP** | `UDP_ACTIVE`, `UDP_IDLE`, `UDP_STALE` | +| **DNS** | `DNS_QUERY`, `DNS_RESPONSE` | +| **SSH** | `BANNER`, `KEYEXCHANGE`, `AUTHENTICATION`, `ESTABLISHED` ⚠️ *Note: Based on packet inspection* | +| **Other** | `ECHO_REQUEST`, `ECHO_REPLY`, `ARP_REQUEST`, `ARP_REPLY` | + +### Regex Filters + +Wrap any filter value in `/pattern/` to use a regular expression (case-insensitive). Regexes use standard syntax supported by the `regex-lite` crate. + +``` +/192\.168\.[0-9]+/ # General regex across all fields +port:/22/ # Ports containing "22" (22, 220, 2200, 5522 …) +sni:/.*github\..*/ # SNI matching github.com, api.github.com, etc. +process:/chrom(e|ium)/ # Chrome or Chromium +``` + +> **Port matching**: `port:443` is an **exact** match (only port 443). Use `port:/443/` if you want substring/regex behaviour. + +### Combining Filters + +Combine multiple filters with spaces (implicit AND): + +``` +sport:80 process:nginx # Nginx connections from port 80 +dport:443 sni:google.com # HTTPS connections to Google +sport:443 state:syn_recv # Half-open connections to port 443 (SYN flood detection) +proto:tcp state:established # All established TCP connections +process:firefox state:quic_connected # Active QUIC connections from Firefox +dport:22 app:openssh # SSH connections using OpenSSH +state:established app:ssh # Established SSH connections +``` + +### Clearing Filters + +Press `Esc` to clear the active filter and return to the full connection list. + +## Sorting + +RustNet provides powerful table sorting to help you analyze network connections. Press `s` to cycle through sortable columns in left-to-right visual order, and press `S` (Shift+s) to toggle between ascending and descending order. + +### Quick Start + +**Find bandwidth hogs (combined up+down traffic):** +``` +Press 's' repeatedly until you see: Bandwidth Total ↓ +The connections with highest total bandwidth appear at the top +``` + +**Sort by process name:** +``` +Press 's' repeatedly until you see: Process ↑ +Connections are sorted alphabetically by process name +``` + +### Sortable Columns + +Press `s` to cycle through columns in left-to-right order: + +| Column | Default Direction | Description | +|--------|-------------------|-------------| +| **Process** | ↑ Ascending | Sort by process name alphabetically | +| **Remote Address** | ↑ Ascending | Sort by remote IP:port | +| **Local Address** | ↑ Ascending | Sort by local IP:port (useful for multi-interface systems) | +| **Location** | ↑ Ascending | Sort by country code (requires GeoIP database) | +| **Service** | ↑ Ascending | Sort by service name or port number | +| **Application** | ↑ Ascending | Sort by detected application protocol (HTTP, DNS, etc.), with TCP/UDP as tie-break | +| **State** | ↑ Ascending | Sort by connection state (ESTABLISHED, etc.) | +| **Bandwidth (Rx/Tx)** | ↓ Descending | Sort by **combined up+down** bandwidth (highest first by default) | + +Columns hidden at narrow terminal widths stay in the cycle — the active sort is always named in the table's section title. + +### Sort Indicators + +The active sort column is highlighted with: +- **Cyan color** and **underline** styling +- **Arrow symbol** (↑ or ↓) showing sort direction +- **Table title** showing current sort state + +**Visual indicators:** +``` +Active column header appears in cyan with underline: +Process │ Remote ↑ │ Local │ Service │ App │ ... + ^^^^^^^^ + (cyan, underlined, with arrow) + +Section title shows current sort: +▎ Active Connections · 42 shown · sort Remote Addr ↑ +``` + +### Sort Behavior + +**Press `s` (lowercase) - Cycle Columns:** +- Moves to the next column in left-to-right visual order +- **Resets to default direction** for that column +- Bandwidth column defaults to descending (↓) to show highest values first +- Text columns default to ascending (↑) for alphabetical order + +**Press `S` (Shift+s) - Toggle Direction:** +- **Stays on current column** +- Flips between ascending (↑) and descending (↓) +- Useful for reversing sort order (e.g., finding smallest bandwidth users) + +**Press `s` multiple times to return to default:** +- Cycling through all columns returns to the default chronological sort (by connection creation time) +- No sort indicator is shown when in default mode + +### Sorting with Filtering + +Sorting works seamlessly with filtering: +1. **Filter first**: Press `/` and enter your filter criteria +2. **Then sort**: Press `s` to sort the filtered results +3. **The sort persists**: Changing the filter keeps your sort order active + +Example workflow: +``` +1. Press '/' and type 'firefox' to filter Firefox connections +2. Press 's' until you see "Bandwidth Total ↓" +3. Now viewing Firefox connections sorted by total bandwidth (up+down combined) +``` + +### Examples + +**Find which process is using the most bandwidth:** +``` +1. Press 's' until "Bandwidth Total ↓" appears +2. Top connection shows the highest total bandwidth (up+down combined) +3. Look at the "Process" column to see which application +``` + +**Sort connections by remote destination:** +``` +1. Press 's' until "Remote Address ↑" appears +2. Connections are grouped by remote IP address +3. Press 'S' to reverse order if needed +``` + +**Find idle connections (lowest bandwidth):** +``` +1. Press 's' to cycle to "Bandwidth Total ↓" +2. Press 'S' to toggle to "Bandwidth Total ↑" (ascending) +3. Connections with lowest total bandwidth appear first +``` + +**Sort by application protocol:** +``` +1. Press 's' until "Application ↑" appears +2. All HTTPS connections group together, DNS queries together, etc. +3. Useful for finding all connections of a specific type +``` + +## Process Grouping + +RustNet can group connections by process name, providing an aggregated view that makes it easier to see which applications are using your network. + +### Enabling Process Grouping + +Press `a` to toggle process grouping mode. When enabled: +- Connections are grouped by process name (sorted alphabetically) +- Each group shows aggregated statistics +- Groups can be expanded/collapsed to show individual connections + +Press `a` again to return to the flat (ungrouped) connection list. + +### Grouped View Display + +When grouping is enabled, the connection list shows process groups: + +``` +▸ firefox (12) TCP:10 UDP:2 12.5K/1.2K +▾ chrome (8) TCP:8 UDP:0 45.2K/5.1K + ├─ 4101 142.250.80.78:443 192.168.1.10:54321 ESTABLISHED 1.2K/0.3K + ├─ 4101 142.250.80.78:443 192.168.1.10:54322 ESTABLISHED 0.8K/0.1K + └─ 4102 8.8.8.8:53 192.168.1.10:54323 UDP_ACTIVE 0.2K/0.1K +▸ systemd-resolved (3) TCP:0 UDP:3 0.2K/0.1K +▸ (5) TCP:2 UDP:3 0.5K/0.2K +``` + +**Group header format:** +- `▸` / `▾` - Collapsed/expanded indicator +- Process name and connection count +- Protocol breakdown (TCP/UDP counts) +- Total bandwidth (rx/tx, in the Bandwidth column) + +**Expanded connections:** +- Tree-style prefixes (`├─` / `└─`) with the PID (the group header carries the process name) +- The same columns as the flat view (addresses, state, application, bandwidth) + +### Expanding and Collapsing Groups + +| Key | Action | +|-----|--------| +| `Space` | Toggle expand/collapse on selected group | +| `→` or `l` | Expand selected group | +| `←` or `h` | Collapse selected group | + +### Navigation in Grouped View + +Navigation works the same as in flat view: +- `↑`/`k` and `↓`/`j` move through visible rows (groups and expanded connections) +- `g` jumps to the first row +- `G` jumps to the last row +- `Enter` on a connection opens the Details view + +### Unknown Processes + +Connections without process information are grouped into a single `` group. This typically includes: +- Short-lived connections that closed before process lookup completed +- System-level connections on some platforms +- Connections from restricted processes + +### Filtering with Grouping + +Filtering works seamlessly with grouping: +1. Press `/` and enter your filter +2. Only groups containing matching connections are shown +3. Expand groups to see which connections matched + +### Sorting in Grouped View + +When grouping is enabled: +- Groups are sorted alphabetically by process name (A-Z) +- The sort column indicator shows how connections within groups are sorted +- Press `s` to change how connections are sorted within expanded groups + +### Reset View + +Press `r` to reset all view settings at once: +- Disables process grouping +- Clears any active filter +- Resets sort to default (chronological order) + +## Network Statistics Panel + +The Network Statistics panel appears on the right side of the interface, below the Traffic panel. It provides real-time TCP connection quality metrics derived directly from packet capture analysis, making it platform-independent across Linux, macOS, Windows, and FreeBSD. + +### Available Metrics + +**TCP Retransmits** +Detects when a TCP segment is retransmitted due to packet loss or timeout. RustNet identifies retransmissions by analyzing TCP sequence numbers: when a packet arrives with a sequence number lower than expected, it indicates the original packet was lost and is being resent. + +**Out-of-Order Packets** +Tracks inbound TCP packets that arrive out of sequence, typically caused by network congestion or multiple routing paths. These packets eventually arrive but in the wrong order, requiring the receiver to buffer and reorder them. + +**Fast Retransmits** +Identifies TCP fast retransmit events triggered by receiving three duplicate acknowledgments (RFC 2581). This mechanism allows TCP to detect and recover from packet loss more quickly than waiting for a timeout, improving connection performance. + +### Statistics Display Format + +The panel shows both **active** and **total** counts for each metric: + +``` +TCP Retransmits: 5 / 142 total +Out-of-Order: 2 / 89 total +Fast Retransmits: 1 / 23 total +Active TCP Flows: 18 +``` + +- **Active count** (left number): Sum of events from currently tracked connections. This number goes up and down as connections are established and cleaned up. +- **Total count** (right number): Cumulative count since RustNet started. This number only increases and provides historical context. +- **Active TCP Flows**: Number of active TCP connections with analytics data. + +### Per-Connection Statistics + +When viewing connection details (press `Enter` on a connection), TCP analytics are shown for that specific connection: + +``` +TCP Retransmits: 3 +Out-of-Order: 1 +Fast Retransmits: 0 +``` + +These counters are tracked independently for each connection, allowing you to identify problematic connections experiencing packet loss or network issues. + +### Use Cases + +**Network Quality Monitoring** +A sudden increase in retransmissions or out-of-order packets indicates network congestion, packet loss, or routing issues. + +**Connection Troubleshooting** +High retransmit counts on specific connections can identify: +- Unreliable network paths to certain destinations +- Bandwidth-constrained links +- Faulty network hardware or drivers + +**Performance Analysis** +Fast retransmit frequency indicates how well TCP is recovering from packet loss without waiting for timeouts. + +### Technical Notes + +- Statistics are derived from TCP sequence number analysis without requiring packet timestamps +- Analysis works on both outbound and inbound packets +- SYN and FIN flags are properly accounted for in sequence number tracking (each consumes 1 sequence number) +- Only TCP connections show analytics; UDP, ICMP, and other protocols do not have these metrics + +## Interface Statistics + +RustNet provides real-time network interface statistics across all supported platforms (Linux, macOS, FreeBSD, Windows). Interface stats are displayed in two locations: + +### Accessing Interface Statistics + +**Overview Tab (Main Screen):** +- Interface stats appear in the right panel below Network Stats +- Shows up to 3 active interfaces with current rates +- Displays: `InterfaceName: X KB/s ↓ / Y KB/s ↑` +- Shows cumulative totals: `Errors (Total): N Drops (Total): M` + +**Interfaces Tab (Detailed View):** +- Press `i` to toggle the Interface Statistics view +- Shows a detailed table of all network interfaces +- Displays comprehensive metrics for each interface + +### Statistics Displayed + +| Metric | Description | Notes | +|--------|-------------|-------| +| **RX Rate** | Current receive rate (bytes/sec) | Calculated from recent activity | +| **TX Rate** | Current transmit rate (bytes/sec) | Calculated from recent activity | +| **RX Packets** | Total packets received | Cumulative since boot/interface up | +| **TX Packets** | Total packets transmitted | Cumulative since boot/interface up | +| **RX Err** | Receive errors | Cumulative total (not recent) | +| **TX Err** | Transmit errors | Cumulative total (not recent) | +| **RX Drop** | Dropped incoming packets | Cumulative total (not recent) | +| **TX Drop** | Dropped outgoing packets | Cumulative total (not recent) | +| **Collisions** | Network collisions | Platform-dependent availability | + +**Important**: Error and drop counters are **cumulative totals** since the system booted or the interface came up, not recent activity. These help identify long-term interface reliability but won't show immediate issues. + +### Platform-Specific Behavior + +**All Platforms:** +- All counters (bytes, packets, errors, drops) are cumulative from boot/interface up +- Rates (bytes/sec) are calculated from snapshots taken every 2 seconds +- Loopback interface is included for monitoring local traffic + +**Windows:** +- Filters out virtual/filter adapters to show only physical interfaces: + - Excludes: `-Npcap`, `-WFP`, `-QoS`, `-Native`, `-Virtual`, `-Packet` variants + - Excludes: `Lightweight Filter`, `MAC Layer` interfaces + - Excludes: Disconnected "Local Area Connection" adapters +- Uses LUID-based deduplication to prevent duplicate interface entries +- Collisions: Always 0 (not available on modern Windows interfaces) + +**macOS:** +- Includes data validation to detect corrupt counters on virtual interfaces +- TX Drops: Always 0 (limited availability on macOS) +- Sanitizes error/drop counters if values appear corrupted (>2^31 or errors>packets) + +**FreeBSD:** +- TX Drops: Always 0 (not typically available on FreeBSD) +- Uses BSD getifaddrs API with AF_LINK filtering + +**Linux:** +- Reads statistics from `/sys/class/net/{interface}/statistics` +- All counters typically available and reliable + +### Interpreting the Statistics + +**Healthy Interface:** +``` +Ethernet: 2.40 KB/s ↓ / 1.96 KB/s ↑ + Errors (Total): 0 Drops (Total): 0 +``` +Zero or very low error/drop counts indicate a reliable network connection. + +**Problematic Interface:** +``` +WiFi: 150 KB/s ↓ / 45 KB/s ↑ + Errors (Total): 1089 Drops (Total): 2178 +``` +High error/drop counts may indicate: +- Signal interference (WiFi) +- Cable issues (Ethernet) +- Network congestion +- Driver or hardware problems + +**Note**: Since error/drop counters are cumulative, evaluate them relative to total packets. A few errors out of millions of packets is normal; thousands of errors with low packet counts indicates problems. + +### Interface Filtering + +**Which Interfaces Are Shown:** +- Interfaces must be operationally "up" OR have traffic statistics +- Loopback interface is included (useful for monitoring local connections) +- Virtual/filter adapters are excluded on Windows (they mirror physical interfaces) + +**Overview Tab Filtering:** +- Windows: Shows all active interfaces (NPF device path detected automatically) +- macOS/Linux: Shows interfaces with recent traffic (`rx_bytes > 0 || tx_bytes > 0 || rx_packets > 0 || tx_packets > 0`) +- Special interfaces (`any`, `pktap`): Shows all interfaces with any activity + +**Interfaces Tab:** +- Shows all detected interfaces that pass the platform-specific filters +- Sorts to show the currently captured interface first (highlighted) +- Other interfaces appear in alphabetical order + +### Use Cases + +**Bandwidth Monitoring:** +Monitor real-time bandwidth usage across all network interfaces to identify: +- Which interface is carrying the most traffic +- Bandwidth distribution across WiFi vs Ethernet +- Local traffic volume (loopback interface) + +**Reliability Analysis:** +Check cumulative error and drop counters to: +- Identify unreliable network interfaces +- Detect hardware or driver issues +- Compare interface quality over time + +**Multi-Interface Systems:** +On systems with multiple network interfaces: +- Compare performance across interfaces +- Monitor VPN tunnel statistics +- Track interface failover behavior + +## Connection Lifecycle & Visual Indicators + +RustNet uses intelligent timeout management to automatically clean up inactive connections while providing visual warnings before removal. + +### Visual Staleness Indicators + +Connections change color based on how close they are to being cleaned up: + +| Color | Meaning | Staleness | +|-------|---------|-----------| +| **White** (default) | Active connection | < 75% of timeout | +| **Yellow** | Stale - approaching timeout | 75-90% of timeout | +| **Red** | Critical - will be removed soon | > 90% of timeout | + +**Example**: An HTTP connection with a 10-minute timeout will: +- Stay **white** for the first 7.5 minutes +- Turn **yellow** from 7.5 to 9 minutes (warning) +- Turn **red** after 9 minutes (critical) +- Be removed at 10 minutes + +This gives you advance warning when a connection is about to disappear from the list. + +### Smart Protocol-Aware Timeouts + +RustNet adjusts connection timeouts based on the protocol and detected application: + +#### TCP Connections +- **HTTP/HTTPS** (detected via DPI): **10 minutes** - supports HTTP keep-alive +- **SSH** (detected via DPI): **30 minutes** - accommodates long interactive sessions +- **Active established** (< 1 min idle): **10 minutes** +- **Idle established** (> 1 min idle): **5 minutes** +- **TIME_WAIT**: 30 seconds - standard TCP timeout +- **CLOSED**: 5 seconds - rapid cleanup +- **SYN_SENT, FIN_WAIT, etc.**: 30-60 seconds + +#### UDP Connections +- **SSH over UDP**: **30 minutes** - long-lived sessions +- **DNS**: **30 seconds** - short-lived queries +- **Regular UDP**: **60 seconds** - standard timeout + +#### QUIC Connections (Detected State) +- **Connected**: **3 minutes** default (or uses idle timeout from transport parameters if available) +- **With CONNECTION_CLOSE frame**: 1-10 seconds (based on close type) +- **Initial/Handshaking**: 60 seconds - allow connection establishment +- **Draining**: 10 seconds - RFC 9000 draining period + +### Activity-Based Adjustment + +Connections showing recent packet activity get longer timeouts: +- **Last packet < 60 seconds ago**: Uses "active" timeout (longer) +- **Last packet > 60 seconds ago**: Uses "idle" timeout (shorter) + +This ensures active connections stay visible while idle connections are cleaned up more quickly. + +### Why Connections Disappear + +A connection is removed when: +1. **No packets received** for the duration of its timeout period +2. The connection enters a **closed state** (TCP CLOSED, QUIC CLOSED) +3. **Explicit close frames** detected (QUIC CONNECTION_CLOSE) + +**Note**: Rate indicators (bandwidth display) show *decaying* traffic based on recent activity. A connection may show declining bandwidth (yellow bars) but remain in the list until it exceeds its idle timeout. This is intentional - the visual decay gives you time to see the connection winding down before it's removed. + +### Historic Connections + +By default, connections disappear from the list once they time out or close. Press `t` to toggle **historic connections** mode, which keeps closed connections visible alongside active ones. + +**How it works:** + +When a connection is cleaned up, it is archived into a historic connections pool (up to 5,000 entries; oldest are evicted first). Pressing `t` toggles their visibility: + +- **Active connections** display normally with standard color indicators +- **Historic connections** appear in **dim gray** to clearly distinguish them from active connections +- The table title changes to **"Active + Historic Connections"** when historic mode is on + +**Details view:** + +Selecting a historic connection and pressing `Enter` shows the usual connection details, plus a **Status** field displaying how long ago the connection was closed (e.g., "Closed (5m ago)"). + +**Stats panel:** + +When historic connections are present, the stats panel shows a separate **"Historic: N"** count below the total active connections. + +**Grouped view:** + +In process grouping mode (`a`), group headers show the historic connection count separately from the active count when historic mode is enabled. + +**Graph tab:** + +The graph tab always shows only active connections, even when historic mode is on. + +**Resetting:** + +- Press `r` to reset all view settings, which also hides historic connections +- Press `x` twice to clear all connections, which also clears the historic pool + +## Logging + +Logging is **disabled by default**. When enabled with the `--log-level` option, RustNet creates timestamped log files in the `logs/` directory. Each session generates a new log file with the format `rustnet_YYYY-MM-DD_HH-MM-SS.log`. + +### Log File Contents + +Log files contain: +- Application startup and shutdown events +- Network interface information +- Packet capture statistics +- Connection state changes +- Error diagnostics +- DPI detection results (at debug/trace levels) +- Performance metrics (at trace level) + +### Enabling Logging + +Use the `--log-level` option to enable logging: + +```bash +# Info-level logging (recommended for general use) +sudo rustnet --log-level info + +# Debug-level logging (detailed troubleshooting) +sudo rustnet --log-level debug + +# Trace-level logging (very verbose, includes packet-level details) +sudo rustnet --log-level trace + +# Error-only logging (minimal logging) +sudo rustnet --log-level error +``` + +### Log Levels Explained + +| Level | What Gets Logged | Use Case | +|-------|------------------|----------| +| `error` | Only errors and critical issues | Production monitoring | +| `warn` | Warnings and errors | Normal operation with warnings | +| `info` | General information, startup/shutdown | Standard debugging | +| `debug` | Detailed debugging information | Troubleshooting issues | +| `trace` | Packet-level details, very verbose | Deep debugging | + +### Managing Log Files + +**Log cleanup script:** + +The `scripts/clear_old_logs.sh` script is provided for log cleanup: + +```bash +# Remove logs older than 7 days +./scripts/clear_old_logs.sh + +# Customize retention period by editing the script +``` + +**Manual cleanup:** + +```bash +# Remove all logs +rm -rf logs/ + +# Remove logs older than 7 days (Linux/macOS) +find logs/ -name "rustnet_*.log" -mtime +7 -delete + +# View log file size +du -sh logs/ +``` + +### Log File Privacy + +⚠️ **Warning**: Log files may contain sensitive information: +- IP addresses and ports +- Hostnames and SNI data (HTTPS) +- DNS queries and responses +- Process names and PIDs +- Packet contents (at trace level) + +**Best practices:** +- Only enable logging when needed for debugging +- Secure log directory permissions: `chmod 700 logs/` +- Review logs for sensitive data before sharing +- Implement log rotation and retention policies +- Delete logs when no longer needed + +### Troubleshooting with Logs + +When reporting issues: +1. Enable debug logging: `rustnet --log-level debug` +2. Reproduce the issue +3. Find the latest log file in `logs/` +4. Review for errors or unexpected behavior +5. Redact sensitive information before sharing + +For performance issues, trace-level logging provides the most detail but generates large log files quickly. + +### JSON Logging + +The `--json-log` option enables structured JSON logging of connection events to a file. Each line is a separate JSON object (JSONL format). + +```bash +# Enable JSON logging +sudo rustnet --json-log /tmp/connections.json + +# Combine with other options +sudo rustnet -i eth0 --json-log ~/network-events.json +``` + +**Event types:** +- `new_connection` - Logged when a new connection is first detected +- `connection_closed` - Logged when a connection is cleaned up after becoming inactive + +**JSON fields:** + +| Field | Type | Description | +|-------|------|-------------| +| `timestamp` | string | RFC3339 UTC timestamp | +| `event` | string | Event type (`new_connection` or `connection_closed`) | +| `protocol` | string | Protocol (TCP, UDP, etc.) | +| `source_ip` | string | Local IP address | +| `source_port` | number | Local port number | +| `destination_ip` | string | Remote IP address | +| `destination_port` | number | Remote port number | +| `pid` | number | Process ID (if available) | +| `process_name` | string | Process name (if available) | +| `service_name` | string | Service name from port lookup (if available) | +| `direction` | string | Connection direction (`outgoing` or `incoming`), TCP only when handshake observed | +| `dpi_protocol` | string | Detected application protocol (if DPI enabled) | +| `dpi_domain` | string | Extracted domain/hostname (if available) | +| `bytes_sent` | number | Total bytes sent (connection_closed only) | +| `bytes_received` | number | Total bytes received (connection_closed only) | +| `duration_secs` | number | Connection duration in seconds (connection_closed only) | + +**Example output:** + +```json +{"timestamp":"2025-01-15T10:30:00Z","event":"new_connection","protocol":"TCP","source_ip":"192.168.1.100","source_port":54321,"destination_ip":"93.184.216.34","destination_port":443,"pid":1234,"process_name":"curl","service_name":"https","direction":"outgoing","dpi_protocol":"HTTPS","dpi_domain":"example.com"} +{"timestamp":"2025-01-15T10:30:05Z","event":"connection_closed","protocol":"TCP","source_ip":"192.168.1.100","source_port":54321,"destination_ip":"93.184.216.34","destination_port":443,"pid":1234,"process_name":"curl","service_name":"https","direction":"outgoing","bytes_sent":1024,"bytes_received":4096,"duration_secs":5} +``` + +**Processing JSON logs:** + +```bash +# Pretty-print latest events +tail -f /tmp/connections.json | jq . + +# Filter by process +cat /tmp/connections.json | jq 'select(.process_name == "firefox")' + +# Count connections by destination +cat /tmp/connections.json | jq -s 'group_by(.destination_ip) | map({ip: .[0].destination_ip, count: length})' +``` + +### PCAP Export + +The `--pcap-export` option captures raw packets to a standard PCAP file for analysis in Wireshark, tcpdump, or other tools. + +```bash +# Export all captured packets +sudo rustnet -i eth0 --pcap-export capture.pcap + +# Combine with BPF filter +sudo rustnet -i eth0 --bpf-filter "tcp port 443" --pcap-export https.pcap +``` + +**Output files:** + +| File | Description | +|------|-------------| +| `capture.pcap` | Raw packet data in standard PCAP format | +| `capture.pcap.connections.jsonl` | Streaming connection metadata with process info | + +**Sidecar JSONL format** (one JSON object per line, written as connections close): + +```json +{"timestamp":"2026-01-17T10:30:00Z","protocol":"TCP","local_addr":"192.168.1.100:54321","remote_addr":"142.250.80.46:443","pid":1234,"process_name":"firefox","first_seen":"...","last_seen":"...","bytes_sent":1024,"bytes_received":8192,"state":"ESTABLISHED"} +``` + +| Field | Description | +|-------|-------------| +| `timestamp` | When the connection record was written | +| `protocol` | TCP, UDP, ICMP, etc. | +| `local_addr` / `remote_addr` | Connection endpoints | +| `pid` / `process_name` | Process info (if identified) | +| `first_seen` / `last_seen` | Connection timestamps | +| `bytes_sent` / `bytes_received` | Traffic totals | +| `state` | Final connection state | + +#### Enriching PCAP with Process Information + +Standard PCAP files don't include process information. Use the included `scripts/pcap_enrich.py` script to correlate packets with processes: + +```bash +# Install scapy (required) +pip install scapy + +# Show packets with process info +python scripts/pcap_enrich.py capture.pcap + +# Output as TSV for further processing +python scripts/pcap_enrich.py capture.pcap --format tsv > report.tsv + +# Create annotated PCAPNG with process comments (requires Wireshark's editcap) +python scripts/pcap_enrich.py capture.pcap -o annotated.pcapng +``` + +The annotated PCAPNG embeds process information as packet comments, visible in Wireshark's packet details. + +#### Native Annotated PCAPNG Export + +The `--pcapng-export` option writes a PCAPNG file directly with RustNet packet comments. This avoids the Python enrichment step when you want to open the capture in Wireshark immediately: + +```bash +sudo rustnet -i eth0 --pcapng-export capture.pcapng +``` + +Packet comments are live best-effort annotations. RustNet waits briefly for process and GeoIP enrichment, then writes the packet even if attribution is still unavailable; packets may still have DPI/SNI, direction, or GeoIP comments without `process=`/`pid=` fields. Under heavy load, packets dropped before the processor stage or dropped by the bounded PCAPNG export queue will not appear in the PCAPNG, so a simultaneous `--pcap-export` file can contain packets missing from the PCAPNG. Packet blocks may be written out of capture order, but they keep their true capture timestamps and Wireshark can sort/display them by time. + +Use `--pcap-export` plus `capture.pcap.connections.jsonl` when cleanup-time metadata completeness matters more than a single annotated file. + +**Manual correlation:** + +```bash +# View packets +wireshark capture.pcap + +# View process mappings +cat capture.pcap.connections.jsonl | jq -r '[.protocol, .local_addr, .remote_addr, .pid, .process_name] | @tsv' + +# Filter in Wireshark by connection tuple +# ip.addr == 142.250.80.46 && tcp.port == 443 +``` diff --git a/USAGE.zh-CN.md b/USAGE.zh-CN.md new file mode 100644 index 0000000..5c3633f --- /dev/null +++ b/USAGE.zh-CN.md @@ -0,0 +1,1147 @@ +

English | 简体中文

+ +# 使用指南 + +本文档涵盖 RustNet 的详细使用说明,包括命令行选项、键盘控制、过滤、排序以及理解连接生命周期。 + +## 目录 + +- [运行 RustNet](#running-rustnet) +- [命令行选项](#command-line-options) +- [键盘控制](#keyboard-controls) +- [鼠标控制](#mouse-controls) +- [过滤](#filtering) +- [排序](#sorting) +- [进程分组](#process-grouping) +- [网络统计面板](#network-statistics-panel) +- [接口统计](#interface-statistics) +- [连接生命周期与视觉指示器](#connection-lifecycle--visual-indicators) +- [日志](#logging) + +## 运行 RustNet + +在大多数系统上,数据包捕获需要提升的特权。详细的权限配置说明参见 [INSTALL.zh-CN.md](INSTALL.zh-CN.md)。 + +**快速开始:** + +```bash +# 使用 sudo 运行(适用于所有平台) +sudo rustnet + +# 或授予 Linux capabilities 以无需 sudo 运行(详情参见 INSTALL.md) +# Linux 示例(现代内核 5.8+): +sudo setcap 'cap_net_raw,cap_bpf,cap_perfmon+eip' /path/to/rustnet +rustnet +``` + +**基本使用示例:** + +```bash +# 使用默认设置运行 +# macOS:使用 PKTAP 获取进程元数据 +# Linux/其他:自动检测活动接口 +rustnet + +# 指定网络接口 +rustnet -i eth0 +rustnet --interface wlan0 + +# Linux:同时监控所有接口 +rustnet -i any + +# 过滤掉 localhost 连接(默认已过滤) +rustnet --no-localhost + +# 显示 localhost 连接(覆盖默认过滤) +rustnet --show-localhost + +# 设置 UI 刷新间隔(毫秒) +rustnet -r 500 +rustnet --refresh-interval 2000 + +# 禁用深度包检测 +rustnet --no-dpi + +# 恢复原始全彩调色板 +rustnet --theme classic + +# 禁用反向 DNS 查找(默认启用) +rustnet --no-resolve-dns + +# 使用指定级别启用日志(选项:error、warn、info、debug、trace) +rustnet -l debug +rustnet --log-level info + +# 查看帮助和所有选项 +rustnet --help +``` + +## 命令行选项 + +``` +Usage: rustnet [OPTIONS] + +Options: + -i, --interface 要监控的网络接口 + --no-localhost 过滤掉 localhost 连接(默认:已过滤) + --show-localhost 显示 localhost 连接(覆盖默认过滤) + -r, --refresh-interval UI 刷新间隔,单位为毫秒 [默认:1000] + --no-dpi 禁用深度包检测 + --no-resolve-dns 禁用反向 DNS 查找(默认启用) + --show-ptr-lookups 显示 PTR 查找连接(默认隐藏) + -l, --log-level 设置日志级别(如果未提供,则不启用日志) + --json-log 将连接事件以 JSON 格式记录到指定文件 + --pcap-export 将捕获的数据包导出到 PCAP 文件供 Wireshark 分析 + --pcapng-export 将捕获的数据包导出为带注释的 PCAPNG 文件供 Wireshark 分析 + --no-color 禁用 UI 中的所有颜色(同时尊重 NO_COLOR 环境变量) + --theme 颜色主题预设:"muted"(单一强调色,颜色仅用于信号,默认) + 或 "classic"(原始全彩调色板) + --geoip-country GeoLite2-Country.mmdb 的路径(未指定时自动发现) + --geoip-asn GeoLite2-ASN.mmdb 的路径(未指定时自动发现) + --geoip-city GeoLite2-City.mmdb 的路径(未指定时自动发现) + --no-geoip 完全禁用 GeoIP 查询 + -f, --bpf-filter 用于数据包捕获的 BPF 过滤器表达式 + --no-sandbox 禁用 Landlock 沙箱(仅限 Linux) + --sandbox-strict 要求完整沙箱强制执行,否则退出(仅限 Linux) + --no-uid-drop 初始化后保持 root 运行,不降权到 + SUDO_UID/SUDO_GID(或 nobody)(仅限 Linux、macOS 和 FreeBSD) + -h, --help 打印帮助 + -V, --version 打印版本 +``` + +### 选项详情 + +#### `-i, --interface ` + +指定要监控的网络接口。 + +**默认行为(无 `-i` 标志):** +- **macOS**:自动使用 PKTAP 获取增强型进程元数据(需要 sudo) +- **Linux/其他**:自动检测第一个可用的非回环接口 + +**示例:** +```bash +# 默认:自动检测接口(macOS 上使用 PKTAP) +rustnet + +# Linux:使用特殊的 "any" 伪接口监控所有接口 +rustnet -i any + +# 监控特定接口 +rustnet -i eth0 # 监控以太网接口 +rustnet -i wlan0 # 监控 WiFi 接口 +rustnet -i en0 # 监控 macOS 主接口 + +# 监控 VPN 和隧道接口(TUN/TAP 支持) +rustnet -i utun0 # macOS VPN 隧道(TUN,Layer 3) +rustnet -i tun0 # Linux/BSD VPN 隧道(TUN,Layer 3) +rustnet -i tap0 # TAP 接口(Layer 2,包含 Ethernet) +``` + +**TUN/TAP 接口支持:** + +RustNet 完全支持监控 VPN 和虚拟网络接口: + +- **TUN 接口**(Layer 3):直接承载 IP 数据包,不含 Ethernet 头部 + - VPN 常见:WireGuard、OpenVPN(tun 模式)、Tailscale + - 示例:`utun0-utun9`(macOS)、`tun0-tun9`(Linux/BSD) + +- **TAP 接口**(Layer 2):包含完整的 Ethernet 帧 + - 用于:OpenVPN(tap 模式)、QEMU/KVM 虚拟网络、Docker + - 示例:`tap0-tap9`(Linux/BSD) + +RustNet 自动检测 TUN/TAP 接口并相应调整数据包解析。接口类型显示在 UI 状态区域。 + +**平台特定说明:** +- **macOS**:不使用 `-i` 时自动使用 PKTAP 以获得更好的进程检测。使用 `-i ` 来监控特定接口 +- **Linux**:使用 `-i any` 同时在所有接口上捕获(其他平台不可用) +- **TUN/TAP**:所有平台均完全支持 —— RustNet 通过名称检测接口类型并调整解析 +- **所有平台**:如果指定了不存在的接口,会显示错误并列出可用接口 + +**查找你的接口:** +- Linux:`ip link show` 或 `ifconfig` +- macOS:`ifconfig` 或 `networksetup -listallhardwareports` +- Windows:`ipconfig /all` + +#### `--no-localhost` / `--show-localhost` + +控制是否显示 localhost(127.0.0.1/::1)连接。 + +- **默认**:过滤掉 localhost 连接(`--no-localhost`) +- **覆盖**:使用 `--show-localhost` 查看 localhost 连接 + +这对于减少连接列表中的噪音很有用,因为大多数用户不需要监控本地 IPC 连接。 + +#### `-r, --refresh-interval ` + +以毫秒为单位设置 UI 刷新率。较低的值提供更灵敏的更新,但会增加 CPU 使用率。 + +**建议:** +- **默认(1000ms)**:大多数用户的良好平衡 +- **高流量网络(2000ms)**:在繁忙网络上降低 CPU 使用率 +- **实时监控(500ms)**:更灵敏的更新,适合快速分析 +- **低端系统(2000-3000ms)**:降低资源受限机器上的负载 + +#### `--no-dpi` + +禁用深度包检测(DPI)。这在高流量网络上可降低 20-40% 的 CPU 使用率,但会禁用: +- HTTP 主机检测 +- HTTPS/TLS SNI 提取 +- DNS 查询/响应检测 +- SSH 版本识别 +- QUIC 协议检测 + +适用于性能受限的环境或不需要应用层详细信息时。 + +#### `--no-resolve-dns` / `--show-ptr-lookups` + +反向 DNS 查找**默认启用**:IP 地址在后台解析为主机名,并显示在连接列表中(按 `d` 键切换)和详情标签页中。 + +- **`--no-resolve-dns`**:完全禁用反向 DNS 解析。连接列表仅显示 IP 地址,不发起 PTR 查询。 +- **`--show-ptr-lookups`**:PTR 查找流量默认隐藏。使用此标志显示解析器生成的 DNS PTR 查询。 + +**注意**:解析后的主机名也包含在 JSON 日志中(`destination_hostname`、`source_hostname` 字段)。 + +#### `--theme ` + +选择颜色主题预设: + +- **`muted`**(默认):克制的调色板,只有一个青色强调色。地址保留柔和的颜色 + (远程 = 蓝色,本地 = 青色);其他颜色仅用于*信号* —— 连接状态变化、 + 过期状态(黄色/红色行)以及实时带宽。 +- **`classic`**:早期版本的原始全彩调色板,每列一种颜色。 + +```bash +# 恢复原始全彩外观 +rustnet --theme classic +``` + +相关:`--no-color` 完全禁用所有颜色(同时尊重 `NO_COLOR` 环境变量)。 + +#### `-f, --bpf-filter ` + +应用 BPF(Berkeley Packet Filter)表达式在捕获时过滤数据包。这比应用层过滤更高效,因为数据包在到达 RustNet 之前在内核中被过滤。 + +**常见过滤器表达式:** + +```bash +# 按端口过滤(匹配源 OR 目的) +rustnet --bpf-filter "port 443" +rustnet --bpf-filter "port 80 or port 8080" + +# 按目的端口过滤 +rustnet --bpf-filter "dst port 443" +rustnet --bpf-filter "tcp dst port 80" + +# 按源端口过滤 +rustnet --bpf-filter "src port 443" + +# 按主机过滤 +rustnet --bpf-filter "host 192.168.1.1" +rustnet --bpf-filter "net 10.0.0.0/8" + +# 按协议过滤 +rustnet --bpf-filter "tcp" +rustnet --bpf-filter "udp port 53" + +# 组合过滤器 +rustnet --bpf-filter "tcp port 443 and host github.com" + +# 排除流量 +rustnet --bpf-filter "not port 22" +``` + +**注意:** +- BPF 过滤器语法遵循 pcap-filter(7) 格式。无效过滤器会导致 RustNet 退出并报错。使用 `man pcap-filter` 查看完整语法文档。 +- **macOS 限制:** BPF 过滤器与 PKTAP(linktype 149)不兼容。在 macOS 上指定 BPF 过滤器时,RustNet 自动回退到常规接口捕获。这意味着进程识别使用 `lsof` 而非 PKTAP 的直接进程元数据,对于短寿命连接可能略不准确。 + +#### `-l, --log-level ` + +使用指定级别启用日志。**默认禁用**日志。 + +**可用级别:** +- `error` —— 仅错误(最小日志) +- `warn` —— 警告和错误 +- `info` —— 一般信息(建议用于常规调试) +- `debug` —— 详细的调试信息 +- `trace` —— 非常详细的输出(包含数据包级详情) + +日志文件创建于 `logs/` 目录,带时间戳:`rustnet_YYYY-MM-DD_HH-MM-SS.log` + +## 键盘控制 + +### 导航 + +- `↑` 或 `k` —— 在连接列表中向上导航 +- `↓` 或 `j` —— 在连接列表中向下导航 +- `g` —— 跳转到第一个连接(vim 风格) +- `G`(Shift+g)—— 跳转到最后一个连接(vim 风格) +- `PageUp` 或 `Ctrl+B` —— 向上翻一页 +- `PageDown` 或 `Ctrl+F` —— 向下翻一页 + +### 视图与标签页 + +- `Tab` 或 `]` —— 下一个标签页 +- `Shift+Tab` 或 `[` —— 上一个标签页 +- `1` / `2` / `3` / `4` / `5` —— 直接跳转到 概览 / 详情 / 接口 / 图表 / 帮助 +- `Enter` —— 查看所选连接的详细信息 +- `Esc` —— 返回上一个视图或清除活动过滤器 +- `h` —— 切换帮助屏幕 + +### 操作 + +- `c` —— 将远程地址复制到剪贴板 +- `p` —— 在服务名和端口号之间切换 +- `d` —— 在主机名和 IP 地址之间切换(由 `--no-resolve-dns` 禁用) +- `/` —— 进入过滤模式(vim 风格搜索,实时结果) +- `x` —— 清除所有连接并重置统计(按两次确认) +- `t` —— 切换历史(已关闭)连接的显示 +- `i` —— 切换 System 信息侧边栏 +- `r` —— 将视图重置为默认值(清除分组、排序、过滤和历史) + +### 进程分组 + +- `a` —— 切换进程分组模式(按进程聚合连接) +- `Space` —— 展开/折叠所选进程分组 +- `←` 或 `h` —— 折叠所选分组 +- `→` 或 `l` —— 展开所选分组 + +### 排序 + +- `s` —— 在可排序列之间循环切换(从左到右顺序) +- `S`(Shift+s)—— 切换排序方向(升序/降序) + +### 退出 + +- `q` —— 退出应用(按两次确认) +- `Ctrl+C` —— 立即退出 + +## 鼠标控制 + +RustNet 具有完整的鼠标支持。鼠标捕获自动启用 —— 以下描述的所有交互均可开箱即用。 + +### 概览标签页 + +| 操作 | 效果 | +|------|------| +| **单击** 连接行 | 选择该连接 | +| **双击** 连接行 | 打开该连接的详情标签页 | +| **滚轮** 在连接列表上 | 在连接中上下导航 | +| **单击** 标签页名称 | 切换到该标签页 | + +### 分组视图(按 `a` 启用) + +| 操作 | 效果 | +|------|------| +| **单击** 分组头部(`▸`/`▾`) | 选择该分组 | +| **双击** 分组头部 | 展开或折叠进程分组 | +| **单击** 展开分组内的连接 | 选择该连接 | +| **双击** 展开分组内的连接 | 打开该连接的详情标签页 | +| **滚轮** | 在分组和连接中导航 | + +### 详情标签页 + +| 操作 | 效果 | +|------|------| +| **单击** 任意字段行 | 将字段值复制到系统剪贴板 | + +单击字段仅复制值(不包括标签)。例如,单击 "Remote Address: 142.250.80.46:443" 行会将 `142.250.80.46:443` 复制到剪贴板。状态栏会显示 3 秒的确认消息。 + +"Connection Information" 和 "Traffic Statistics" 两个面板都支持点击复制。 + +## 过滤 + +按 `/` 进入过滤模式。输入以实时过滤连接,输入时可用方向键导航。 + +### 基本搜索 + +直接输入任意文本即可在所有连接字段中搜索: + +``` +/google # 查找包含 "google" 的连接 +/firefox # 查找 Firefox 连接 +/192.168 # 查找 IP 以 192.168 开头的连接 +``` + +### 关键字过滤器 + +使用关键字过滤器进行定向搜索: + +| 关键字 | 别名 | 描述 | 示例 | +|---------|---------|-------------|---------| +| `port:` | | 精确端口匹配;使用 `/pattern/` 进行正则 | `port:22` 仅匹配 22;`port:/22/` 匹配 22、220、5522 | +| `sport:` | `srcport:`、`source-port:` | 源端口(精确或正则) | `sport:80` 仅匹配源端口 80 | +| `dport:` | `dstport:`、`dest-port:`、`destination-port:` | 目的端口(精确或正则) | `dport:443` 仅匹配目的端口 443 | +| `src:` | `source:` | 源 IP/主机名 | `src:192.168` 匹配 192.168.x.x | +| `dst:` | `dest:`、`destination:` | 目的地址 | `dst:github.com` 匹配 github.com | +| `process:` | `proc:` | 进程名 | `process:ssh` 匹配 ssh、sshd | +| `sni:` | `host:`、`hostname:` | SNI 主机名(HTTPS) | `sni:api` 匹配 api.example.com | +| `service:` | `svc:` | 服务名 | `service:https` 匹配 HTTPS 服务 | +| `app:` | `application:` | 检测到的应用协议 | `app:ssh` 匹配 SSH 连接 | +| `state:` | | 协议状态 | `state:established` 匹配已建立的连接 | +| `proto:` | `protocol:` | 协议类型 | `proto:tcp` 匹配 TCP 连接 | + +### 状态过滤 + +按当前协议状态过滤连接(不区分大小写): + +⚠️ **注意:** 状态追踪的准确性因协议而异。TCP 状态最可靠,而 UDP、QUIC 和其他协议的状态来自数据包检测,可能并不总是反映真实的连接状态。 + +**示例:** +``` +state:syn_recv # 显示半开连接(可用于检测 SYN flood) +state:established # 仅显示已建立的连接 +state:fin_wait # 显示处于关闭状态的连接 +state:quic_handshake # 显示握手期间的 QUIC 连接 +state:dns_query # 显示 DNS 查询连接 +state:udp_active # 显示活跃的 UDP 连接 +``` + +**可用状态:** + +| 协议 | 状态 | +|----------|--------| +| **TCP** | `SYN_SENT`、`SYN_RECV`、`ESTABLISHED`、`FIN_WAIT1`、`FIN_WAIT2`、`TIME_WAIT`、`CLOSE_WAIT`、`LAST_ACK`、`CLOSING`、`CLOSED` | +| **QUIC** | `QUIC_INITIAL`、`QUIC_HANDSHAKE`、`QUIC_CONNECTED`、`QUIC_DRAINING`、`QUIC_CLOSED` ⚠️ *注意:可能不完整,因为握手已加密* | +| **UDP** | `UDP_ACTIVE`、`UDP_IDLE`、`UDP_STALE` | +| **DNS** | `DNS_QUERY`、`DNS_RESPONSE` | +| **SSH** | `BANNER`、`KEYEXCHANGE`、`AUTHENTICATION`、`ESTABLISHED` ⚠️ *注意:基于数据包检测* | +| **Other** | `ECHO_REQUEST`、`ECHO_REPLY`、`ARP_REQUEST`、`ARP_REPLY` | + +### 正则过滤器 + +将任何过滤值包裹在 `/pattern/` 中以使用正则表达式(不区分大小写)。正则使用 `regex-lite` crate 支持的标准语法。 + +``` +/192\.168\.[0-9]+/ # 在所有字段中通用正则 +port:/22/ # 端口包含 "22"(22、220、2200、5522 …) +sni:/.*github\..*/ # SNI 匹配 github.com、api.github.com 等 +process:/chrom(e|ium)/ # Chrome 或 Chromium +``` + +> **端口匹配**:`port:443` 是**精确**匹配(仅端口 443)。如需子串/正则行为,请使用 `port:/443/`。 + +### 组合过滤器 + +用空格组合多个过滤器(隐式 AND): + +``` +sport:80 process:nginx # Nginx 从 80 端口发出的连接 +dport:443 sni:google.com # 到 Google 的 HTTPS 连接 +sport:443 state:syn_recv # 到 443 端口的半开连接(SYN flood 检测) +proto:tcp state:established # 所有已建立的 TCP 连接 +process:firefox state:quic_connected # Firefox 的活跃 QUIC 连接 +dport:22 app:openssh # 使用 OpenSSH 的 SSH 连接 +state:established app:ssh # 已建立的 SSH 连接 +``` + +### 清除过滤器 + +按 `Esc` 清除活动过滤器并返回完整连接列表。 + +## 排序 + +RustNet 提供强大的表格排序功能来帮助你分析网络连接。按 `s` 按从左到右的视觉顺序在可排序列之间循环切换,按 `S`(Shift+s)在升序和降序之间切换。 + +### 快速开始 + +**找出带宽大户(上下行合计流量):** +``` +反复按 's' 直到看到:Bandwidth Total ↓ +总带宽最高的连接显示在顶部 +``` + +**按进程名排序:** +``` +反复按 's' 直到看到:Process ↑ +连接按进程名字母顺序排序 +``` + +### 可排序列 + +按 `s` 按从左到右顺序在列之间循环切换: + +| 列 | 默认方向 | 描述 | +|--------|-------------------|-------------| +| **Process** | ↑ 升序 | 按进程名字母顺序排序 | +| **Remote Address** | ↑ 升序 | 按远程 IP:port 排序 | +| **Local Address** | ↑ 升序 | 按本地 IP:port 排序(适用于多接口系统) | +| **Location** | ↑ 升序 | 按国家代码排序(需要 GeoIP 数据库) | +| **Service** | ↑ 升序 | 按服务名或端口号排序 | +| **Application** | ↑ 升序 | 按检测到的应用协议排序(HTTP、DNS 等),以 TCP/UDP 作为同序比较 | +| **State** | ↑ 升序 | 按连接状态排序(ESTABLISHED 等) | +| **Bandwidth (Rx/Tx)** | ↓ 降序 | 按**上下行合计**带宽排序(默认最高优先) | + +在窄终端下被隐藏的列仍留在循环中 —— 当前排序列始终显示在表格的区段标题中。 + +### 排序指示器 + +活动排序列通过以下方式高亮: +- **青色**和**下划线**样式 +- 显示排序方向的**箭头符号**(↑ 或 ↓) +- 显示当前排序状态的**表格标题** + +**视觉指示器:** +``` +活动列头部以青色和下划线显示: +Process │ Remote ↑ │ Local │ Service │ App │ ... + ^^^^^^^^ + (青色、下划线、带箭头) + +区段标题显示当前排序: +▎ Active Connections · 42 shown · sort Remote Addr ↑ +``` + +### 排序行为 + +**按 `s`(小写)—— 循环列:** +- 移动到从左到右视觉顺序的下一列 +- **重置为该列的默认方向** +- 带宽列默认降序(↓)以优先显示最高值 +- 文本列默认升序(↑)以按字母顺序排列 + +**按 `S`(Shift+s)—— 切换方向:** +- **保持在当前列** +- 在升序(↑)和降序(↓)之间切换 +- 可用于反转排序顺序(例如找出带宽最小的用户) + +**多次按 `s` 返回默认:** +- 循环浏览所有列后返回默认按时间排序(按连接创建时间) +- 默认模式下不显示排序指示器 + +### 过滤时排序 + +排序与过滤无缝配合: +1. **先过滤**:按 `/` 输入过滤条件 +2. **再排序**:按 `s` 对过滤结果排序 +3. **排序持续**:更改过滤器时保持排序顺序活动 + +示例工作流: +``` +1. 按 '/' 并输入 'firefox' 过滤 Firefox 连接 +2. 按 's' 直到看到 "Bandwidth Total ↓" +3. 现在查看按总带宽(上下行合计)排序的 Firefox 连接 +``` + +### 示例 + +**找出哪个进程使用最多带宽:** +``` +1. 按 's' 直到出现 "Bandwidth Total ↓" +2. 顶部连接显示最高总带宽(上下行合计) +3. 查看 "Process" 列以确定是哪个应用 +``` + +**按远程目的地排序连接:** +``` +1. 按 's' 直到出现 "Remote Address ↑" +2. 连接按远程 IP 地址分组 +3. 如需,按 'S' 反转顺序 +``` + +**找出空闲连接(最低带宽):** +``` +1. 按 's' 循环到 "Bandwidth Total ↓" +2. 按 'S' 切换到 "Bandwidth Total ↑"(升序) +3. 总带宽最低的连接显示在最前面 +``` + +**按应用协议排序:** +``` +1. 按 's' 直到出现 "Application / Host ↑" +2. 所有 HTTPS 连接分组在一起,DNS 查询分组在一起,等等 +3. 适合查找特定类型的所有连接 +``` + +## 进程分组 + +RustNet 可以按进程名分组连接,提供聚合视图,让你更容易看到哪些应用正在使用你的网络。 + +### 启用进程分组 + +按 `a` 切换进程分组模式。启用时: +- 连接按进程名分组(按字母顺序排序) +- 每个分组显示聚合统计 +- 分组可以展开/折叠以显示单个连接 + +再次按 `a` 返回扁平(未分组)连接列表。 + +### 分组视图显示 + +启用分组时,连接列表显示进程分组: + +``` +▸ firefox (12) TCP:10 UDP:2 12.5K/1.2K +▾ chrome (8) TCP:8 UDP:0 45.2K/5.1K + ├─ 4101 142.250.80.78:443 192.168.1.10:54321 ESTABLISHED 1.2K/0.3K + ├─ 4101 142.250.80.78:443 192.168.1.10:54322 ESTABLISHED 0.8K/0.1K + └─ 4102 8.8.8.8:53 192.168.1.10:54323 UDP_ACTIVE 0.2K/0.1K +▸ systemd-resolved (3) TCP:0 UDP:3 0.2K/0.1K +▸ (5) TCP:2 UDP:3 0.5K/0.2K +``` + +**分组头部格式:** +- `▸` / `▾` —— 折叠/展开指示器 +- 进程名和连接数 +- 协议细分(TCP/UDP 计数) +- 总带宽(rx/tx,位于 Bandwidth 列) + +**展开的连接:** +- 树形前缀(`├─` / `└─`)加 PID(进程名由上方的分组头部承载) +- 单个连接详情(协议、地址、状态、应用) + +### 展开和折叠分组 + +| 按键 | 操作 | +|-----|--------| +| `Space` | 切换所选分组的展开/折叠 | +| `→` 或 `l` | 展开所选分组 | +| `←` 或 `h` | 折叠所选分组 | + +### 分组视图中的导航 + +导航方式与扁平视图相同: +- `↑`/`k` 和 `↓`/`j` 在可见行(分组和展开的连接)之间移动 +- `g` 跳转到第一行 +- `G` 跳转到最后一行 +- `Enter` 在连接上打开详情视图 + +### 未知进程 + +没有进程信息的连接被分组到单个 `` 分组中。这通常包括: +- 在进程查找完成前就已关闭的短寿命连接 +- 某些平台上的系统级连接 +- 来自受限进程的连接 + +### 分组时过滤 + +过滤与分组无缝配合: +1. 按 `/` 输入你的过滤条件 +2. 仅显示包含匹配连接的分组 +3. 展开分组以查看哪些连接匹配 + +### 分组时排序 + +启用分组时: +- 分组按进程名字母顺序排序(A-Z) +- 排序列指示器显示分组内连接的排序方式 +- 按 `s` 更改展开分组内连接的排序方式 + +### 重置视图 + +按 `r` 一次性重置所有视图设置: +- 禁用进程分组 +- 清除任何活动过滤器 +- 将排序重置为默认(按时间顺序) + +## 网络统计面板 + +网络统计面板显示在界面右侧,位于流量面板下方。它提供直接从数据包捕获分析中得出的实时 TCP 连接质量指标,使其在 Linux、macOS、Windows 和 FreeBSD 上跨平台一致。 + +### 可用指标 + +**TCP 重传** +检测由于数据包丢失或超时而重新传输的 TCP 段。RustNet 通过分析 TCP 序列号来识别重传:当到达的数据包序列号低于预期时,表示原始数据包已丢失并正在重发。 + +**乱序包** +追踪到达顺序错误的入站 TCP 数据包,通常由网络拥塞或多条路由路径导致。这些数据包最终会到达,但顺序错误,需要接收方缓冲并重新排序。 + +**快速重传** +识别由收到三个重复确认(RFC 2581)触发的 TCP 快速重传事件。这种机制允许 TCP 比等待超时更快地从数据包丢失中恢复,从而提高连接性能。 + +### 统计显示格式 + +面板为每个指标显示**活动**和**总计**计数: + +``` +TCP Retransmits: 5 / 142 total +Out-of-Order: 2 / 89 total +Fast Retransmits: 1 / 23 total +Active TCP Flows: 18 +``` + +- **活动计数**(左侧数字):当前追踪连接的各事件总和。这个数字随着连接的建立和清理而上下波动。 +- **总计计数**(右侧数字):自 RustNet 启动以来的累积计数。这个数字只增不减,提供历史背景。 +- **Active TCP Flows**:具有分析数据的活跃 TCP 连接数。 + +### 逐连接统计 + +查看连接详情时(在连接上按 `Enter`),显示该特定连接的 TCP 分析: + +``` +TCP Retransmits: 3 +Out-of-Order: 1 +Fast Retransmits: 0 +``` + +这些计数器独立追踪每个连接,允许你识别遇到数据包丢失或网络问题的有问题连接。 + +### 使用场景 + +**网络质量监控** +重传或乱序包的突然增加表明网络拥塞、数据包丢失或路由问题。 + +**连接故障排查** +特定连接上的高重传计数可以识别: +- 到某些目的地的不可靠网络路径 +- 带宽受限的链路 +- 故障的网络硬件或驱动 + +**性能分析** +快速重传频率表明 TCP 在不等待超时的情况下从数据包丢失中恢复的情况。 + +### 技术说明 + +- 统计源自 TCP 序列号分析,不需要数据包时间戳 +- 分析适用于出站和入站数据包 +- SYN 和 FIN 标志在序列号追踪中被正确计算(每个消耗 1 个序列号) +- 仅 TCP 连接显示分析指标;UDP、ICMP 和其他协议没有这些指标 + +## 接口统计 + +RustNet 在所有支持的平台上(Linux、macOS、FreeBSD、Windows)提供实时网络接口统计。接口统计显示在两个位置: + +### 访问接口统计 + +**概览标签页(主屏幕):** +- 接口统计出现在右侧面板,位于网络统计下方 +- 显示最多 3 个活跃接口及当前速率 +- 显示:`InterfaceName: X KB/s ↓ / Y KB/s ↑` +- 显示累计总数:`Errors (Total): N Drops (Total): M` + +**接口标签页(详细视图):** +- 按 `i` 切换接口统计视图 +- 显示所有网络接口的详细表格 +- 显示每个接口的综合指标 + +### 显示的统计 + +| 指标 | 描述 | 说明 | +|--------|-------------|-------| +| **RX Rate** | 当前接收速率(字节/秒) | 根据近期活动计算 | +| **TX Rate** | 当前发送速率(字节/秒) | 根据近期活动计算 | +| **RX Packets** | 接收的总数据包数 | 自启动/接口上线以来累计 | +| **TX Packets** | 发送的总数据包数 | 自启动/接口上线以来累计 | +| **RX Err** | 接收错误 | 累计总数(非近期) | +| **TX Err** | 发送错误 | 累计总数(非近期) | +| **RX Drop** | 丢弃的入站数据包 | 累计总数(非近期) | +| **TX Drop** | 丢弃的出站数据包 | 累计总数(非近期) | +| **Collisions** | 网络冲突 | 平台相关的可用性 | + +**重要**:错误和丢弃计数器是**自系统启动或接口上线以来的累计总数**,不是近期活动。这些有助于识别长期接口可靠性,但不会显示即时问题。 + +### 平台特定行为 + +**所有平台:** +- 所有计数器(字节、数据包、错误、丢弃)自启动/接口上线以来累计 +- 速率(字节/秒)根据每 2 秒采集的快照计算 +- 包含回环接口用于监控本地流量 + +**Windows:** +- 过滤掉虚拟/过滤适配器,仅显示物理接口: + - 排除:`-Npcap`、`-WFP`、`-QoS`、`-Native`、`-Virtual`、`-Packet` 变体 + - 排除:`Lightweight Filter`、`MAC Layer` 接口 + - 排除:已断开的 "Local Area Connection" 适配器 +- 使用基于 LUID 的去重防止重复的接口条目 +- Collisions:始终为 0(现代 Windows 接口上不可用) + +**macOS:** +- 包含数据验证以检测虚拟接口上的损坏计数器 +- TX Drops:始终为 0(macOS 上可用性有限) +- 如果错误/丢弃计数器值看起来损坏(>2^31 或 errors>packets),则进行清理 + +**FreeBSD:** +- TX Drops:始终为 0(FreeBSD 上通常不可用) +- 使用 BSD getifaddrs API 配合 AF_LINK 过滤 + +**Linux:** +- 从 `/sys/class/net/{interface}/statistics` 读取统计 +- 所有计数器通常可用且可靠 + +### 解读统计 + +**健康的接口:** +``` +Ethernet: 2.40 KB/s ↓ / 1.96 KB/s ↑ + Errors (Total): 0 Drops (Total): 0 +``` +零或极低的错误/丢弃计数表明可靠的网络连接。 + +**有问题的接口:** +``` +WiFi: 150 KB/s ↓ / 45 KB/s ↑ + Errors (Total): 1089 Drops (Total): 2178 +``` +高错误/丢弃计数可能表明: +- 信号干扰(WiFi) +- 线缆问题(Ethernet) +- 网络拥塞 +- 驱动或硬件问题 + +**注意**:由于错误/丢弃计数器是累计的,请相对于总数据包数来评估它们。数百万数据包中有少量错误是正常的;数据包数低但有数千错误则表明有问题。 + +### 接口过滤 + +**显示哪些接口:** +- 接口必须在操作上 "up" OR 拥有流量统计 +- 包含回环接口(用于监控本地连接) +- Windows 上排除虚拟/过滤适配器(它们镜像物理接口) + +**概览标签页过滤:** +- Windows:显示所有活跃接口(NPF 设备路径自动检测) +- macOS/Linux:显示有近期流量的接口(`rx_bytes > 0 || tx_bytes > 0 || rx_packets > 0 || tx_packets > 0`) +- 特殊接口(`any`、`pktap`):显示有任何活动的所有接口 + +**接口标签页:** +- 显示通过平台特定过滤的所有检测到的接口 +- 排序将当前捕获的接口排在最前面(高亮) +- 其他接口按字母顺序出现 + +### 使用场景 + +**带宽监控:** +监控所有网络接口的实时带宽使用情况以识别: +- 哪个接口承载最多流量 +- WiFi 与 Ethernet 之间的带宽分布 +- 本地流量体积(回环接口) + +**可靠性分析:** +检查累计错误和丢弃计数器以: +- 识别不可靠的网络接口 +- 检测硬件或驱动问题 +- 随时间比较接口质量 + +**多接口系统:** +在具有多个网络接口的系统上: +- 跨接口比较性能 +- 监控 VPN 隧道统计 +- 追踪接口故障转移行为 + +## 连接生命周期与视觉指示器 + +RustNet 使用智能超时管理自动清理不活跃的连接,同时在移除前提供视觉警告。 + +### 视觉陈旧度指示器 + +连接根据距离被清理的接近程度改变颜色: + +| 颜色 | 含义 | 陈旧度 | +|-------|---------|-----------| +| **白色**(默认) | 活跃连接 | < 75% 的超时时间 | +| **黄色** | 陈旧 - 接近超时 | 75-90% 的超时时间 | +| **红色** | 严重 - 即将被移除 | > 90% 的超时时间 | + +**示例**:一条超时为 10 分钟的 HTTP 连接会: +- 前 7.5 分钟保持**白色** +- 7.5 到 9 分钟变为**黄色**(警告) +- 9 分钟后变为**红色**(严重) +- 10 分钟时被移除 + +这让你在连接即将从列表中消失前得到预警。 + +### 智能协议感知超时 + +RustNet 根据协议和检测到的应用调整连接超时: + +#### TCP 连接 +- **HTTP/HTTPS**(通过 DPI 检测):**10 分钟** —— 支持 HTTP keep-alive +- **SSH**(通过 DPI 检测):**30 分钟** —— 适应长交互会话 +- **活跃已建立**(< 1 分钟空闲):**10 分钟** +- **空闲已建立**(> 1 分钟空闲):**5 分钟** +- **TIME_WAIT**:30 秒 —— 标准 TCP 超时 +- **CLOSED**:5 秒 —— 快速清理 +- **SYN_SENT、FIN_WAIT 等**:30-60 秒 + +#### UDP 连接 +- **基于 UDP 的 SSH**:**30 分钟** —— 长会话 +- **DNS**:**30 秒** —— 短查询 +- **普通 UDP**:**60 秒** —— 标准超时 + +#### QUIC 连接(检测到的状态) +- **已连接**:**3 分钟** 默认(或当可用时使用来自传输参数的 idle timeout) +- **带 CONNECTION_CLOSE 帧**:1-10 秒(基于关闭类型) +- **Initial/Handshaking**:60 秒 —— 允许连接建立 +- **Draining**:10 秒 —— RFC 9000 draining 周期 + +### 基于活动的调整 + +显示近期数据包活动的连接获得更长的超时: +- **最近数据包 < 60 秒前**:使用"活跃"超时(更长) +- **最近数据包 > 60 秒前**:使用"空闲"超时(更短) + +这确保活跃连接保持可见,而空闲连接更快被清理。 + +### 连接为什么消失 + +连接在以下情况下被移除: +1. **在超时期间内未收到数据包** +2. 连接进入**关闭状态**(TCP CLOSED、QUIC CLOSED) +3. 检测到**显式关闭帧**(QUIC CONNECTION_CLOSE) + +**注意**:速率指示器(带宽显示)基于近期活动显示*衰减*的流量。连接可能显示带宽下降(黄色条),但在超过空闲超时前仍保留在列表中。这是有意设计的 —— 视觉衰减让你在连接被移除前有时间看到它逐渐结束。 + +### 历史连接 + +默认情况下,连接在超时或关闭后从列表中消失。按 `t` 切换**历史连接**模式,使已关闭的连接与活跃连接一起保持可见。 + +**工作原理:** + +当连接被清理时,它被归档到历史连接池中(最多 5,000 条;最旧的先被逐出)。按 `t` 切换它们的可见性: + +- **活跃连接**以标准颜色指示器正常显示 +- **历史连接**以**暗灰色**显示,以清楚区分于活跃连接 +- 启用历史模式时,表格标题变为 **"Active + Historic Connections"** + +**详情视图:** + +选择历史连接并按 `Enter` 显示通常的连接详情,外加一个**Status**字段显示连接关闭多久前(例如 "Closed (5m ago)")。 + +**统计面板:** + +当存在历史连接时,统计面板在总活跃连接数下方显示一个单独的 **"Historic: N"** 计数。 + +**分组视图:** + +在进程分组模式(`a`)中,当启用历史模式时,分组头部将历史连接数与活跃计数分开显示。 + +**图表标签页:** + +图表标签页始终只显示活跃连接,即使历史模式开启。 + +**重置:** + +- 按 `r` 重置所有视图设置,这也会隐藏历史连接 +- 按 `x` 两次清除所有连接,这也会清除历史池 + +## 日志 + +日志**默认禁用**。使用 `--log-level` 选项启用时,RustNet 在 `logs/` 目录中创建带时间戳的日志文件。每个会话生成一个新日志文件,格式为 `rustnet_YYYY-MM-DD_HH-MM-SS.log`。 + +### 日志文件内容 + +日志文件包含: +- 应用启动和关闭事件 +- 网络接口信息 +- 数据包捕获统计 +- 连接状态变更 +- 错误诊断 +- DPI 检测结果(debug/trace 级别) +- 性能指标(trace 级别) + +### 启用日志 + +使用 `--log-level` 选项启用日志: + +```bash +# Info 级别日志(建议常规使用) +sudo rustnet --log-level info + +# Debug 级别日志(详细故障排查) +sudo rustnet --log-level debug + +# Trace 级别日志(非常详细,包含数据包级详情) +sudo rustnet --log-level trace + +# 仅 Error 级别日志(最小日志) +sudo rustnet --log-level error +``` + +### 日志级别说明 + +| 级别 | 记录内容 | 使用场景 | +|-------|------------------|----------| +| `error` | 仅错误和关键问题 | 生产监控 | +| `warn` | 警告和错误 | 带警告的正常运行 | +| `info` | 一般信息、启动/关闭 | 标准调试 | +| `debug` | 详细的调试信息 | 故障排查 | +| `trace` | 数据包级详情,非常详细 | 深度调试 | + +### 管理日志文件 + +**日志清理脚本:** + +提供了 `scripts/clear_old_logs.sh` 脚本用于日志清理: + +```bash +# 删除 7 天前的日志 +./scripts/clear_old_logs.sh + +# 通过编辑脚本自定义保留期 +``` + +**手动清理:** + +```bash +# 删除所有日志 +rm -rf logs/ + +# 删除 7 天前的日志(Linux/macOS) +find logs/ -name "rustnet_*.log" -mtime +7 -delete + +# 查看日志文件大小 +du -sh logs/ +``` + +### 日志文件隐私 + +⚠️ **警告**:日志文件可能包含敏感信息: +- IP 地址和端口 +- 主机名和 SNI 数据(HTTPS) +- DNS 查询和响应 +- 进程名和 PID +- 数据包内容(trace 级别) + +**最佳实践:** +- 仅在需要调试时启用日志 +- 保护日志目录权限:`chmod 700 logs/` +- 分享前检查日志中的敏感数据 +- 实施日志轮转和保留策略 +- 不再需要时删除日志 + +### 使用日志故障排查 + +报告问题时: +1. 启用 debug 日志:`rustnet --log-level debug` +2. 重现问题 +3. 在 `logs/` 中找到最新的日志文件 +4. 查看错误或异常行为 +5. 分享前脱敏敏感信息 + +对于性能问题,trace 级别日志提供最详细的细节,但会快速生成大量日志文件。 + +### JSON 日志 + +`--json-log` 选项启用将连接事件以结构化 JSON 格式记录到文件。每行是一个独立的 JSON 对象(JSONL 格式)。 + +```bash +# 启用 JSON 日志 +sudo rustnet --json-log /tmp/connections.json + +# 与其他选项组合 +sudo rustnet -i eth0 --json-log ~/network-events.json +``` + +**事件类型:** +- `new_connection` —— 首次检测到新连接时记录 +- `connection_closed` —— 连接在变为不活跃后被清理时记录 + +**JSON 字段:** + +| 字段 | 类型 | 描述 | +|-------|------|-------------| +| `timestamp` | string | RFC3339 UTC 时间戳 | +| `event` | string | 事件类型(`new_connection` 或 `connection_closed`) | +| `protocol` | string | 协议(TCP、UDP 等) | +| `source_ip` | string | 本地 IP 地址 | +| `source_port` | number | 本地端口号 | +| `destination_ip` | string | 远程 IP 地址 | +| `destination_port` | number | 远程端口号 | +| `pid` | number | 进程 ID(如果可用) | +| `process_name` | string | 进程名(如果可用) | +| `service_name` | string | 端口查找的服务名(如果可用) | +| `direction` | string | 连接方向(`outgoing` 或 `incoming`),仅当观察到 TCP 握手时 | +| `dpi_protocol` | string | 检测到的应用协议(如果启用 DPI) | +| `dpi_domain` | string | 提取的域名/主机名(如果可用) | +| `bytes_sent` | number | 发送的总字节数(仅 connection_closed) | +| `bytes_received` | number | 接收的总字节数(仅 connection_closed) | +| `duration_secs` | number | 连接持续时间,单位为秒(仅 connection_closed) | + +**示例输出:** + +```json +{"timestamp":"2025-01-15T10:30:00Z","event":"new_connection","protocol":"TCP","source_ip":"192.168.1.100","source_port":54321,"destination_ip":"93.184.216.34","destination_port":443,"pid":1234,"process_name":"curl","service_name":"https","direction":"outgoing","dpi_protocol":"HTTPS","dpi_domain":"example.com"} +{"timestamp":"2025-01-15T10:30:05Z","event":"connection_closed","protocol":"TCP","source_ip":"192.168.1.100","source_port":54321,"destination_ip":"93.184.216.34","destination_port":443,"pid":1234,"process_name":"curl","service_name":"https","direction":"outgoing","bytes_sent":1024,"bytes_received":4096,"duration_secs":5} +``` + +**处理 JSON 日志:** + +```bash +# 漂亮打印最新事件 +tail -f /tmp/connections.json | jq . + +# 按进程过滤 +cat /tmp/connections.json | jq 'select(.process_name == "firefox")' + +# 按目的地统计连接数 +cat /tmp/connections.json | jq -s 'group_by(.destination_ip) | map({ip: .[0].destination_ip, count: length})' +``` + +### PCAP 导出 + +`--pcap-export` 选项将原始数据包捕获到标准 PCAP 文件,供 Wireshark、tcpdump 或其他工具分析。 + +```bash +# 导出所有捕获的数据包 +sudo rustnet -i eth0 --pcap-export capture.pcap + +# 与 BPF 过滤器组合 +sudo rustnet -i eth0 --bpf-filter "tcp port 443" --pcap-export https.pcap +``` + +**输出文件:** + +| 文件 | 描述 | +|------|-------------| +| `capture.pcap` | 标准 PCAP 格式的原始数据包 | +| `capture.pcap.connections.jsonl` | 流式连接元数据,带进程信息 | + +**Sidecar JSONL 格式**(每行一个 JSON 对象,连接关闭时写入): + +```json +{"timestamp":"2026-01-17T10:30:00Z","protocol":"TCP","local_addr":"192.168.1.100:54321","remote_addr":"142.250.80.46:443","pid":1234,"process_name":"firefox","first_seen":"...","last_seen":"...","bytes_sent":1024,"bytes_received":8192,"state":"ESTABLISHED"} +``` + +| 字段 | 描述 | +|-------|-------------| +| `timestamp` | 连接记录写入时间 | +| `protocol` | TCP、UDP、ICMP 等 | +| `local_addr` / `remote_addr` | 连接端点 | +| `pid` / `process_name` | 进程信息(如果已识别) | +| `first_seen` / `last_seen` | 连接时间戳 | +| `bytes_sent` / `bytes_received` | 流量总计 | +| `state` | 最终连接状态 | + +#### 用进程信息富化 PCAP + +标准 PCAP 文件不包含进程信息。使用附带的 `scripts/pcap_enrich.py` 脚本将数据包与进程关联: + +```bash +# 安装 scapy(必需) +pip install scapy + +# 显示带进程信息的数据包 +python scripts/pcap_enrich.py capture.pcap + +# 输出为 TSV 以便进一步处理 +python scripts/pcap_enrich.py capture.pcap --format tsv > report.tsv + +# 创建带进程注释的注释 PCAPNG(需要 Wireshark 的 editcap) +python scripts/pcap_enrich.py capture.pcap -o annotated.pcapng +``` + +注释 PCAPNG 将进程信息嵌入为数据包注释,在 Wireshark 的数据包详情中可见。 + +#### 原生带注释 PCAPNG 导出 + +`--pcapng-export` 选项会直接写出带 RustNet 数据包注释的 PCAPNG 文件。想立即在 Wireshark 中打开捕获文件时,可以省去 Python 富化步骤: + +```bash +sudo rustnet -i eth0 --pcapng-export capture.pcapng +``` + +数据包注释是实时的 best-effort 标注。RustNet 会短暂等待进程和 GeoIP 补全,然后即使归因仍不可用也会写出数据包;因此有些注释可能只有 DPI/SNI、方向或 GeoIP 字段,而没有 `process=`/`pid=`。高负载下,在处理器阶段之前丢弃的数据包或被有界 PCAPNG 导出队列丢弃的数据包不会出现在 PCAPNG 中,所以同时生成的 `--pcap-export` 文件可能包含 PCAPNG 中缺失的数据包。Enhanced Packet Block 可能不是按捕获时间顺序写入,但保留真实捕获时间戳,Wireshark 可以按时间排序/显示。 + +当清理阶段的元数据完整性比单个带注释文件更重要时,请使用 `--pcap-export` 加 `capture.pcap.connections.jsonl`。 + +**手动关联:** + +```bash +# 查看数据包 +wireshark capture.pcap + +# 查看进程映射 +cat capture.pcap.connections.jsonl | jq -r '[.protocol, .local_addr, .remote_addr, .pid, .process_name] | @tsv' + +# 在 Wireshark 中按连接元组过滤 +# ip.addr == 142.250.80.46 && tcp.port == 443 +``` diff --git a/assets/rustnet.gif b/assets/rustnet.gif new file mode 100644 index 0000000000000000000000000000000000000000..be5f1cc8006363a134c870ff2964dc2e1381b0c2 GIT binary patch literal 2735489 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