Skip to content

fix(sender): try every advertised address, not just the first - #48

Merged
Developer1010x merged 1 commit into
masterfrom
fix/openplay-address-fallback
Sep 13, 2026
Merged

Developer1010x merged 1 commit into
masterfrom
fix/openplay-address-fallback

Conversation

@Developer1010x

Copy link
Copy Markdown
Owner

The defect

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 sorts ahead of 192.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.md that OpenPlay "has never been reported working between two separate machines".

Observed

Running both binaries on this machine, the receiver advertised:

wlp0s20f3  IPv4  192.168.1.10     <- the only externally reachable address
docker0    IPv4  172.17.0.1
br-27e5..  IPv4  172.18.0.1

and the sender logged, and showed in its UI subtitle:

Receiver found name=spn addr=[172.17.0.1, 172.18.0.1, 192.168.1.10, fe80::...] port=7290

It does not fail on one machine, because the receiver binds [::]:7290 and 172.17.0.1 is local — which is why in-process tests could never have caught it.

Why ranking cannot fix it

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 address too. "Prefer 192.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_receiver walks 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_connectability is 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 — real SignalingServer on 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 warnings clean.

Also

openplay-crypto now re-exports rustls::ClientConfig. Without it a caller can hold what client_config_pinned returns 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

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>
@Developer1010x
Developer1010x merged commit afe3522 into master Sep 13, 2026
6 checks passed
@Developer1010x
Developer1010x deleted the fix/openplay-address-fallback branch September 13, 2026 11:06
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.

2 participants