Skip to content

Add support for V3 tunnels#845

Open
11EJDE11 wants to merge 143 commits into
CnCNet:developfrom
11EJDE11:new-v3-tunnels
Open

Add support for V3 tunnels#845
11EJDE11 wants to merge 143 commits into
CnCNet:developfrom
11EJDE11:new-v3-tunnels

Conversation

@11EJDE11

@11EJDE11 11EJDE11 commented Oct 11, 2025

Copy link
Copy Markdown
Member

Closes #349

  • V2 tunnels remain fully supported.
  • Adds V3 Static tunnels: One host-chosen tunnel shared by all players (similar to V2).
  • Adds V3 Dynamic tunnels: Each pair of players automatically negotiates the optimal tunnel/P2P between them.

How V3 Tunnels Work

In V2, all players share a single tunnel.
Example:

Player1 (Australia)
Player2 (New Zealand)
Player3 (England)
Tunnel: France

For P1 to send data to P2:
P1 Australia -> Tunnel France -> P2 NZ  = 550ms

In V3 dynamic mode, each pair of players negotiates the best tunnel route:

P1 <--> P2  -> Tunnel: Australia (50ms)
P1 <--> P3  -> Tunnel: France (300ms)
P2 <--> P3  -> Tunnel: Seattle (310ms)
So now for P1 to send data to P2:
P1 Australia > Tunnel Australia > P2 NZ = 50ms (90.9% faster)
And the slowest link is now 310ms instead of 550ms (43.6% faster overall).

The client now acts as an intermediary: the game connects to the client, which then routes packets to the negotiated tunnels. This is for both static and dynamic V3.


Negotiation Process

Negotiations occur automatically when players join a lobby or the tunnel mode changes, and takes a few seconds to run once the player list has arrived.

Phase 1 - Relay Negotiation

  1. Each player pair tests all available V3 relay tunnels.
    Which tunnels are tested is controlled by the Ping unofficial CnCNet tunnels option - when off, only official and recommended tunnels are used.
  2. One player in each pair is designated as the decider (lower player ID; ties broken by name ordering).
  • The non-decider sends Connected packets to every candidate tunnel every 500 ms.
  • The decider, on receiving a Connected packet through a tunnel, sends 5 PingRequest packets and measures round-trip time.
  • The non-decider replies with PingResponse packets.
  1. Once 50% of tunnels have been tested, the decider selects the best tunnel (lowest RTT, with a packet-loss penalty) and sends a TunnelChoice packet, which includes the measured ping, packet loss, and the decider's P2P capability.
  2. The non-decider acknowledges with TunnelAck, also advertising whether they have P2P enabled. Relay negotiation is now complete.

Phase 2 - P2P Upgrade Round (optional; requires both players to have P2P enabled)

After a relay tunnel is agreed:

  1. Each player gathers their P2P candidates: all local LAN addresses plus a STUN-discovered public (reflexive) address.
  2. Candidates are exchanged through the agreed relay tunnel via P2PInfo packets.
  3. Both players send Connected packets directly to each other's candidates - unlike relay negotiation, both sides punch simultaneously to open NAT mappings on each end.
  4. The same decider/non-decider ping protocol runs over the direct paths only (4 pings per path, tighter timeouts).
  5. The decider calls SelectBestTunnel() across all paths - relay and direct combined - and sends a new TunnelChoice through whichever wins.
  6. The non-decider adopts whichever tunnel the TunnelChoice arrives on. If no direct path wins or the round times out, both stay on the relay from Phase 1.

Add V3GameTunnelBridge
Add V3TunnelCommunicator
Add V3TunnelNegotiator
Add TunnelFailed event
Update supported tunnel versions
Fixup GameCreationWindow control visibility
Remove unnecessary tunnel parameter in V3PlayerInfo
Add NegotiationStatusPanel
Add tunnel negotiations to CnCNetGameLobby
Add V3 tunnel support to CnCNetGameLobby
Move to file-scoped namespaces
Fix missing texture on status panel
Fix issue with decider seeing OK instead of ping results
Fix an issue with automatic tunnel selection
Style fixups
Split TunnelChosenEventArgs into new file
Remove IDisposable on bridge
Remove unecessary code
Better updating of player ping indicators
@github-actions

