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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# AGENTS.md

This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.

## Build & Test Commands

```bash
cargo build --workspace # build everything
cargo test --workspace # run all tests (188 tests)
cargo test -p prt-core core::scanner # tests for specific module
cargo test -- --nocapture # show println output
cargo clippy --workspace --all-targets # lint
cargo fmt --all -- --check # format check
cargo bench -p prt-core # criterion benchmarks
```

Note: `cargo` may require `export PATH="$HOME/.cargo/bin:$PATH"` on this machine.

## Architecture

Network port monitor with TUI interface (ratatui) for macOS and Linux. Workspace with 2 crates:

- **prt-core** — library: model, scanner, killer, platform abstraction, i18n, session, config, known ports, alerts, suspicious detection, bandwidth, containers, namespaces, process detail, firewall
- **prt** — TUI binary (ratatui + crossterm + clap) with stream/watch/tracer/forward modules

**Data flow:** `platform::scan_ports()` → `Session::refresh()` → `scanner::diff_entries()` (tracks New/Unchanged/Gone with first_seen carry-forward) → enrich (service names, suspicious, containers) → retain (drop Gone after 5s) → `bandwidth.sample()` → `scanner::sort_entries()` → (in App::refresh) `alerts::evaluate()` → cache invalidation → `scanner::filter_indices()` → UI renders (ViewMode-based routing)

**Key design decisions:**
- Platform abstraction via `platform/mod.rs` with `#[cfg(target_os)]` — macOS uses `lsof` output parsing, Linux uses `/proc` via `procfs` crate
- `PortEntry` is the core data type; `TrackedEntry` wraps it with status (New/Unchanged/Gone), timestamp, and enrichment fields (first_seen, suspicious, container_name, service_name)
- Entry identity key is `(port, pid)` tuple — used in `diff_entries()` and focus stability (selection tracks by identity, not index)
- `Session` struct encapsulates the refresh/diff/retain/sort cycle — shared logic that UI delegates to
- `ViewMode` enum controls fullscreen views (Table/Chart/Topology/ProcessDetail/Namespaces); `DetailTab` enum controls bottom panel tabs (Tree/Interface/Connection)
- `ExportFormat` in core has no clap dependency; binary crate wraps it with `clap::ValueEnum`
- Gone entries are retained for 5 seconds before removal; auto-refresh every 2 seconds
- Config from `~/.config/prt/config.toml` — optional, missing file = defaults, parse error = stderr warning + defaults
- Error handling: `anyhow::Result` throughout, UI shows errors as status messages
- Caching: process detail and namespace data cached per-refresh (not per-frame) in App

**i18n system:** `prt-core/src/i18n/` — static `Strings` structs per language (en, ru, zh), `AtomicU8` for global state. Language set via `--lang` flag, `PRT_LANG` env, or auto-detected from system locale. Compile-time completeness check: adding a field to `Strings` forces all language files to be updated.

**macOS performance:** `platform/macos.rs` uses batch `ps` calls (`batch_ps_info`, `batch_parent_names`) — 2 total ps invocations per scan cycle instead of 4*N. This is critical for responsiveness with many connections.

**Shared constants:** `TICK_RATE` and `GONE_RETENTION` are defined in `model.rs`.

## Testing Patterns

Tests are inline `#[cfg(test)] mod tests` in each module (172 in prt-core + 1 doc-test, 15 in prt = 188 total). Helper functions `make_entry()` / `make_tracked()` create test data with minimal required fields. Platform-specific parsing tests (macos.rs) run only on macOS via `#[cfg(target_os = "macos")]`.
43 changes: 43 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,49 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Changed (breaking)

- **Top-level navigation simplified.** `ViewMode` shrinks from
`{ Table, Chart, Topology, ProcessDetail, Namespaces, SshHosts, Tunnels }`
to three sections: `Connections`, `Processes`, `Ssh`.
`Tab` / `Shift+Tab` cycles between sections.
- **Sub-tabs replace fullscreen modes.** Topology and ProcessDetail are
sub-tabs of *Processes*; SSH Hosts and Tunnels are sub-tabs of *SSH*.
Switch sub-tabs with `[` / `]`.
- **Sort moves off Tab.** `o` now picks the next sort column, `O` reverses
direction. `Tab` is reserved for section navigation.
- **Per-action shortcuts collapse into a Space-key menu.** `b` (Block IP),
`t` (Trace), `F` (SSH Forward), `p` (Copy PID), and the old fullscreen
toggles `4`/`5`/`6`/`7`/`8`/`9` are removed. Use `Space` → choose
action. Direct shortcuts remain only for `K` (Kill) and `c` (Copy).
- **Bottom Details panel is a single unified view** (no more 1/2/3
Tree/Network/Connection tabs). Combines bind type, interface, remote,
state, cmdline, related ports, and process tree in one scroll view.
- **Esc cascade is armed for filter clear.** First press shows
"Esc again to clear filter"; a second press inside 1.5s clears.
Same guard for the tunnel form when it has unsaved input.

### Added

- **Action menu** opened with `Space` — contextual list (Kill / Copy /
Copy PID / Block IP / Trace / SSH forward) with j/k navigation, Enter
to execute, 1..9 to jump.
- **Tunnel real status** — `TunnelStatus { Starting, Alive, Failed }`
replaces the hard-coded "alive". Failed tunnels stay visible in the
list (red) until the user restarts or removes them.
- **Tunnel edit mode** — `e` on the selected tunnel re-opens the form
with all fields pre-filled; Enter replaces the tunnel in place.
- **Inline form validation** — bad fields turn red as you type, no need
to wait for Enter.

### Removed

- **Chart fullscreen view** and its `4` shortcut.
- **Namespaces fullscreen view**, `7` shortcut, App `namespace_cache`,
and the `prt_core::core::namespace` module.

## [0.3.0] - 2026-04-05

### Added
Expand Down
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Note: `cargo` may require `export PATH="$HOME/.cargo/bin:$PATH"` on this machine

Network port monitor with TUI interface (ratatui) for macOS and Linux. Workspace with 2 crates:

- **prt-core** — library: model, scanner, killer, platform abstraction, i18n, session, config, known ports, alerts, suspicious detection, bandwidth, containers, namespaces, process detail, firewall
- **prt-core** — library: model, scanner, killer, platform abstraction, i18n, session, config, known ports, alerts, suspicious detection, bandwidth, containers, process detail, firewall
- **prt** — TUI binary (ratatui + crossterm + clap) with stream/watch/tracer/forward modules

**Data flow:** `platform::scan_ports()` → `Session::refresh()` → `scanner::diff_entries()` (tracks New/Unchanged/Gone with first_seen carry-forward) → enrich (service names, suspicious, containers) → retain (drop Gone after 5s) → `bandwidth.sample()` → `scanner::sort_entries()` → (in App::refresh) `alerts::evaluate()` → cache invalidation → `scanner::filter_indices()` → UI renders (ViewMode-based routing)
Expand All @@ -30,12 +30,12 @@ Network port monitor with TUI interface (ratatui) for macOS and Linux. Workspace
- `PortEntry` is the core data type; `TrackedEntry` wraps it with status (New/Unchanged/Gone), timestamp, and enrichment fields (first_seen, suspicious, container_name, service_name)
- Entry identity key is `(port, pid)` tuple — used in `diff_entries()` and focus stability (selection tracks by identity, not index)
- `Session` struct encapsulates the refresh/diff/retain/sort cycle — shared logic that UI delegates to
- `ViewMode` enum controls fullscreen views (Table/Chart/Topology/ProcessDetail/Namespaces); `DetailTab` enum controls bottom panel tabs (Tree/Interface/Connection)
- `ViewMode` enum is the top-level section: `Connections` / `Processes` / `Ssh` (Tab/Shift+Tab cycles). `ProcessesTab` and `SshTab` enums drive sub-tabs (`[` / `]`). The bottom Details panel under Connections is a single unified view (no tabs). Discrete actions live in the `Space`-key `ActionItem` menu.
- `ExportFormat` in core has no clap dependency; binary crate wraps it with `clap::ValueEnum`
- Gone entries are retained for 5 seconds before removal; auto-refresh every 2 seconds
- Config from `~/.config/prt/config.toml` — optional, missing file = defaults, parse error = stderr warning + defaults
- Error handling: `anyhow::Result` throughout, UI shows errors as status messages
- Caching: process detail and namespace data cached per-refresh (not per-frame) in App
- Caching: process detail data cached per-refresh (not per-frame) in App

**i18n system:** `prt-core/src/i18n/` — static `Strings` structs per language (en, ru, zh), `AtomicU8` for global state. Language set via `--lang` flag, `PRT_LANG` env, or auto-detected from system locale. Compile-time completeness check: adding a field to `Strings` forces all language files to be updated.

Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ members = ["crates/*"]
resolver = "2"

[workspace.package]
version = "0.4.0"
version = "0.5.0"
edition = "2021"
license = "MIT"
repository = "https://github.com/rekurt/prt"
Expand All @@ -24,4 +24,4 @@ uzers = "0.11"
clap = { version = "4", features = ["derive"] }
toml = "0.8"
dirs = "6"
prt-core = { path = "crates/prt-core", version = "0.4.0" }
prt-core = { path = "crates/prt-core", version = "0.5.0" }
140 changes: 74 additions & 66 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,57 +106,59 @@ The header bar shows system-wide network throughput: `▼ 1.2 MB/s ▲ 340 KB/s`

Press `Enter` or `d` to open the detail panel, then `1` to see the full parent chain for the selected process (e.g., `launchd → nginx → worker`). Built by traversing PPID relationships.

### Detail Panel Tabs
### Sections

The bottom panel (toggle with `Enter`/`d`) has three tabs:
`Tab` / `Shift+Tab` cycles between three top-level sections. The active
section is highlighted in the header.

| Tab | Key | Content |
|-----|-----|---------|
| **Tree** | `1` | Process parent chain |
| **Network** | `2` | Interface details, IP addresses, MTU |
| **Connection** | `3` | All connections for the selected PID |
| Section | Default content | Sub-tabs (`[` / `]`) |
|---------|-----------------|----------------------|
| **Connections** | Port table + bottom Details panel (toggle with `Enter` / `d`) | — |
| **Processes** | Selected entry's process detail (CWD, CPU %, RSS, open files, env, all connections, process tree) | Detail ⇄ Topology |
| **SSH** | Saved hosts and active tunnels in one place | Hosts ⇄ Tunnels |

### Fullscreen Views
The **Details** panel under the Connections table is a single unified view
combining bind type, interface, remote address, state, cmdline, related
ports, and the process tree — no tab switching needed.

Four dedicated views accessible with keys `4`-`7`:
The **Topology** sub-tab in Processes draws an ASCII tree
`process → :local_port → remote` for the whole working set.

| View | Key | Description |
|------|-----|-------------|
| **Chart** | `4` | Horizontal bar chart showing connection count per process |
| **Topology** | `5` | ASCII network graph: process → local port → remote host |
| **Process Detail** | `6` | Comprehensive info page: CWD, CPU %, RSS, open files, environment variables, all connections, network interfaces, process tree |
| **Namespaces** | `7` | Network namespace grouping (Linux only). Shows named namespaces from `/run/netns/` or raw inode numbers |
All scrollable views support `j`/`k` and `g`/`G`.

All fullscreen views support scrolling with `j`/`k` and `g`/`G`. Press `Esc` to return to the table.
### Action menu (`Space`)

### Firewall Quick-Block
Almost every action on the selected entry is reached through one
contextual popup, opened with `Space`:

Press `b` on a connection with a remote address to block that IP. A confirmation dialog shows the exact command that will be executed:
- **Kill process** (also bound to `K` directly)
- **Copy line** (also bound to `c` directly) / **Copy PID**
- **Block remote IP** — `iptables -A INPUT -s <IP> -j DROP` (Linux) /
`pfctl -t prt_blocked -T add <IP>` (macOS). Status bar shows the undo
command. Requires sudo.
- **Trace syscalls** — `strace -p <PID> -e trace=network -f` (Linux) or
`dtruss -p <PID>` (macOS, needs SIP disabled or root). Re-run to detach.
- **SSH forward** — opens the tunnel form so you can pick local port,
remote target, and host alias.

- **Linux:** `iptables -A INPUT -s <IP> -j DROP`
- **macOS:** `pfctl -t prt_blocked -T add <IP>`
The menu only shows actions that are valid for the current entry — Block
and Forward are hidden when there's no remote address.

The status bar shows the undo command after blocking. Requires sudo privileges.
### SSH section

### Strace / Dtruss Attach
`SSH` aggregates two sub-tabs:

Press `t` to attach a system call tracer to the selected process. The detail panel splits to show a live stream of network-related syscalls:
- **Hosts** — read-only list parsed from `~/.ssh/config` plus
`[[ssh_hosts]]` entries in `~/.config/prt/config.toml`. Press `Enter`
to open the tunnel form pre-filled with the host alias.
- **Tunnels** — running tunnels with live status: 🟢 alive, 🟡 starting,
🔴 failed (failures stay visible until you act on them).
Keys: `n` new · `e` edit · `K` kill · `r` restart · `s` save to config.

- **Linux:** `strace -p <PID> -e trace=network -f`
- **macOS:** `dtruss -p <PID>` (requires SIP disabled or root)

Press `t` again to detach. The tracer process is automatically killed on exit.

### SSH Port Forwarding

Press `F` (Shift+F) to create an SSH tunnel for the selected port. A dialog prompts for the remote host:

```
localhost:5432 →
host:port → user@server.io:5432█
```

The tunnel is created via `ssh -N -L <local>:localhost:<remote> <host>`. Active tunnels are shown in the header bar (`⇄ localhost:5432 → server:22`). Tunnels are health-checked each tick and automatically killed on exit via `Drop`.
The tunnel form does **inline validation** (bad fields turn red as you
type), supports **edit-mode** (Enter replaces the existing tunnel), and
**guards Esc** — discarding a non-empty form requires a second Esc
within 1.5 seconds.

### Alert Rules

Expand Down Expand Up @@ -254,47 +256,53 @@ sudo prt # run as root (see all processes)

**Navigation:**

**Global:**

| Key | Action |
|-----|--------|
| `j`/`k` `↑`/`↓` | Move selection / scroll |
| `g` / `G` | Jump to top / bottom |
| `/` | Search & filter (`!` = suspicious only) |
| `Esc` | Back to table / clear filter |
| `?` | Help (cheat sheet) |
| `q` | Quit |
| `Tab` / `Shift+Tab` | Next / previous section (Connections \| Processes \| SSH) |
| `Space` | Action menu (Kill / Copy / Block / Trace / Forward) |
| `/` | Search & filter (`!` = suspicious only) |
| `Esc` | Close modal · twice to clear an active filter |
| `r` | Refresh |
| `s` | Sudo prompt |
| `L` | Cycle language |
| `j`/`k` `↑`/`↓` `g`/`G` | Move / scroll · jump to top / bottom |

**Bottom panel (Table mode):**
**Direct shortcuts (any section):**

| Key | Action |
|-----|--------|
| `Enter` / `d` | Toggle detail panel |
| `1` `2` `3` | Tree / Network / Connection tab |
| `←`/`→` `h`/`l` | Switch detail tab |
| `K` / `Del` | Kill selected process |
| `c` | Copy line to clipboard |

**Fullscreen views:**
**Connections section:**

| Key | Action |
|-----|--------|
| `4` | Chart — connections per process |
| `5` | Topology — process → port → remote |
| `6` | Process detail — info, files, env |
| `7` | Namespaces (Linux only) |
| `Enter` / `d` | Toggle bottom Details panel |
| `o` / `O` | Next sort column / reverse direction |

**Actions:**
**Processes section:**

| Key | Action |
|-----|--------|
| `K` / `Del` | Kill process |
| `c` | Copy line to clipboard |
| `p` | Copy PID to clipboard |
| `b` | Block remote IP (firewall) |
| `t` | Attach/detach strace |
| `F` | SSH port forward (tunnel) |
| `r` | Refresh |
| `s` | Sudo prompt |
| `Tab` | Next sort column |
| `Shift+Tab` | Reverse sort direction |
| `L` | Cycle language |
| `?` | Help |
| `[` / `]` | Switch sub-tab (Detail \| Topology) |

**SSH section:**

| Key | Action |
|-----|--------|
| `[` / `]` | Switch sub-tab (Hosts \| Tunnels) |
| Hosts: `Enter` | New tunnel from selected host |
| Hosts: `r` | Reload `~/.ssh/config` and prt config |
| Tunnels: `n` | Open new-tunnel form |
| Tunnels: `e` | Edit selected tunnel (kill + respawn on submit) |
| Tunnels: `K` | Kill selected tunnel |
| Tunnels: `r` | Restart selected tunnel |
| Tunnels: `s` | Save active tunnels to config |

## Configuration

Expand Down Expand Up @@ -326,7 +334,7 @@ action = "bell"
```
crates/
├── prt-core/ # Core library (platform-independent)
│ ├── model.rs # PortEntry, TrackedEntry, ViewMode, DetailTab, enums
│ ├── model.rs # PortEntry, TrackedEntry, ViewMode, ProcessesTab, SshTab, ActionItem
│ ├── config.rs # TOML config loading (~/.config/prt/)
│ ├── known_ports.rs # Well-known port → service name database
│ ├── core/
Expand Down Expand Up @@ -397,7 +405,7 @@ This provides a fast “observe → contain → inspect” workflow.

### 3) Container port exposure audit

In container-heavy hosts, use **Topology** (`5`) and **Namespaces** (`7`) to spot
In container-heavy hosts, switch to **Processes → Topology** (`Tab` to Processes, `]` to Topology) to spot
unexpected exposure (e.g., debug ports, admin APIs, accidental public binds).

### 4) Runtime feature-flag verification
Expand Down
Loading
Loading