Skip to content

feat(tunnels): detect intermittent listener flapping#16

Merged
rekurt merged 2 commits into
masterfrom
claude/exciting-brown-p4waxl
Jun 15, 2026
Merged

feat(tunnels): detect intermittent listener flapping#16
rekurt merged 2 commits into
masterfrom
claude/exciting-brown-p4waxl

Conversation

@rekurt

@rekurt rekurt commented Jun 15, 2026

Copy link
Copy Markdown
Owner

What

Adds a listener flapping health signal for ssh -L/-D tunnels in the Tunnels view. Today the view has a binary listener check — green alive vs yellow no listener (a LISTEN socket owned by the tunnel's ssh PID is gone right now). That only catches a full bind failure. This PR catches the "degrades slowly rather than breaks all at once" case: a listener that is intermittently present across scans (leaked descriptors, stuck connections, a flaky ssh -D upstream), surfacing it as a distinct flapping status.

This is observation-only — prt monitors ports, it never proxies traffic, so this reuses data already scanned and opens no new connections. The auto-reconnect loop and last_status are untouched.

How

  • SshTunnel keeps a capped VecDeque<bool> of recent listener-presence observations. is_flapping() is true when the window has enough samples (≥4 of 6) and holds both a present and an absent sample. History clears on respawn so a restarted tunnel starts clean.
  • Recording happens once per scan in App::refresh, only for Alive tunnels past the startup grace window (and only while auto-refresh is active). This mirrors the existing LISTENER_GRACE guard so a freshly (re)started tunnel the scan hasn't observed yet never forges a phantom flap.
  • Shared helpers: LISTENER_GRACE and entry_has_listener moved to app.rs and reused by both the recorder and the views::tunnels renderer, so the grace window and the PID-match invariant live in one place.
  • Rendering is now three-state: no listener (yellow, gone now — keeps priority) › flapping (light yellow, present now but recently dropped) › alive (green).
  • i18n: new tunnel_health_flapping string for en (flapping) / ru (нестабилен) / zh (监听抖动).

Tests

Six new unit tests in forward.rs cover the flapping predicate and window capping (stable-present, all-absent, alternating, insufficient samples, empty, eviction). Logic is extracted into pure free functions so it's testable without spawning a real ssh child. Full suite green; clippy and fmt clean.

https://claude.ai/code/session_01BHfdoVE66Dag2SEFtzNSR3


Generated by Claude Code

Track a short window of listener-presence observations per SSH tunnel and
surface a distinct 'flapping' status when an Alive -L/-D tunnel's local
LISTEN socket comes and goes across scans. This catches a tunnel that
degrades slowly (listener intermittently present) rather than only the
binary 'no listener' (gone right now) case.

- SshTunnel records one listener observation per confirmable scan into a
  capped VecDeque; is_flapping() reports a window holding both present and
  absent samples. History clears on respawn.
- Recording lives in App::refresh (only past the startup grace window and
  while auto-refresh is active) so stale/premature samples never forge a
  phantom flap. Shared LISTENER_GRACE + entry_has_listener helper between
  the recorder and the tunnels view.
- New tunnel_health_flapping i18n string (en/ru/zh), rendered in light
  yellow, distinct from 'no listener' (yellow) which keeps priority.

https://claude.ai/code/session_01BHfdoVE66Dag2SEFtzNSR3
@rekurt rekurt marked this pull request as ready for review June 15, 2026 20:40
@rekurt

rekurt commented Jun 15, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@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: beb85c8a41

ℹ️ 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
diff_entries retains a vanished LISTEN socket in session.entries as
EntryStatus::Gone for GONE_RETENTION (5s) before removal. entry_has_listener
matched it regardless of status, so a single-refresh listener drop recorded
'true' for the missing socket — the flapping window never saw the absence and
the new signal stayed hidden. It also kept the binary 'no listener' check green
for up to 5s after the socket actually died. Filter out Gone so short drops are
observable. Reported by Codex review.

https://claude.ai/code/session_01BHfdoVE66Dag2SEFtzNSR3
@rekurt rekurt merged commit fc69228 into master Jun 15, 2026
3 checks passed
@rekurt rekurt deleted the claude/exciting-brown-p4waxl branch June 15, 2026 21:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants