Skip to content

Track entries by full tuple (pid/proto/addr/state); improve container lookup, sudo flow, SSH tunnels, and watch UI#6

Merged
rekurt merged 5 commits into
masterfrom
feat/analyze-project-for-improvements-and-issues
Apr 11, 2026
Merged

Track entries by full tuple (pid/proto/addr/state); improve container lookup, sudo flow, SSH tunnels, and watch UI#6
rekurt merged 5 commits into
masterfrom
feat/analyze-project-for-improvements-and-issues

Conversation

@rekurt

@rekurt rekurt commented Apr 10, 2026

Copy link
Copy Markdown
Owner

Motivation

  • Improve accuracy of connection change tracking by using a richer identity key that includes addresses, protocol and connection state.
  • Make container resolution and external command handling more robust and slightly faster by avoiding repeated linear lookups and not treating empty stdout as an error when the command succeeded.
  • Remove in-memory sudo password storage and simplify elevation usage, and improve UX for SSH tunnels and watch mode by surfacing immediate failures and showing "since" timestamps.

Description

  • Change the diff identity key in core::scanner from (local_port, pid) to (pid, protocol, local_addr, remote_addr, state) and add EntryKey, entry_key, and tracked_entry_key helpers, updating diff_entries and related tests accordingly.
  • Add Hash derives for Protocol and ConnectionState in model.rs to support use in hash keys.
  • Optimize container resolution in container.rs by building a HashSet of target PIDs for O(1) checks and stop treating an empty command stdout as a failure in run_with_timeout when the process exits successfully.
  • Remove storing the sudo password from Session and switch refresh/elevation logic to use is_elevated with scanner::scan_elevated, and stop exposing sudo_password from Session and App.
  • Improve SshTunnel::new in forward.rs to detect and surface immediate ssh failures by waiting briefly and reading stderr before returning a successfully spawned tunnel.
  • Enhance watch output in watch.rs to include a human-friendly "since" duration for UP/DOWN state and add format_since plus tests for the new behavior.
  • Update README.md with new watch examples, a new "High-ROI Use Cases" section, and a small dev instruction tweak to remove the hardcoded test count.

Testing

  • Ran cargo test --workspace, which executed the unit tests in prt-core and prt including the new diff_same_pid_port_but_different_remote_is_new and format_since_seconds tests, and all tests passed.
  • The new and existing scanner and watch unit tests were executed as part of the workspace test run and succeeded.

Codex Task

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 97ea33c26d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +51 to +52
let scan_result = if self.is_elevated {
scanner::scan_elevated()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Clear elevated mode when sudo cache is no longer valid

Switching refresh to scanner::scan_elevated() means macOS now silently falls back to unprivileged scan() when sudo -n credentials expire, but this path never clears is_elevated/is_root; the session can stay in a permanently "sudo OK" state even though privileged scans and sudo -n firewall actions no longer work. In that state users cannot re-enter a password from the UI because input handling only opens the sudo prompt when !is_root, so they are effectively stuck until restart.

Useful? React with 👍 / 👎.

let _ = stdout.read_to_string(&mut out);
}
return if out.is_empty() { None } else { Some(out) };
return Some(out);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep Podman fallback when Docker returns empty output

Returning Some(out) for successful commands with empty stdout changes control flow in resolve_container_names: docker_resolve now returns Some(empty_map) when Docker is installed but has zero running containers, so or_else(|| podman_resolve(...)) is never reached. On hosts where Docker is present but workloads run under Podman, container names will no longer be resolved.

Useful? React with 👍 / 👎.

