Skip to content

Let the NAT fix use a relay the blocked player can actually reach - #415

Open
greluc wants to merge 1 commit into
OhMyGuus:nightlyfrom
greluc:fix/natfix-keeps-server-relays
Open

Let the NAT fix use a relay the blocked player can actually reach#415
greluc wants to merge 1 commit into
OhMyGuus:nightlyfrom
greluc:fix/natfix-keeps-server-relays

Conversation

@greluc

@greluc greluc commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Turning the NAT fix on can move a player to a relay they cannot reach.

What it did

const config = SettingsStore.store.natFix ? DEFAULT_ICE_CONFIG_TURN : this.iceConfig;

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.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 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:

  • deduplicated against what the server already advertised — a server that lists both must not cause two allocations for one relay, and a relay's port range is finite;
  • plain URL first, since ICE tries candidates in the order given and TCP is the worse path for anyone who can use the other;
  • 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_TURN 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 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.ts and driven over six configurations, so the same script runs against either revision:

nightly this branch
NAT fix off, server offers a bare relay all [stun, turn:relay] ok — TCP form added
NAT fix on, server offers a bare relay relay [turn:turn.bettercrewl.ink] ok — the server's own relay
server advertises both transports substituted away ok — exactly one TCP entry
a turns: relay substituted away ok — left as advertised
NAT fix on, no relay advertised relay [bettercrewl.ink], UDP only ok — TCP form added too
UDP offered before TCP ok
every path asks for max-bundle no ok

Seven assertions, all failing before and passing after. The nightly column is worth reading on its own: every case collapses to the same hardcoded URL, whatever the server advertised.

lint, typecheck, format:check, verify:esm and build pass, and prettier reports 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

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

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
greluc force-pushed the fix/natfix-keeps-server-relays branch from 54e94c7 to 1655256 Compare September 9, 2026 12:43
@greluc greluc changed the title Let the NAT fix use the relay the server advertised Let the NAT fix use a relay the blocked player can actually reach Sep 9, 2026
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.

1 participant