Skip to content

feat(network): Self-healing NAT state#3010

Open
githubawn wants to merge 3 commits into
TheSuperHackers:mainfrom
githubawn:refactor/remove-refresh-nat
Open

feat(network): Self-healing NAT state#3010
githubawn wants to merge 3 commits into
TheSuperHackers:mainfrom
githubawn:refactor/remove-refresh-nat

Conversation

@githubawn

Copy link
Copy Markdown

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.

@githubawn githubawn changed the title feat(network): make NAT state self-healing feat(network): Self-healing NAT state Jul 23, 2026
@greptile-apps

greptile-apps Bot commented Jul 23, 2026

Copy link
Copy Markdown

Greptile Summary

The PR moves NAT classification from persisted global preferences into a session-lived FirewallHelper.

  • Automatically starts and advances background firewall detection.
  • Restarts detection after an IP change or failed NAT negotiation.
  • Retains completed detection results for matchmaking and removes the manual refresh workflow.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains in the fixes associated with the previous review threads.

Important Files Changed

Filename Overview
Core/GameEngine/Source/GameNetwork/FirewallHelper.cpp Centralizes the in-memory detection lifecycle, performs a complete socket/state reset on refresh, and preserves completed classifications.
Core/GameEngine/Source/GameNetwork/NAT.cpp Retains the shared helper across connection setup and requests a fresh classification after negotiation failure.
Core/GameEngine/Source/Common/OptionPreferences.cpp Invalidates the active NAT classification when the selected LAN or online address changes.
Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp Advances background detection without deleting its session-lived result.
GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp Mirrors the persistent background-detection lifecycle for Zero Hour.
Generals/Code/GameEngine/Source/GameNetwork/GameSpy.cpp Continues firewall probing from the GameSpy update loop while retaining the resulting helper state.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Enter online lobby] --> B{FirewallHelper exists?}
  B -- No --> C[Create helper and start detection]
  B -- Yes --> D[Reuse session helper]
  C --> E[Advance probe during updates]
  D --> E
  E --> F[Cache NAT behavior in memory]
  F --> G[Game setup and quick match consume behavior]
  H[Selected IP changes] --> I[Reset probe state and sockets]
  J[NAT negotiation fails] --> I
  I --> E
Loading

Reviews (5): Last reviewed commit: "fix(network): Preserve FirewallHelper li..." | Re-trigger Greptile

Comment thread Core/GameEngine/Source/GameNetwork/FirewallHelper.cpp
@githubawn
githubawn force-pushed the refactor/remove-refresh-nat branch from c5806f1 to 79e5c8e Compare July 23, 2026 22:14
### 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`.
@githubawn
githubawn force-pushed the refactor/remove-refresh-nat branch from 79e5c8e to 8113c09 Compare July 23, 2026 22:18
Comment thread Core/GameEngine/Source/GameNetwork/FirewallHelper.cpp
FirewallHelperClass();
virtual ~FirewallHelperClass();
Bool detectFirewall();
void detectFirewallBehavior(/*Bool &canRecord*/);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why is the argument commented out?

if ((*this)["IPAddress"].compareNoCase(IP) != 0)
{
(*this)["IPAddress"] = IP;
if (TheFirewallHelper != nullptr)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: Assignment of IP and resetting refresh flag of firewallhelper are two non-related commands. An empty line between the assignment and the if statement would have my preference for higher readability.

void OptionPreferences::setOnlineIPAddress(AsciiString IP)
{
(*this)["GameSpyIPAddress"] = IP;
if ((*this)["GameSpyIPAddress"].compareNoCase(IP) != 0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This code is a duplication of setLanIPAddress(). IMO it is better to have a private function with this logic and the two public functions calling the private function, i.e.:

void OptionPreferences::setLANIPAddress(AsciiString IP)
{
  setIpAddress(IP);
}

void OptionPreferences::setOnlineIPAddress(AsciiString IP)
{
  setIpAddress(IP);
}

void setIpAddress(AsciiString IP)
{
	if ((*this)["IPAddress"].compareNoCase(IP) != 0)
	{
		(*this)["IPAddress"] = IP;
		if (TheFirewallHelper != nullptr)
				TheFirewallHelper->flagNeedToRefresh(TRUE);
	}
}

void OptionPreferences::setLANIPAddress(AsciiString IP)
{
(*this)["IPAddress"] = IP;
if ((*this)["IPAddress"].compareNoCase(IP) != 0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Rather than putting all the logic within an if statement, you could tap out early

if ((*this)["IPAddress"].compareNoCase(IP) == 0)
  return;

(*this) ....

This increases readability and maintainability.


void OptionPreferences::setOnlineIPAddress(UnsignedInt IP)
{
AsciiString tmp;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

As this code is touched, maybe also change the naming of the parameter tmp as it is a poor name that doesn't reflect its purpose.

delete TheFirewallHelper;
TheFirewallHelper = nullptr;
}
FirewallHelperClass *helper = NEW FirewallHelperClass();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nit: add a whiteline after the closing bracket of an if-statement for higher readability

FirewallHelperClass * createFirewallHelper()
{
return NEW FirewallHelperClass();
if (TheFirewallHelper)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I am confused by the logic here. createFirewallHelper creates a new FirewallHelperClass and assigns it to a local variable, then returns that variable.

But first it deletes the instance (if any) of the global TheFirewallHelper parameter.

This reads like this function is not adhering to single responsibility.

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.

2 participants