fix(sender): bind the local SRT listener before dialing uplinks - #20
Merged
Conversation
run_sender_with_config bound the local SRT_LISTEN_PORT listener only after create_connections_from_ips had walked every bonded uplink sequentially (resolve + bind + connect per link). CeraUI spawns srtla_send and then immediately tells the encoder to SRT-connect to that port with no readiness handshake, so uplink setup latency left the port closed under an in-flight handshake and the operator got a hard SRT_REJ_TIMEOUT stream-start failure. Reproduced live on device on 2 of 3 start-stop-start cycles. The window scales with the bond: N modems means up to N sequential connects ahead of the bind, so the failure gets more likely on exactly the multi-link deployments this sender exists for. Binding a UDP port depends on nothing the uplinks provide, so the bind moves to the top of the function, ahead of read_ip_list and the connect loop. Nothing else changes: uplink setup, start_probing, SIGHUP reload, --dry-run and the main.rs startup order are untouched, and no parity-contract behavior is affected. tests/startup_bind_ordering.rs locks the ordering (unprivileged, no receiver needed); its two ordering assertions fail on the previous code.
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.
What
srtla_sendnow binds its local SRT listener (SRT_LISTEN_PORT) as the very first thingrun_sender_with_configdoes — before the ips file is read and before any bonded uplink isdialed. Previously the bind happened after
create_connections_from_ips().Adds
tests/startup_bind_ordering.rs(3 tests) to lock the ordering, plus the Rule-A docupdates in
AGENTS.mdandREADME.md.Why
CeraUI spawns
srtla_sendand then immediately tells the streaming engine to open an SRTconnection to
127.0.0.1:<SRT_LISTEN_PORT>. There is no readiness handshake between the twosteps — by design, since binding a UDP port should be instantaneous.
It wasn't.
create_connections_from_ips()is a sequential loop: oneresolve_remote+bindconnectawait per uplink. All of that ran ahead of the local bind, so the port the encoderwas already dialing stayed closed for the whole duration of bond setup. When the encoder's
handshake landed in that window it went unanswered and SRT gave up with rejection code 16
(
SRT_REJ_TIMEOUT), which surfaces to the operator as a hard, non-retriable stream-startfailure.
This was reproduced live on device: 2 of 3 start → stop → (1-3s) → start cycles failed with
internal engine error: transport connect: SRT connect failed (reason 16).The window scales with the bond — N modems means up to N sequential resolve+connect round
trips before the bind — so the failure gets more likely on exactly the multi-link
deployments this sender exists for, not less. Fixing it here rather than adding a retry in
CeraUI removes the race instead of papering over it.
How to verify
Full gate on the pinned nightly, all green:
The new test proves the regression, not just the fix. Reverting
src/sender/mod.rsto theold ordering and re-running
cargo test --test startup_bind_orderingfails 2 of 3 tests,with the captured process log showing the inversion directly:
After the fix those two lines are in the opposite order, and the multi-link test additionally
asserts the bind precedes even a failing uplink attempt — a link that errors or stalls must
not hold the local listener closed.
the_bound_port_is_really_taken_once_the_listener_is_loggedchecks the log isn't lying: oncelistening for SRTis emitted, binding the same address from the test must fail withAddrInUse.The tests need no privileges, no network namespaces, and no reachable receiver.
Risks
Low. The change is a pure statement move inside one function — the bind expression itself is
byte-identical, and nothing between the old and new position reads
local_listener.SIGHUP reload semantics, empty-start, and clean-shutdown unlinking are all untouched.
--dry-runnever reachesrun_sender_with_config, so that path (which must bind nosockets) is unaffected —
tests/dry_run.rsstill passes.main.rsstartup order (config listener, telemetry writer) is unchanged.reg.start_probing(&mut connections)and every other post-bind step still run in the samerelative order against the connection pool.
fails before attempting uplinks rather than after. That is strictly better — it no longer
dials modems for a session it cannot serve.