rekurt and others added 2 commits April 11, 2026 11:44
fix: reset expired sudo elevation and restore podman fallback after

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0de283d7ce

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/prt/src/app.rs
if let Some((ip, _)) = self.confirm_block.take() {
let sudo_pw = self.session.sudo_password();
match firewall::execute_block(ip, sudo_pw) {
match firewall::execute_block(ip, None) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore interactive sudo path for firewall blocking

Calling firewall::execute_block(ip, None) forces the block action down the non-interactive sudo -n path, so it now fails whenever no sudo timestamp is already cached. This is a regression for non-root users, especially on Linux: pressing s no longer helps because scan_ports_with_sudo there does not invoke sudo (it just calls linux::scan()), so the app reports elevation but still cannot run iptables/pfctl from the block flow unless sudo was primed outside the app.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ef0e9140d8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/prt-core/src/core/session.rs Outdated
pub fn refresh(&mut self) -> Result<(), String> {
let scan_result = if let Some(password) = &self.sudo_password {
scanner::scan_with_sudo(password)
self.sync_elevation_state(scanner::has_elevated_access());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip unconditional sudo cache probes in refresh

Session::refresh now calls scanner::has_elevated_access() every cycle even when the user never requested elevation; on Linux/macOS that probe executes sudo -n true via Command::status() (see platform helpers), which inherits stderr. For non-root users without a cached sudo timestamp, sudo emits a password is required, so the app can spam terminal output every tick and disrupt the TUI. Please only probe when is_elevated is set (or silence stdio for the probe).

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

.stderr(Stdio::piped())

P2 Badge Drain SSH stderr for long-lived tunnels

The tunnel process is spawned with stderr(Stdio::piped()), but stderr is only consumed in the immediate-failure check; for successful startups there is no reader attached. If ssh emits ongoing stderr output at runtime (for example repeated forwarding errors), the pipe buffer can fill and block the child process, causing tunnel behavior to hang unexpectedly. Keep stderr redirected to null after startup validation or continuously drain it.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

docker_resolve(pids)
.or_else(|| podman_resolve(pids))
.unwrap_or_default()
select_runtime_names(docker_resolve(pids), podman_resolve(pids))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid probing Podman when Docker resolution already succeeded

resolve_container_names now calls select_runtime_names(docker_resolve(pids), podman_resolve(pids)), which eagerly evaluates both resolvers, so Podman CLI calls run every refresh even when Docker already returned container names. This is a regression from the prior lazy or_else behavior and can noticeably slow/lag refresh cycles on hosts where Podman commands are slow (including up to the timeout path), despite Docker data being sufficient.

Useful? React with 👍 / 👎.

@rekurt rekurt merged commit b78279f into master Apr 11, 2026
3 checks passed
@rekurt rekurt deleted the feat/analyze-project-for-improvements-and-issues branch April 12, 2026 08:52
rekurt pushed a commit that referenced this pull request Jun 12, 2026
…t, model health

Auto-reconnect (#1,#2,#3,#8):
- Backoff now actually grows for unreachable hosts. The reset was tied to
  a 'successful' 150ms spawn, which an unreachable host passes (it blocks
  on TCP timeout), so the delay never increased. Growth now happens on
  every attempt and is reset only after a tunnel stays Alive for
  STABLE_THRESHOLD (30s), via refresh_health.
- reconnect uses a new non-blocking restart_async() (no 150ms sleep in the
  render thread); failures are caught on the next tick. Manual restart()
  stays blocking for immediate feedback.
- After MAX_RETRIES attempts without recovery a tunnel is marked permanently
  failed (auto_reconnect=false); drop_failed() now prunes only those, so
  'save' no longer discards a tunnel that's mid-reconnect. auto_reconnect
  is now a meaningful flag.

Listener health in the model (#4,#5):
- New TunnelStatus::Unhealthy (process alive, local port not listening).
  Decision extracted to a pure, unit-tested decide_status(); a HEALTH_GRACE
  window and a scan_usable flag suppress false 'no listener' right after
  start and while auto-refresh is paused. cleanup() now takes the listening
  port set; the view no longer reaches into the scan.

Cleanups:
- command_string() shell-quotes args via shlex (#6).
- uptime column reuses prt-core format_duration (#7).
- private from_parts() constructor removes spawn duplication (#9).
- cross-reference comments between PROXY_PORTS and known_ports (#10).

New unit tests: decide_status, next_backoff, command_string quoting.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant