Skip to content

feat(matchmaking): let trusted servers override advertised address for NAT/tunnels - #249

Open
itpick wants to merge 3 commits into
timiimit:masterfrom
ut4-hub:feat/trusted-server-address-override
Open

feat(matchmaking): let trusted servers override advertised address for NAT/tunnels#249
itpick wants to merge 3 commits into
timiimit:masterfrom
ut4-hub:feat/trusted-server-address-override

Conversation

@itpick

@itpick itpick commented Jul 24, 2026

Copy link
Copy Markdown

Problem

UT4 dedicated servers register with the master via POST /ut/api/matchmaking/session (MatchmakingController.CreateGameServer). The handler unconditionally overwrites the server's declared serverAddress with the source IP of the registration request:

server.ServerAddress = ipClient.ToString();

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 existing TrustedGameServerService), 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 Trusted config section (bound via IOptions<TrustedGameServerSettings>, matching the existing settings pattern) provides two opt-ins:

  1. Trusted:AllowDeclaredAddress (bool) — when true, honor the serverAddress the 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.
  2. Trusted:AddressOverrides (dictionary sourceIP -> publicIP) — otherwise, if the request's source IP is a key in this map, advertise the mapped public IP instead.

When an override is applied, it is logged at Info level (server id, from → to, and reason).

Example config (environment variables)

Trusted__AllowDeclaredAddress=true
Trusted__AddressOverrides__23.123.225.250=69.9.179.19

Notes

  • No change to the model or wire format; only the value assigned to ServerAddress for trusted servers can differ.
  • Built with dotnet publish -c Release (the repo Dockerfile) — compiles clean.

🤖 Generated with Claude Code

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Trusted configuration section bound to TrustedGameServerSettings.
  • Captures the server-declared address before overwriting with source IP, and applies trusted-only overrides (declared address or source→public mapping).
  • Introduces TrustedGameServerSettings to 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.

Comment thread UT4MasterServer/Controllers/UT/MatchmakingController.cs Outdated
Comment thread UT4MasterServer/Controllers/UT/MatchmakingController.cs Outdated
- 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
@itpick

itpick commented Jul 26, 2026

Copy link
Copy Markdown
Author

@timiimit - please merge and update master server with this so that I can connect my hub. Thanks!

@itpick

itpick commented Jul 26, 2026

Copy link
Copy Markdown
Author

@Saibamen - would be awesome if you can see this one getting merged in! Thanks :)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comment thread UT4MasterServer/Controllers/UT/MatchmakingController.cs
@itpick

itpick commented Jul 28, 2026

Copy link
Copy Markdown
Author

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 _))
			{

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.

3 participants