Comprehensive bug-fix release driven by smoke testing + a full code review
This release replaces the v1.2.5 "sleep 500ms and hope" shutdown with a real probe-drain barrier, eliminates two classes of silent event loss, and adds three regression tests for subtle state-machine behaviour.
Fixed
- Re-catch ladder waste: Connect / raw / SYN-race schedulers no longer spawn a fresh ladder run when the port-state tracker reports an already-Open re-catch. Was hammering single-threaded upstream services and emitting orphan events the TUI silently discarded.
- Ladder fresh-connect timeout: inter-probe `TcpStream::connect` now bounded by `DEFAULT_PROBE_TIMEOUT`. An overloaded target no longer stalls the ladder indefinitely.
- Probe shutdown drain: catch-receiver tracks every spawned ladder run in a `tokio::task::JoinSet` and drains it via `join_next` (with a 2s outer deadline) before `EngagementFinished` is emitted. Real shutdown barrier, not a magic sleep.
- Mutex poison panic in `PortStateTracker`: replaced `.unwrap()` on lock with `.unwrap_or_else(|p| p.into_inner())`. The critical section can't leave inconsistent state.
- Spurious flap event: `WaitingForClose → Open` recovery transition no longer emits a fresh `PortOpenDetected` (no matching `PortClosedDetected` had been emitted, so re-emitting Open looked like a flap to consumers).
- SYN-race false-close: handoff connect failure after a confirmed SYN-ACK no longer feeds `Observation::Transient` to the tracker. TIME_WAIT / contention no longer flips a still-open port to Closed.
- Hold-open silent miss: `spawn_hold_open_manager` no longer papers over a missing `catch_id` with `CatchId::default()`. Logs an error and skips that catch.
- Hold-open CA failure log level: `warn!` → `error!` for both no-config-dir and CA load failures.
- u64 truncation on `Duration::as_millis()`: replaced with `u64::try_from(...).unwrap_or(u64::MAX)`.
Added
- TUI Port Status empty-state hint — first-time users on the default `ephemeral-iana` range now see guidance + example invocations instead of an empty table.
- Tests: `engine_label_round_trips_on_port_open_detected` and `one_transient_then_open_resets_without_emit`.
Changed
- Dispatcher doc-comment corrected: lossless delivery is received-scoped, not accepted-scoped (`broadcast::Lagged` still drops on slow subscribers).
- `PortLiveState::Flapping` doc-comment matches implementation (≥4 transitions).
Verified
Smoke-tested on Windows against a multi-threaded local HTTP listener: full pipeline (PortOpenDetected → HoldOpenReady → 3 ProbeAttempted → FingerprintCaptured{protocol=http11, conf=0.90} → CatchComplete → EngagementFinished) lands in JSONL with no events lost. CI green across Linux / macOS / Windows.
Install
```bash
cargo install --git https://github.com/IntegSec/PortSnatcher --tag v1.2.6 --bin portsnatcher
```
Prebuilt binaries attached below.
Full changelog
https://github.com/IntegSec/PortSnatcher/blob/v1.2.6/CHANGELOG.md