feat(network): Self-healing NAT state - #3010
Conversation
|
| Filename | Overview |
|---|---|
| Core/GameEngine/Source/GameNetwork/FirewallHelper.cpp | Moves firewall classification and restart state into the helper and fully resets active probe resources before redetection. |
| Core/GameEngine/Source/GameNetwork/NAT.cpp | Restarts detection after negotiation failures and refreshes local traversal behavior, but the refresh does not correct NAT state already distributed to peers. |
| Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp | Publishes the helper's best-known NAT behavior when entering a staging room. |
| Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLQuickMatchMenu.cpp | Supplies the helper's best-known NAT behavior to quick matchmaking. |
| GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp | Mirrors the Generals staging-room NAT publication changes for Zero Hour. |
| GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLQuickMatchMenu.cpp | Mirrors the Generals quick-match NAT publication changes for Zero Hour. |
Sequence Diagram
sequenceDiagram
participant Probe as FirewallHelper
participant Lobby as GameSpy Lobby
participant Peer as Remote Peer
participant NAT as NAT Traversal
Probe->>Probe: Start background detection
Lobby->>Probe: Request best-known behavior
Probe-->>Lobby: Classification or UNKNOWN
Lobby->>Peer: Distribute slot NAT values
NAT->>Probe: Refresh local behavior at traversal
NAT->>NAT: Use completed local classification
Peer->>Peer: Retain distributed slot classification
Prompt To Fix All With AI
### Issue 1
Core/GameEngine/Source/GameNetwork/NAT.cpp:600-610
**Remote peers retain UNKNOWN NAT**
When a player enters staging or quick match before firewall detection completes, the lobby distributes UNKNOWN as that player's NAT behavior. This fallback refreshes only the local traversal logic and does not update the slot state already distributed to remote peers, causing them to use the raw source port and time out behind a port-mangling NAT.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (12): Last reviewed commit: "review feedback" | Re-trigger Greptile
c5806f1 to
79e5c8e
Compare
### What this code did: The `ButtonFirewallRefresh` button in `OptionsMenu.cpp` and `FirewallNeedToRefresh` in `FirewallHelper.cpp` / `OptionPreferences.cpp`: 1. Saved/read `FirewallNeedToRefresh` and `LastFirewallIP` boolean flags to/from `Options.ini`. 2. Required players to manually click a "Refresh NAT" button in the Options GUI when changing network interfaces or encountering P2P negotiation failures. 3. Contaminated `GlobalData` (`TheWritableGlobalData->m_firewallBehavior`) with disk-persisted firewall state across process restarts. ### How the new code works: 1. **Automated In-Engine RAM Lifecycle**: NAT state classification (`m_behavior`) is managed 100% in memory within `FirewallHelperClass`. 2. **Transparent Background Probing**: Non-blocking STUN probing runs in the background during online lobby entry (`WOLWelcomeMenu`), caching the classified result in RAM for the remainder of the game session. 3. **Self-Healing Re-Detection**: If a peer connection times out (`NAT.cpp`) or the user selects a new IP address in Options (`OptionPreferences.cpp`), `TheFirewallHelper->flagNeedToRefresh(TRUE)` resets `m_behavior = FIREWALL_TYPE_UNKNOWN` in RAM to seamlessly trigger a fresh background probe. 4. **Clean GUI Deprecation**: Hides `OptionsMenu.wnd:ButtonFirewallRefresh` via `winHide(TRUE)` in C++ without breaking custom or legacy `.wnd` layout files. ### Background & Reason for Removal: - **Obsolete Manual Workaround**: Manual NAT refresh buttons are a legacy 2003 workaround; modern network stacks handle NAT re-detection automatically in-engine. - **Elimination of Disk I/O**: Completely removes `FirewallNeedToRefresh` and `LastFirewallIP` from `Options.ini`, preventing stale or corrupted flags from persisting across game crashes or process restarts. - **Architectural Decoupling**: Fully encapsulates STUN probing state inside `FirewallHelperClass` in memory, removing direct mutations to `TheWritableGlobalData->m_firewallBehavior`.
79e5c8e to
8113c09
Compare
placed nat detection earlier in the online Code some more cleanup
xezon
left a comment
There was a problem hiding this comment.
How can this change be tested? Did you test it? Does it work?
| checkSendDelay = TheWindowManager->winGetWindowFromId( nullptr, checkSendDelayID); | ||
| buttonFirewallRefreshID = TheNameKeyGenerator->nameToKey( "OptionsMenu.wnd:ButtonFirewallRefresh" ); | ||
| buttonFirewallRefresh = TheWindowManager->winGetWindowFromId( nullptr, buttonFirewallRefreshID); | ||
| // TheSuperHackers @info 25/07/2026 Refresh button has been hidden, we have migrated this to self-healing |
There was a problem hiding this comment.
Please avoid using "we", "i". Ideally comments are person-less.
|
|
||
| if (TheFirewallHelper != nullptr) | ||
| { | ||
| TheFirewallHelper->behaviorDetectionUpdate(); |
There was a problem hiding this comment.
Why does the WOL Login need a firewall helper update? Isn't the firewall helper needed for peer to peer connections?
There was a problem hiding this comment.
This is to move the firewall NAT detection as early as possible to heighten the chance it is known once you join a match.
| (*this)[key] = IP; | ||
|
|
||
| if (TheFirewallHelper != nullptr) | ||
| TheFirewallHelper->flagNeedToRefresh(TRUE); |
There was a problem hiding this comment.
It looks like this class is not intended to set state elsewhere in the program. Can this be implemented differently to keep the focus of this class for writing options?
|
|
||
| Bool isBehaviorDetectionComplete() {return(m_currentState == DETECTIONSTATE_DONE);} | ||
| FirewallBehaviorType getLastFirewallBehavior() {return(m_lastBehavior);} | ||
| Short getLastSourcePortAllocationDelta() {return((Short)m_lastSourcePortAllocationDelta);} |
|
|
||
| TheGameSpyConfig = GameSpyConfigInterface::create(configBuffer); | ||
|
|
||
| if (TheFirewallHelper == nullptr) |
There was a problem hiding this comment.
Can this condition ever be false at this point? If not, can it be replaced with an assert?
| return helper; | ||
| } | ||
|
|
||
| FirewallHelperClass::FirewallBehaviorType getBestKnownFirewallBehavior() |
There was a problem hiding this comment.
All it does is interact with FirewallHelperClass. Can this be member of FirewallHelperClass?
| return FirewallHelperClass::FIREWALL_TYPE_UNKNOWN; | ||
| } | ||
|
|
||
| Short getBestKnownSourcePortAllocationDelta() |
There was a problem hiding this comment.
All it does is interact with FirewallHelperClass. Can this be member of FirewallHelperClass?
| m_lastBehavior = (FirewallBehaviorType) ConfigINI.Get_Int("MultiPlayer", "FirewallSettings", FIREWALL_UNKNOWN); | ||
| m_lastSourcePortAllocationDelta = ConfigINI.Get_Int("MultiPlayer", "FirewallDelta", 1); | ||
| #endif //(0) | ||
| if (flag) { |
There was a problem hiding this comment.
This flag argument looks very useless. Can be removed. "Munkee" text above needs updating then.
Tested this with a Gamespy emulator and it does follow the intended changes. |
| FirewallHelperClass::FirewallBehaviorType NAT::getLocalNATBehavior() { | ||
| FirewallHelperClass::FirewallBehaviorType published = | ||
| m_slotList[m_connectionNodes[m_localNodeNumber].m_slotIndex]->getNATBehavior(); | ||
|
|
||
| FirewallHelperClass::FirewallBehaviorType detected = FirewallHelperClass::getBestKnownFirewallBehavior(); | ||
| if (detected != FirewallHelperClass::FIREWALL_TYPE_UNKNOWN) { | ||
| return detected; | ||
| } | ||
|
|
||
| return published; | ||
| } |
There was a problem hiding this comment.
Remote peers retain UNKNOWN NAT
When a player enters staging or quick match before firewall detection completes, the lobby distributes UNKNOWN as that player's NAT behavior. This fallback refreshes only the local traversal logic and does not update the slot state already distributed to remote peers, causing them to use the raw source port and time out behind a port-mangling NAT.
Knowledge Base Used: GameNetwork: Multiplayer Networking
Prompt To Fix With AI
This is a comment left during a code review.
Path: Core/GameEngine/Source/GameNetwork/NAT.cpp
Line: 600-610
Comment:
**Remote peers retain UNKNOWN NAT**
When a player enters staging or quick match before firewall detection completes, the lobby distributes UNKNOWN as that player's NAT behavior. This fallback refreshes only the local traversal logic and does not update the slot state already distributed to remote peers, causing them to use the raw source port and time out behind a port-mangling NAT.
**Knowledge Base Used:** [GameNetwork: Multiplayer Networking](https://app.greptile.com/thesuperhackers/-/custom-context/knowledge-base/thesuperhackers/generalsgamecode/-/docs/gamenetwork-multiplayer.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
What this code did:
The
ButtonFirewallRefreshbutton inOptionsMenu.cppandFirewallNeedToRefreshinFirewallHelper.cpp/OptionPreferences.cpp:FirewallNeedToRefreshandLastFirewallIPboolean flags to/fromOptions.ini.GlobalData(TheWritableGlobalData->m_firewallBehavior) with disk-persisted firewall state across process restarts.How the new code works:
m_behavior) is managed 100% in memory withinFirewallHelperClass.WOLWelcomeMenu), caching the classified result in RAM for the remainder of the game session.NAT.cpp) or the user selects a new IP address in Options (OptionPreferences.cpp),TheFirewallHelper->flagNeedToRefresh(TRUE)resetsm_behavior = FIREWALL_TYPE_UNKNOWNin RAM to seamlessly trigger a fresh background probe.OptionsMenu.wnd:ButtonFirewallRefreshviawinHide(TRUE)in C++ without breaking custom or legacy.wndlayout files.Background & Reason for Removal:
FirewallNeedToRefreshandLastFirewallIPfromOptions.ini, preventing stale or corrupted flags from persisting across game crashes or process restarts.FirewallHelperClassin memory, removing direct mutations toTheWritableGlobalData->m_firewallBehavior.