feat(matchmaking): let trusted servers override advertised address for NAT/tunnels - #249
feat(matchmaking): let trusted servers override advertised address for NAT/tunnels#249itpick wants to merge 4 commits into
Conversation
…r NAT/tunnels
Game servers register via POST /ut/api/matchmaking/session, and
CreateGameServer unconditionally overwrites the declared serverAddress with
the request's source IP. For a server behind NAT or a tunnel (playit.gg,
Cloudflare Spectrum, home servers behind CGNAT), that source IP is the egress
address, not the ingress address players must connect to, so the browser lists
an unreachable server.
For TRUSTED servers only, allow an operator to opt in (both mechanisms default
OFF, so upstream behavior is unchanged) to advertising the real public address
via a new top-level "Trusted" config section:
- Trusted:AllowDeclaredAddress (bool) -- honor the serverAddress the server
declared in its request body, when it is a valid non-"0.0.0.0" IP.
- Trusted:AddressOverrides (dict sourceIP -> publicIP) -- map the request
source IP to a configured public IP.
The declared value is captured before the source-IP overwrite. An override is
logged at Info level (server id, from -> to, reason). Untrusted-server
behavior is intentionally left unchanged.
There was a problem hiding this comment.
Pull request overview
Adds configuration-gated support for trusted UT4 dedicated servers to advertise a different serverAddress than the request source IP, enabling NAT/tunnel deployments to show a reachable ingress address while keeping untrusted behavior unchanged.
Changes:
- Registers a new
Trustedconfiguration section bound toTrustedGameServerSettings. - Captures the server-declared address before overwriting with source IP, and applies trusted-only overrides (declared address or source→public mapping).
- Introduces
TrustedGameServerSettingsto model the opt-in override mechanisms.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| UT4MasterServer/Program.cs | Binds new top-level Trusted settings section into DI options. |
| UT4MasterServer/Controllers/UT/MatchmakingController.cs | Applies trusted-only server address override logic during session registration. |
| UT4MasterServer.Models/Settings/TrustedGameServerSettings.cs | Defines settings for allowing declared addresses and source-IP→public-IP overrides. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- trim declared/mapped addresses so a stray-whitespace value (e.g. " 0.0.0.0") cannot slip past the "0.0.0.0" guard while IPAddress.TryParse still accepts it - validate the config-driven AddressOverrides value the same way as the declared address (non-empty, != 0.0.0.0, parseable IP) so a misconfigured entry cannot make the master advertise an invalid/unreachable address
|
@timiimit - please merge and update master server with this so that I can connect my hub. Thanks! |
|
@Saibamen - would be awesome if you can see this one getting merged in! Thanks :) |
|
@timiimit - you alive? |
… IPv6 key (Copilot review) TryGetValue could throw if AddressOverrides is bound null (JSON "AddressOverrides": null); and an IPv4-mapped IPv6 source (::ffff:1.2.3.4) never matched plain-IPv4 config keys. Guard the dictionary and look it up by a normalized IPv4 key.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
UT4MasterServer/Controllers/UT/MatchmakingController.cs:134
- The config-driven override validates against "0.0.0.0" but would still allow the IPv6 unspecified address ("::") (or any value that parses to IPAddress.Any/IPAddress.IPv6Any). It’s safer to reject unspecified addresses based on the parsed IP rather than string matching.
&& addressOverrides.TryGetValue(overrideKey, out var mappedAddress)
&& mappedAddress?.Trim() is { Length: > 0 } trimmedMapped
&& trimmedMapped != "0.0.0.0"
&& IPAddress.TryParse(trimmedMapped, out _))
{
UT4MasterServer/Controllers/UT/MatchmakingController.cs:118
- The declared-address override blocks only the IPv4 unspecified address ("0.0.0.0") but would still accept the IPv6 unspecified address ("::") (and any other value that parses to IPAddress.Any/IPAddress.IPv6Any). That would cause the master to advertise an explicitly non-routable address even when overrides are enabled.
This issue also appears on line 130 of the same file.
if (overrides.AllowDeclaredAddress
&& !string.IsNullOrWhiteSpace(declaredAddress)
&& declaredAddress != "0.0.0.0"
&& IPAddress.TryParse(declaredAddress, out _))
{
…ress override The UE5.8 UT client shim advertises its ServerAddressOverride via an X-Forwarded-For header rather than the request body, so the AllowDeclaredAddress body path missed those servers entirely. When AllowDeclaredAddress is enabled and the trusted server declares no valid body address, fall back to the left-most X-Forwarded-For entry (validated: non-empty, not 0.0.0.0, parseable IP). Precedence: body declaredAddress, then XFF fallback, then AddressOverrides map, then the default source-IP behavior. Remains default-OFF and trusted-only.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Suppressed comments (3)
UT4MasterServer/Controllers/UT/MatchmakingController.cs:169
- The config-driven AddressOverrides validation only rejects the IPv4 unspecified address string ("0.0.0.0"). A misconfiguration like "::" (IPv6Any) or a loopback value would still parse and be advertised. Validate the parsed IP and reject IPAddress.Any/IPAddress.IPv6Any and loopback to avoid advertising clearly invalid addresses.
else if (overrides.AddressOverrides is { } addressOverrides
&& addressOverrides.TryGetValue(overrideKey, out var mappedAddress)
&& mappedAddress?.Trim() is { Length: > 0 } trimmedMapped
&& trimmedMapped != "0.0.0.0"
&& IPAddress.TryParse(trimmedMapped, out _))
UT4MasterServer/Controllers/UT/MatchmakingController.cs:154
- The X-Forwarded-For fallback uses the same string check for "0.0.0.0" as the body-declared path, so it would still allow an unspecified IPv6 address ("::") or loopback address to be advertised if provided in the header. Validating the parsed IP and rejecting IPAddress.Any/IPAddress.IPv6Any and loopback would prevent advertising obviously-unreachable addresses.
else if (overrides.AllowDeclaredAddress
&& !string.IsNullOrWhiteSpace(forwardedForAddress)
&& forwardedForAddress != "0.0.0.0"
&& IPAddress.TryParse(forwardedForAddress, out _))
{
UT4MasterServer/Controllers/UT/MatchmakingController.cs:140
- The declared-address validation only rejects the IPv4 unspecified address string ("0.0.0.0"). If a trusted server declares an unspecified/placeholder IPv6 address ("::") or a loopback address, it will still pass IPAddress.TryParse and be advertised to clients, producing an unreachable server entry. Consider validating the parsed IP instead of comparing strings, and explicitly rejecting IPAddress.Any/IPAddress.IPv6Any and loopback.
This issue also appears in the following locations of the same file:
- line 150
- line 165
if (overrides.AllowDeclaredAddress
&& !string.IsNullOrWhiteSpace(declaredAddress)
&& declaredAddress != "0.0.0.0"
&& IPAddress.TryParse(declaredAddress, out _))
{
Problem
UT4 dedicated servers register with the master via
POST /ut/api/matchmaking/session(MatchmakingController.CreateGameServer). The handler unconditionally overwrites the server's declaredserverAddresswith the source IP of the registration request:That is correct for a server on a routable public IP. But for a server behind NAT or a tunnel — playit.gg, Cloudflare Spectrum, or a home server behind CGNAT — the source IP the master sees is the server's egress address, which is not the ingress address players must connect to. The result is that the server browser lists an address nobody can reach.
Change
For trusted servers only (
trust != Untrusted, using the existingTrustedGameServerService), an operator can opt in to advertising the real public address. Both mechanisms are config-gated and default OFF, so upstream behavior is unchanged unless explicitly enabled. Untrusted-server behavior is left exactly as-is.A new top-level
Trustedconfig section (bound viaIOptions<TrustedGameServerSettings>, matching the existing settings pattern) provides two opt-ins:Trusted:AllowDeclaredAddress(bool) — when true, honor theserverAddressthe server declared in its request body, provided it is a valid, non-empty, non-"0.0.0.0"IP. The declared value is captured before the source-IP overwrite.Trusted:AllowDeclaredAddress—X-Forwarded-Forfallback — the UE5.8 UT client shim (OnlineSessionUT::RegisterServer) advertises itsServerAddressOverridevia anX-Forwarded-Forrequest header rather than in the body, so the body path above misses those servers entirely. Under the sameAllowDeclaredAddressgate, when the trusted server declares no valid body address, fall back to the left-mostX-Forwarded-Forentry (validated the same way: non-empty, non-"0.0.0.0", parseable IP). Captured before the source-IP overwrite, same as the body path. Logged with reasonxff-fallback.Trusted:AddressOverrides(dictionarysourceIP -> publicIP) — otherwise, if the request's source IP is a key in this map, advertise the mapped public IP instead.Precedence: explicit body
declaredAddress→X-Forwarded-Forfallback →AddressOverridesmap → the default source-IP behavior.When an override is applied, it is logged at Info level (server id, from → to, and reason).
Example config (environment variables)
Notes
ServerAddressfor trusted servers can differ.dotnet publish -c Release(the repo Dockerfile) — compiles clean.🤖 Generated with Claude Code