Skip to content

refactor(control): remove two latent panics on control-supplied wire data#254

Merged
GeiserX merged 1 commit into
mainfrom
fix/control-dialer-typed-error-no-panic
Jun 15, 2026
Merged

refactor(control): remove two latent panics on control-supplied wire data#254
GeiserX merged 1 commit into
mainfrom
fix/control-dialer-typed-error-no-panic

Conversation

@GeiserX

@GeiserX GeiserX commented Jun 15, 2026

Copy link
Copy Markdown
Owner

What

An adversarial control-client audit flagged two panics reachable (today only in theory) from control-server-supplied wire data. A hostile or MITM control server could otherwise drop the node off its control connection (DoS).

  • ts_control/src/control_dialer.rs — the DialMode::Ace arm of the dial-plan TcpDialer::dial was unimplemented!(). next_dialer skips every Ace candidate before dial is called, so it's unreachable today — but candidate.mode comes from the control-supplied MapResponse.control_dial_plan, so a future change that wires Ace dialing without re-checking the skip would turn it into a remotely-triggerable panic in the reconnect path. Return a typed io::ErrorKind::Unsupported error instead; the dial loop falls through to the next candidate / system DNS exactly as for any failed candidate.
  • ts_packetfilter_serde/src/ip_range.rsIpRange::iter_prefixes had _ => unreachable!() for a mixed-family (one v4, one v6) Range. The deserialize path (parse_range) rejects mismatched families so it's unreachable from the wire, but IpRange::Range is a public constructor — make the function total (empty iterator for the impossible case) so packet-filter compile can never panic the netmap-apply path even if a future internal caller builds one by hand.

Why no behavior change

  • The Ip dial arm is byte-identical (.await? preserved). The Ace arm is now a bare typed Err(...); both arms unify to io::Result<TcpStream> (the .timeout().await? folds Elapsed into io::Error), so no return is needed (clippy-clean).
  • The parser still rejects mismatched-family ranges — the mismatched_ip_kinds #[should_panic] test (which asserts IpRange::try_from("127.0.0.1-ffef::") errors) still passes. iter_prefixes is only ever called on parser-produced (same-family) ranges.

The rest of the control client audited clean: Noise IK pinned-server-key handshake (no downgrade/MITM), bounded 16 MiB netmap frame + 120s read watchdog, Result-based parsing with no production unwrap/index on wire data, fail-closed filter compile, n²-backoff map-poll reset only on substantive frames.

Verification

cargo build -p geiserx_ts_control (0 errors), cargo test -p geiserx_ts_control (233) + geiserx_ts_packetfilter_serde (15, incl. mismatched_ip_kinds), clippy -D warnings (0), fmt, cargo run -p checks (anti-leak guard) all green.

Created using Claude Code (Opus 4.8)

Summary by CodeRabbit

  • Bug Fixes
    • TCP connection handling has been improved to properly respond to unsupported protocol modes by returning helpful and clear error messages instead of causing unexpected application crashes
    • IP address range processing has been significantly enhanced to gracefully handle edge cases involving mixed IPv4 and IPv6 address combinations, substantially improving overall system stability

…data

An adversarial control-client audit flagged two panics reachable (today only
in theory) from control-server-supplied data — a hostile or MITM control
server could otherwise drop the node off its control connection:

- control_dialer.rs: the `DialMode::Ace` arm of the dial-plan TcpDialer was
  `unimplemented!()`. `next_dialer` skips every Ace candidate before `dial`
  is called, so it's unreachable today — but `candidate.mode` is
  control-controlled, so a future change wiring Ace dialing without
  re-checking the skip could turn it into a remotely-triggerable panic in the
  reconnect path. Return a typed `io::ErrorKind::Unsupported` error instead;
  the dial loop then falls through to the next candidate / system DNS exactly
  as it does for any failed candidate.

- ip_range.rs: `IpRange::iter_prefixes` had `_ => unreachable!()` for a
  mixed-family (one v4, one v6) Range. The deserialize path (`parse_range`)
  rejects mismatched families, so it's unreachable from the wire, but
  `IpRange::Range` is a public constructor — make the function total (empty
  iterator for the impossible case) so packet-filter compile can never panic
  the netmap-apply path even if a future internal caller builds one by hand.

Both are defense-in-depth: no behavior change on any reachable path. The
parser still rejects mismatched-family ranges (mismatched_ip_kinds test still
should_panic), and Ace candidates are still skipped before dialing. The rest
of the control client audited clean (Noise IK pinned-key handshake, bounded
16 MiB netmap frame + read watchdog, Result-based parsing, fail-closed filter
compile, n²-backoff map-poll).

Signed-off-by: Sergio <sergio@geiser.cloud>
@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6c011ab9-37ce-4eae-b137-0b7a197d5ccf

📥 Commits

Reviewing files that changed from the base of the PR and between a2fd8fa and e4f1709.

📒 Files selected for processing (2)
  • ts_control/src/control_dialer.rs
  • ts_packetfilter_serde/src/ip_range.rs

📝 Walkthrough

Walkthrough

Two independent panic removal fixes across two crates: ControlTcpDialer's DialMode::Ace arm returns a tokio::io::Error (Unsupported) instead of panicking via unimplemented!(), and IpRange::iter_prefixes returns an empty iterator for mixed-family endpoints instead of panicking via unreachable!().

Changes

Panic-to-error conversions

Layer / File(s) Summary
ControlTcpDialer: ACE arm returns Unsupported error
ts_control/src/control_dialer.rs
DialMode::Ace match arm now returns a failed future with ErrorKind::Unsupported instead of calling unimplemented!().
IpRange::iter_prefixes: mixed-family returns empty iterator
ts_packetfilter_serde/src/ip_range.rs
Mixed-family (V4/V6) endpoint arm returns an empty iterator instead of calling unreachable!(), with a doc comment describing the deserialization invariant.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~4 minutes

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/control-dialer-typed-error-no-panic

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@GeiserX GeiserX merged commit c62c3ca into main Jun 15, 2026
12 of 19 checks passed
GeiserX pushed a commit that referenced this pull request Jun 15, 2026
Release 0.39.0. Bundles the parity + anti-leak batch: magicsock CallMeMaybe immediate-ping (#246), peerless-STUN-stop (#247), STUN SOFTWARE+FINGERPRINT (#241); MagicDNS RD/RA+compression (#242) + SERVFAIL-not-NXDOMAIN (#248); DERP send rate-limit (#249); IPv4 fragment handling (#253); control Hostinfo/ProtoPortRange/NetInfo wire fixes (#244/#245); netcheck StunProber deletion (#250); forwarder subnet-SSRF doc+test (#252); control panic-hardening (#254).
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.

1 participant