Let the NAT fix use a relay the blocked player can actually reach - #415
Open
greluc wants to merge 1 commit into
Open
Let the NAT fix use a relay the blocked player can actually reach#415greluc wants to merge 1 commit into
greluc wants to merge 1 commit into
Conversation
|
Download the artifacts for this pull request:
This service is provided by nightly.link. These artifacts will expire in 90 days and will not be available for download after that time. |
The NAT fix means "route everything through a relay", not "route everything through this particular relay". Substituting DEFAULT_ICE_CONFIG_TURN threw away what the server sent, so a self-hosted deployment with its own relay had its players' audio sent through turn.bettercrewl.ink instead, and any transport, port or credential that operator configured went with it. The policy is now forced onto the list we were given, and the built-in relay is used only when the server offered none to force through. That alone is not enough to connect. A turn: URL with no ?transport= means UDP, and the networks that need a relay are largely the ones blocking UDP: a player there gets working signalling, avatars that light up when people talk, a lobby that looks healthy, and no audio in either direction, because no allocation is ever made. So every bare turn: URL now also gets a TCP form, deduplicated against what the server already advertised -- a server that lists both must not cause two allocations for one relay -- with the plain URL still first, since ICE tries them in order and TCP is the worse path for anyone who can use the other. turns: is TLS over TCP already and is left alone, as is any URL whose author named its transport. The built-in fallback gets the same treatment, which is the case that made this worth doing: it is a single UDP-only URL, so before this a player who turned the NAT fix on, on a server advertising no relay, was moved to a relay they could not reach by the very switch meant to rescue them. Connections also ask for max-bundle. Voice and the data channel then share one transport: one set of connectivity checks, one DTLS handshake, and one relay allocation per peer rather than two at peak. A ten-player lobby is forty-five connections allocating at once, and a relay's reservations are finite. Both ends of every connection are this same client, so there is no peer that might not support it. Checked by extracting the decision and the helper from the source and running them over six configurations: seven assertions, all failing before and passing after, including that a server advertising both transports still yields exactly one TCP entry and that a turns: relay is left untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Lucas Greuloch (greluc) <lucas.greuloch@gmail.com>
greluc
force-pushed
the
fix/natfix-keeps-server-relays
branch
from
September 9, 2026 12:43
54e94c7 to
1655256
Compare
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.
Turning the NAT fix on can move a player to a relay they cannot reach.
What it did
The switch means "route everything through a relay", not "route everything through this relay". Substituting the constant throws away what the server sent, so a self-hosted deployment with its own relay had its players' audio sent through
turn.bettercrewl.inkinstead — and any transport, port or credential that operator configured went with it.The policy is now forced onto the list we were given, and the built-in relay is used only when the server offered none to force through.
That alone does not connect
A
turn:URL with no?transport=means UDP. The networks that need a relay are largely the ones blocking UDP — schools, offices, some mobile carriers — so a player there gets working signalling (it is WebSocket over TLS), avatars that light up when people talk, a lobby that looks completely healthy, and no audio in either direction, because no allocation is ever made.So every bare
turn:URL now also gets a TCP form:turns:untouched — it is TLS over TCP already — as is any URL whose author named its transport.The built-in fallback gets the same treatment, and that is the case that made this worth doing.
DEFAULT_ICE_CONFIG_TURNis a single UDP-only URL, so before this a player who turned the NAT fix on, on a server advertising no relay, was moved by the rescue switch to a relay they could not reach.One transport per peer
Connections now ask for
bundlePolicy: 'max-bundle'. Voice and the data channel share one transport: one set of connectivity checks, one DTLS handshake, and one relay allocation per peer instead of two at peak.Honest about the size of this one: BUNDLE is negotiated by both ends anyway, so the extra transport is merged and its allocation released once the answer arrives — the cost is peak, not steady-state. It matters because a ten-player lobby is forty-five connections allocating at once during a fill, and a relay's reservations are finite. Both ends of every connection are this same client, so there is no peer that might not support it.
Verification
The decision and the helper are extracted from
ConnectionController.tsand driven over six configurations, so the same script runs against either revision:all [stun, turn:relay]relay [turn:turn.bettercrewl.ink]turns:relayrelay [bettercrewl.ink], UDP onlySeven assertions, all failing before and passing after. The
nightlycolumn is worth reading on its own: every case collapses to the same hardcoded URL, whatever the server advertised.lint,typecheck,format:check,verify:esmandbuildpass, andprettierreports the file unchanged. CI was run on a fork before this was pushed.Limits, stated plainly
Nothing here was run against a real UDP-blocking network or a real relay. What is verified is which ICE configuration the client builds, not that a TURN server answers on TCP at the port it advertises — a relay that does not simply produces no candidate from the extra entry, which is why adding it is cheap.
The TCP form is added on every path, not only when the NAT fix is on. That is deliberate — a player behind a UDP-blocking firewall is in the same trouble whether or not they found the switch — but it does mean this changes the candidate list for everyone, not just for people who turned something on.
I have not measured the peak-allocation claim against a live relay's counters; it follows from one transport rather than two, not from an experiment.
Disclaimer
This change was written with AI assistance (Claude Code). The author reviewed it, and the table above is real output from a script driving the real code on both revisions, not a description of intent. Please review it as you would any patch from a stranger; if something here is wrong I would rather hear it than have it merged.
🤖 Generated with Claude Code