github-actions Bot commented Oct 11, 2025

Copy link
Copy Markdown

Nightly build for this pull request:

  • artifacts.zip
    This comment is automatic and is meant to allow guests to get latest automatic builds without registering. It is updated on every successful build.

@CnCRAZER

Copy link
Copy Markdown
Contributor

Perhaps the ability to add a button within one of the clients ini files to toggle between V2 and V3 would be useful. Just a thought

Comment thread DXMainClient/Domain/Multiplayer/CnCNet/NegotiationDataManager.cs
Comment thread DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs Outdated
Comment thread DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs Outdated
Comment thread DXMainClient/Domain/Multiplayer/CnCNet/V3PlayerInfo.cs Outdated
Comment thread DXMainClient/Domain/Multiplayer/CnCNet/V3PlayerInfo.cs Outdated
Comment thread DXMainClient/Domain/Multiplayer/CnCNet/V3PlayerNegotiator.cs Outdated
Comment thread DXMainClient/Domain/Multiplayer/CnCNet/V3PlayerNegotiator.cs
Comment thread DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelCommunicator.cs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 41 out of 43 changed files in this pull request and generated 2 comments.

Comment on lines +550 to +562
int received = udpClient.Client.ReceiveFrom(receiveBuffer, ref remoteEndpoint);
var receivedTime = Stopwatch.GetTimestamp();

if (_endpointToTunnel.TryGetValue((IPEndPoint)remoteEndpoint, out var tunnel))
ProcessReceivedPacket(receiveBuffer.AsMemory(0, received), receivedTime, tunnel);
else if (_pendingStunQueries.TryRemove((IPEndPoint)remoteEndpoint, out var stunTcs))
{
var stunData = new byte[received];
Buffer.BlockCopy(receiveBuffer, 0, stunData, 0, received);
stunTcs.TrySetResult(stunData);
}
else if (!TryAutoLearnAndDispatch(receiveBuffer.AsMemory(0, received), receivedTime, (IPEndPoint)remoteEndpoint))
Logger.Log($"V3TunnelCommunicator: Received packet from unknown endpoint: {remoteEndpoint}");
Comment on lines +56 to +60
/// <summary>
/// Returns a string representation of this ping value.
/// Format: "50" for valid pings, or localized "Unknown" for invalid pings.
/// Note: Does not include the "ms" unit - callers should append the localized unit.
/// </summary>
@SadPencil

Copy link
Copy Markdown
Member

Stop pings wavering between unknown and real value

umm, what if the player is indeed offline suddenly? could be better if we can show Unknown ping after several attempts (or maybe leave it as another PR)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 41 out of 43 changed files in this pull request and generated 2 comments.

