fix(sender): try every advertised address, not just the first - #48
Merged
Merged
Conversation
A receiver advertises every interface it has. On a machine running Docker that includes the bridge addresses, and `sort_by_connectability` puts them first: all routable IPv4 rank equally, so the tie breaks on the numeric value and `172.17.0.1` beats `192.168.1.10`. The sender dialled only `addresses.first()`, so on a second machine it connected to its *own* Docker bridge and reported the receiver as unreachable. This is consistent with OpenPlay never having been reported working between two machines. Observed on this machine: the sender's UI subtitle read `172.17.0.1:7290` for a receiver whose reachable address was `192.168.1.10:7290`. Ranking cannot fix this. The sender's own `docker0` is `172.17.0.1/16`, so "prefer an address on the same subnet as a local interface" prefers the wrong one too, and "prefer 192.168/16" is wrong on every 10.x corporate LAN. There is no property of the address itself that distinguishes the receiver's bridge from the sender's. What does distinguish them is already here: the pinned certificate. Only the receiver can complete the handshake, so trying each address in turn is a search with an exact answer rather than a guess. `connect_to_receiver` walks the list and stops at the first address that completes a pinned TLS handshake, with a 5-second per-address timeout — a bridge address black-holes rather than refusing, and the default TCP timeout would leave the user watching a spinner for minutes. `sort_by_connectability` is left alone: it still provides determinism, and the test added here asserts the ordering it produces rather than an ordering that would be safe to dial blindly — so if the ranking ever changes, that test says whether the fallback is still needed. `openplay-crypto` re-exports `rustls::ClientConfig`, without which a caller can hold what `client_config_pinned` returns but cannot name it in a function signature. Not verified between two machines — that still needs a second machine. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
The defect
A receiver advertises every interface it has. On a machine running Docker that includes the bridge addresses, and
sort_by_connectabilityputs them first: all routable IPv4 rank equally, so the tie breaks on the numeric value and172.17.0.1sorts ahead of192.168.1.10.The sender dialled only
addresses.first(), with no fallback. On a second machine that address is the sender's own Docker bridge, so the connection goes nowhere and the receiver is reported unreachable.This is consistent with the note in
CLAUDE.mdthat OpenPlay "has never been reported working between two separate machines".Observed
Running both binaries on this machine, the receiver advertised:
and the sender logged, and showed in its UI subtitle:
It does not fail on one machine, because the receiver binds
[::]:7290and172.17.0.1is local — which is why in-process tests could never have caught it.Why ranking cannot fix it
The sender's own
docker0is172.17.0.1/16, so "prefer an address on the same subnet as a local interface" prefers the wrong address too. "Prefer192.168/16" is wrong on every 10.x corporate LAN. No property of the address itself separates the receiver's bridge from the sender's.The fix
The thing that does separate them is already in the codebase: the pinned certificate. Only the receiver can complete the handshake, so trying each address in turn is a search with an exact answer rather than a guess.
connect_to_receiverwalks the advertised list and stops at the first address that completes a pinned TLS handshake, with a 5-second per-address timeout — a bridge address black-holes rather than refusing, and the default TCP timeout would leave the user on a spinner for minutes.sort_by_connectabilityis deliberately left alone. It still provides determinism, and the test added here asserts the ordering it actually produces rather than one that would be safe to dial blindly, so if the ranking ever changes that test says whether the fallback is still needed.Tests
a_dead_first_address_does_not_stop_the_connection— realSignalingServeron loopback, a closed port first in the list, asserts the live address is reached.every_address_dead_reports_what_was_tried— the error names the address that failed.bridge_addresses_still_sort_ahead_of_the_real_lan_address— pins the ordering as a fact, with the reasoning for why sorting alone is not a fix.cargo test --all: 323 passing, 0 failures. fmt and clippy-D warningsclean.Also
openplay-cryptonow re-exportsrustls::ClientConfig. Without it a caller can hold whatclient_config_pinnedreturns but cannot name it in a signature, which forces the connection logic to stay inline at the call site.Not verified
Still not tested between two machines. This is the most likely cause of that failure and it is fixed by construction and by test, but confirming it needs a second machine.
🤖 Generated with Claude Code