fix(cli): M6 CP6 — single-UDP-transport startup guard - #153
Merged
Conversation
added 2 commits
July 17, 2026 04:07
Two transport-wiring passes disagree if more than one runs: the channel driver keeps the LAST transport (set_transport is last-wins) while the global event handler keeps the FIRST (set_global_event_handler is a first-wins OnceLock), so two UDP transports would bind different sockets and race (inbound handled on one, outbound sent on the other). Add a startup guard, single_udp_transport(), that asserts exactly one UDP bind and fails startup fast (Err from startup_sequence) rather than binding two. Two identical binds deduped to one still pass; two distinct binds are rejected. Tests: single_udp_transport accepts exactly one, rejects two distinct, rejects empty. RED: make the guard return Ok for >1 -> the reject-two test fails (accepts 0.0.0.0:5060).
Extract udp_transport_binds (collect + de-dup) and resolve_single_udp_bind (binds + guard), and route startup through resolve_single_udp_bind — the single path startup uses. Config-level tests now exercise that exact entry point: two DISTINCT udp transports are rejected, two IDENTICAL dedup to one and are accepted (pinning dedup-before-guard), and no-config falls back to the default. So the tests fail RED if the guard is removed from the startup path or dedup is reordered after it, not only if the helper itself is changed.
Owner
Author
Codex review disposition (gpt-5.5, xhigh)Codex verified placement (guard after de-dup, before any SipStack::new/set_transport/set_global_event_handler), cardinality, loop semantics (from_ref preserves the item type + single iteration), fail-fast appropriateness, and UDP+TLS still accepted (only udp binds enter the guarded set). One finding:
CI green; workspace test + clippy green locally. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
M6 CP6 — single-UDP-transport startup guard
FINDING. rustisk wires the SIP transport through two passes that DISAGREE if more than one runs:
the channel driver keeps the LAST transport it is handed (
set_transportis last-wins) while theglobal event handler keeps the FIRST (
set_global_event_handleris a first-winsOnceLock). Withtwo configured UDP transports the startup loop runs twice, so inbound would be handled on the first
socket while outbound is sent on the second — a silent split-brain / race.
FIX. A startup guard,
single_udp_transport(&[SocketAddr]) -> Result<SocketAddr, String>,asserts exactly ONE UDP bind after de-dup and fails startup fast (
Errpropagated fromstartup_sequence) rather than binding two. The transport loop now iterates over the single guardedbind. Two identical binds (deduped to one) still pass; two DISTINCT binds are rejected with a message
explaining the single-transport rule.
PROOF + RED. Unit tests: accepts exactly one (returns the bind unchanged); rejects two distinct
(the guard's whole purpose); rejects empty. RED — make the guard return
Ok(many[0])for>1:(the guard silently accepts two → the reject-two assertion fails).
Ordering note. CP6 is independent of CP5 (it only touches
crates/rustisk-cli/src/main.rs),landed ahead of CP5 to bank its value; CP5 branches cleanly on top.
GATES.
cargo test --workspace --exclude pjsip-shimgreen;cargo clippy --workspace --exclude pjsip-shim -- -D warningsclean. Rust 1.97.0.