else
{
connection.QueueMessage(QueuedMessageType.SYSTEM_MESSAGE, 9, "PART " + ChannelName);
connection.QueueMessage(QueuedMessageType.INSTANT_MESSAGE, 0, "PART " + ChannelName);
Comment on lines 2484 to +2489
tunnelHandler.CurrentTunnel = tunnel;
AddNotice(string.Format("The game host has changed the tunnel server to: {0}".L10N("Client:Main:HostChangeTunnel"), tunnel.Name));

foreach (PlayerInfo pInfo in Players)
{
pInfo.Ping = -1;
UpdatePlayerPingIndicator(pInfo);
}

CopyPlayerDataToUI();
UpdatePing();

11EJDE11 added 14 commits July 3, 2026 08:43
- Fix negotiated auto-learned P2P paths breaking in-game
- Fix the game tunnel bridge thread dying permanently on transient socket errors
- Fix tunnel-choice split brain after a failed P2P upgrade
- Fix TunnelAck ambiguity
- Fix pair status masking
- Fix stale-packet corruption from overlapping renegotiation rounds
- Fix disposed negotiators raising spurious failure/complete events
- Fix a race where fire-and-forget ping tasks could touch the negotiator's disposed CancellationTokenSource (token now cached)
- Fix V2 regression: STARTV2 carries the real tunnel address again and joiners re-sync their tunnel from it
- Restrict TunnelFailed to two consecutive bad pings
- Add 15-second lobby keepalives on negotiated relay tunnels and P2P paths so NAT mappings and tunnel registrations survive
- Marshal negotiator events to the game thread, fixing the render-target NullReferenceException when negotiations completed
- Add a Renegotiate All debounce
- Add failure notices for every pair transition to Failed (deduplicated), so the host now hears about failures between other players too
- Add a renegotiate hint to the launch-blocked message and auto-open the negotiation status panel
- Add visible error notices when a client can't process the host's STARTV3 message
- Rebuild the panel list view as a scrolling multicolumn listbox with ping bars
- Report the full round-trip time between players instead of half
- Prevent lobby ping icons flickering
- Sort the list view worst-first
- Align the panel's ping colors/bars with the lobby ping icon tiers
- Center the "Tunnel Negotiation Status" panel title
- Hide the local player's ping icon in dynamic mode
- Show launch-blocked negotiation statuses as readable text
- Add a host-only suggestion when kicking one player would significantly reduce the worst connection ping
- Log the stale-failure-notification burst once per negotiator instead of once per tunnel
… up to date.

Add a pre-launch connectivity check
Fix duplicate L10N key
Snapshot remote endpoints before storing them as P2P routing keys
Reset player pings to unknown when host changes tunnel in V2 or V3 static mode.
Fix some nullable issues
Fix L10N strings
Fix game starting with null tunnel in v3 static mode when tunnel doesn't exist locally
Fix issue renegotiating with a player in game
Probe all users in the pre-launch connectivity verification
Reset tunnelErrorMode on lobby setup/clear
Set pings to unkown when TNLPNG -1 arrives
Change tunnel picker to address:port instead of just address
Make TunnelEventArgs.Tunnel nullable
Make ApplyV3StartEntry fail if missing V3PlayerInfo record(s)
Dedupe P2P candidates
Fix layout issue with the listbox in the negotiation status panel
@11EJDE11

11EJDE11 commented Jul 4, 2026

Copy link
Copy Markdown
Member Author

Stop pings wavering between unknown and real value

umm, what if the player is indeed offline suddenly? could be better if we can show Unknown ping after several attempts (or maybe leave it as another PR)

Not entirely sure what you mean - it's a ping indicator, not a disconnect indicator. The client was incorrectly setting them to unknown as well - like when a player joins the lobby it resets everyone's to unknown. They could also be unknown from the "Only ping official tunnels" setting. There were some errors with the change I made though - corrected now.

Had to add a keep alive thing to keep the NAT mapped, so a lot of new code in the latest commits unfortunately. Good news is I've tested just about every scenario I can think of and it's all working brilliantly.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 44 out of 46 changed files in this pull request and generated 2 comments.

Comment on lines 373 to 376
else
{
connection.QueueMessage(QueuedMessageType.SYSTEM_MESSAGE, 9, "PART " + ChannelName);
connection.QueueMessage(QueuedMessageType.INSTANT_MESSAGE, 0, "PART " + ChannelName);
}
Comment on lines +100 to +104
private void EvaluateTunnelHealth(CnCNetTunnel tunnel)
{
bool pingBad = tunnel.Ping.IsUnknown() || tunnel.Ping.Milliseconds > TUNNEL_FAILED_PING_AMOUNT;

if (!pingBad)
Comment on lines 202 to +204
return;

XNAListBoxItem lbItem = GetItem(2, filteredIndex);
XNAListBoxItem lbItem = GetItem(3, filteredIndex);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why this magic number has been changed?

Comment thread ClientCore/ProgramConstants.cs
Comment on lines +19 to +20


Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Extra \n


/// <summary>
/// This tracks what each player reports about their negotiation with each other player
/// // reportingPlayer -> targetPlayer -> ping

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
/// // reportingPlayer -> targetPlayer -> ping
/// reportingPlayer -> targetPlayer -> ping

Comment on lines +84 to +85


Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change

else
return $"Negotiations: {succeeded}/{total} complete";
}
} No newline at end of file

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
}
}

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.

P2P & V3 tunnel support

6 participants