Add framework for setting a mock network environment

This commit is contained in:
Fletcher Dunn
2026-05-10 17:23:35 -07:00
parent e47e9ff3f1
commit 1ca37d421b
4 changed files with 466 additions and 14 deletions
-3
View File
@@ -387,9 +387,6 @@ if (BUILD_TESTS AND BUILD_STATIC_LIB AND ENABLE_ICE)
target_compile_definitions(GameNetworkingSockets_test_s INTERFACE STEAMNETWORKINGSOCKETS_STATIC_LINK)
set_clientlib_target_properties(GameNetworkingSockets_test_s)
target_compile_definitions(GameNetworkingSockets_test_s PUBLIC STEAMNETWORKINGSOCKETS_ENABLE_MOCK)
target_sources(GameNetworkingSockets_test_s PRIVATE
"steamnetworkingsockets/clientlib/steamnetworkingsockets_mock.cpp"
)
endif()
#
@@ -0,0 +1,57 @@
#pragma once
/////////////////////////////////////////////////////////////////////////////
//
// Interface for the mock network framework exposes to tests
//
/////////////////////////////////////////////////////////////////////////////
#include <vector>
#include <steam/steamnetworkingtypes.h>
#ifdef STEAMNETWORKINGSOCKETS_ENABLE_MOCK
struct TEST_mocknetwork_interface_t
{
SteamNetworkingIPAddr m_ip; // port not relevant and must be zero
int m_nSendLatencyMS = 0;
};
enum class TEST_mocknetwork_nat_type
{
// IPs are not translated at all, they are "public IPs"
None,
// Internal IP:port mapped to public IP:port. Any peer can send to the
// public port
FullCone,
// Inbound traffic to public port from IP X will only be forwarded if
// an inbound traffic was previously sent to X(but any port)
RestrictedCone,
// Inbound traffic to public port from IP X and port P will only be
// forwarded if an inbound traffic was previously sent to X *and* port P
PortRestrictedCone,
// Every internal IP:port <-> remote IP:port generates a new NAT port
Symmetric,
};
struct TEST_mocknetwork_config_t
{
std::vector<TEST_mocknetwork_interface_t> m_vecInterfaces;
SteamNetworkingIPAddr m_ipv4_gateway; // Port not relevant and must be zero
TEST_mocknetwork_nat_type m_natType = TEST_mocknetwork_nat_type::None;
};
void TEST_mocknetwork_init( const TEST_mocknetwork_config_t &info );
extern bool TEST_mocknetwork_active;
#else
constexpr bool TEST_mocknetwork_active = false;
#endif // STEAMNETWORKINGSOCKETS_ENABLE_MOCK
@@ -13,6 +13,7 @@
#include <atomic>
#include "steamnetworkingsockets_lowlevel.h"
#include "steamnetworkingsockets_mock.h"
#include <tier0/platform_sockets.h>
#include "../steamnetworkingsockets_internal.h"
#include "../steamnetworkingsockets_thinker.h"
@@ -107,6 +108,14 @@ static volatile bool s_bManualPollMode;
bool IsRouteToAddressProbablyLocal( netadr_t addr )
{
#ifdef STEAMNETWORKINGSOCKETS_ENABLE_MOCK
// In mock mode 127.0.100.x is the simulated public internet, not a local route,
// even though it falls in the reserved 127.x.x.x range.
if ( TEST_mocknetwork_active && addr.GetType() == k_EIPTypeV4
&& ( addr.GetIPv4() & 0xFF00 ) == ( 100 << 8 ) )
return false;
#endif
// Assume that if we are able to send to any "reserved" route, that is is local.
// Note that this will be true for VPNs, too!
if ( addr.IsReservedAdr() )
@@ -1699,8 +1708,356 @@ static CRawUDPSocketImpl *OpenRawUDPSocketInternal( CRecvPacketCallback callback
return pSock;
}
#if STEAMNETWORKINGSOCKETS_ENABLE_MOCK
#include <algorithm>
#include <memory>
/////////////////////////////////////////////////////////////////////////////
//
// Mock network
//
/////////////////////////////////////////////////////////////////////////////
// Config supplied by TEST_mocknetwork_init(); valid when TEST_mocknetwork_active == true
static TEST_mocknetwork_config_t s_mockNetworkConfig;
// Third octet identifies the network in 127.0.X.Y addressing.
// Third octet = 100 means public/gateway network (127.0.100.x)
const uint32 k_nMockPublicIPv4Net = (100 << 8);
// Custom implementation of IRawUDPSocket that applies the appropriate routing
// rules from the mocked network environment
class CUDPSocketMock : public IRawUDPSocket
{
public:
bool BindLocal( CRecvPacketCallback userCallback, SteamNetworkingErrMsg &errMsg, const SteamNetworkingIPAddr &addrLocal )
{
Assert( !m_pSockLocal );
m_userCallback = userCallback;
m_pSockLocal = OpenRawUDPSocketInternal( { StaticRecvInternalPacketThunk, this }, errMsg, &addrLocal, nullptr );
if ( !m_pSockLocal )
return false;
m_boundAddr = m_pSockLocal->m_boundAddr;
return true;
}
virtual void SetCallbackRecvPacket( CRecvPacketCallback callback ) override
{
m_userCallback = callback;
}
// Implements IRawUDPSocket
virtual bool BSendRawPacketGather( int nChunks, const iovec *pChunks, const netadr_t &adrTo, int ecn = -1 ) const override final
{
if ( !m_pSockLocal )
return false;
DbgAssert( m_boundAddr == m_pSockLocal->m_boundAddr );
if ( adrTo.GetType() == k_EIPTypeV4 )
{
if ( !m_boundAddr.IsIPv4() )
{
// Can't send to IPv4 address if we don't have an IPv4 socket
return false;
}
Assert( m_pSockLocal->m_nAddressFamilies & k_nAddressFamily_IPv4 );
const uint32 net_remote = adrTo.GetIPv4() & 0xFF00;
// Check if this is a 'public IP'; if so, route via NAT
if ( net_remote == k_nMockPublicIPv4Net )
return const_cast<CUDPSocketMock *>(this)->BCreateNATAndSend( nChunks, pChunks, adrTo, ecn );
const uint32 net_local = m_boundAddr.GetIPv4() & 0xFF00;
if ( net_local == net_remote )
{
// Same 'LAN', send directly
return m_pSockLocal->BSendRawPacketGather( nChunks, pChunks, adrTo, ecn );
}
}
// No route
return false;
}
virtual void Close() override
{
if ( m_pSockLocal )
{
m_pSockLocal->Close();
m_pSockLocal = nullptr;
}
}
protected:
// The internal (LAN-side) socket
CRawUDPSocketImpl *m_pSockLocal = nullptr;
// The user-provided callback, kept separately so we can wrap it
CRecvPacketCallback m_userCallback;
// Derived classes override this to apply NAT rules
virtual bool BCreateNATAndSend( int nChunks, const iovec *pChunks, const netadr_t &adrTo, int ecn ) = 0;
// Forward a packet received on an external (public) port to the user callback.
// Fixes up info.m_pSock to point to this mock socket so replies route correctly.
void DispatchExternalPacket( const RecvPktInfo_t &info )
{
if ( !m_userCallback.m_fnCallback )
return;
RecvPktInfo_t fixedInfo = info;
fixedInfo.m_pSock = this;
m_userCallback( fixedInfo );
}
// Allocate a port on the public gateway IP for a NAT entry
static CRawUDPSocketImpl *CreateExternalSock( CRecvPacketCallback callback )
{
Assert( s_mockNetworkConfig.m_ipv4_gateway.IsIPv4() );
Assert( s_mockNetworkConfig.m_ipv4_gateway.m_port == 0 );
SteamNetworkingIPAddr addrLocal = s_mockNetworkConfig.m_ipv4_gateway;
SteamNetworkingErrMsg errMsg;
CRawUDPSocketImpl *pSock = OpenRawUDPSocketInternal( callback, errMsg, &addrLocal, nullptr );
if ( !pSock )
SpewError( "Failed to create external socket for NAT. %s\n", errMsg );
return pSock;
}
private:
static void StaticRecvInternalPacketThunk( const RecvPktInfo_t &info, CUDPSocketMock *pSelf )
{
// Fix up m_pSock to point to this mock socket, not the internal one, so replies route correctly
RecvPktInfo_t fixedInfo = info;
fixedInfo.m_pSock = pSelf;
pSelf->m_userCallback( fixedInfo );
}
};
// No NAT -- local IP is already on the public network
class CUDPSocketMock_None final : public CUDPSocketMock
{
public:
virtual bool BCreateNATAndSend( int nChunks, const iovec *pChunks, const netadr_t &adrTo, int ecn ) override
{
// No NAT, send directly
return m_pSockLocal->BSendRawPacketGather( nChunks, pChunks, adrTo, ecn );
}
};
// FullCone, RestrictedCone, and PortRestrictedCone all map one internal port to one external
// port. They differ only in which inbound packets are allowed.
class CUDPSocketMock_OnePublicPort final : public CUDPSocketMock
{
public:
const TEST_mocknetwork_nat_type m_eNATType;
CUDPSocketMock_OnePublicPort( TEST_mocknetwork_nat_type eNATType ) : m_eNATType( eNATType )
{
Assert( m_eNATType == TEST_mocknetwork_nat_type::FullCone
|| m_eNATType == TEST_mocknetwork_nat_type::RestrictedCone
|| m_eNATType == TEST_mocknetwork_nat_type::PortRestrictedCone );
}
virtual void Close() override
{
if ( m_pSockExternal )
{
m_pSockExternal->Close();
m_pSockExternal = nullptr;
}
CUDPSocketMock::Close();
}
private:
CRawUDPSocketImpl *m_pSockExternal = nullptr;
std::vector<netadr_t> m_vecAdrsSentTo;
static void StaticRecvPacketThunk( const RecvPktInfo_t &info, CUDPSocketMock_OnePublicPort *self )
{
self->RecvPacketExternal( info );
}
virtual bool BCreateNATAndSend( int nChunks, const iovec *pChunks, const netadr_t &adrTo, int ecn ) override
{
// Create the external socket on first send
if ( !m_pSockExternal )
{
m_pSockExternal = CreateExternalSock( { StaticRecvPacketThunk, this } );
if ( !m_pSockExternal )
return false;
SpewVerbose( "[MOCK NAT] Created [internal %s] <-> [public %s] <-> (any)\n",
SteamNetworkingIPAddrRender( m_pSockLocal->m_boundAddr ).c_str(),
SteamNetworkingIPAddrRender( m_pSockExternal->m_boundAddr ).c_str() );
}
// Track destinations for restricted NAT types
if ( m_eNATType != TEST_mocknetwork_nat_type::FullCone )
{
netadr_t adrRecordSent = adrTo;
if ( m_eNATType == TEST_mocknetwork_nat_type::RestrictedCone )
adrRecordSent.SetPort(0);
if ( std::find( m_vecAdrsSentTo.begin(), m_vecAdrsSentTo.end(), adrRecordSent ) == m_vecAdrsSentTo.end() )
m_vecAdrsSentTo.push_back( adrRecordSent );
}
return m_pSockExternal->BSendRawPacketGather( nChunks, pChunks, adrTo, ecn );
}
void RecvPacketExternal( const RecvPktInfo_t &info )
{
// For restricted NAT types, drop packets from addresses we haven't sent to
if ( m_eNATType != TEST_mocknetwork_nat_type::FullCone )
{
netadr_t adrCheck = info.m_adrFrom;
if ( m_eNATType == TEST_mocknetwork_nat_type::RestrictedCone )
adrCheck.SetPort(0);
if ( std::find( m_vecAdrsSentTo.begin(), m_vecAdrsSentTo.end(), adrCheck ) == m_vecAdrsSentTo.end() )
{
SpewVerbose( "[MOCK NAT] Dropped [public %s] <- [remote %s]\n",
SteamNetworkingIPAddrRender( m_pSockExternal->m_boundAddr ).c_str(),
CUtlNetAdrRender( info.m_adrFrom ).String() );
return;
}
}
DispatchExternalPacket( info );
}
};
// Symmetric NAT: every unique remote destination gets a separate external port
class CUDPSocketMock_Symmetric final : public CUDPSocketMock
{
virtual void Close() override
{
m_vecNATEntries.clear(); // NATEntry_t destructor closes external sockets
CUDPSocketMock::Close();
}
private:
struct NATEntry_t
{
CUDPSocketMock_Symmetric *m_pOwner = nullptr;
netadr_t m_adrRemote;
CRawUDPSocketImpl *m_pSockExternal = nullptr;
~NATEntry_t()
{
if ( m_pSockExternal )
{
m_pSockExternal->Close();
m_pSockExternal = nullptr;
}
}
};
std::vector<std::unique_ptr<NATEntry_t>> m_vecNATEntries;
static void StaticRecvPacketThunk( const RecvPktInfo_t &info, NATEntry_t *pEntry )
{
if ( pEntry->m_adrRemote != info.m_adrFrom )
{
SpewVerbose( "[MOCK NAT] Dropped [public %s] <- [remote %s] (expected %s)\n",
SteamNetworkingIPAddrRender( pEntry->m_pSockExternal->m_boundAddr ).c_str(),
CUtlNetAdrRender( info.m_adrFrom ).String(),
CUtlNetAdrRender( pEntry->m_adrRemote ).String() );
return;
}
pEntry->m_pOwner->DispatchExternalPacket( info );
}
virtual bool BCreateNATAndSend( int nChunks, const iovec *pChunks, const netadr_t &adrTo, int ecn ) override
{
// Find existing NAT entry for this remote address
NATEntry_t *pNatEntry = nullptr;
for ( const auto &pEntry : m_vecNATEntries )
{
if ( pEntry->m_adrRemote == adrTo )
{
pNatEntry = pEntry.get();
break;
}
}
if ( !pNatEntry )
{
auto pNewEntry = std::make_unique<NATEntry_t>();
pNewEntry->m_pOwner = this;
pNewEntry->m_adrRemote = adrTo;
pNewEntry->m_pSockExternal = CreateExternalSock( { StaticRecvPacketThunk, pNewEntry.get() } );
if ( !pNewEntry->m_pSockExternal )
return false;
SpewVerbose( "[MOCK NAT] Created [internal %s] <-> [public %s] <-> [remote %s]\n",
SteamNetworkingIPAddrRender( m_pSockLocal->m_boundAddr ).c_str(),
SteamNetworkingIPAddrRender( pNewEntry->m_pSockExternal->m_boundAddr ).c_str(),
CUtlNetAdrRender( adrTo ).String() );
pNatEntry = pNewEntry.get();
m_vecNATEntries.push_back( std::move( pNewEntry ) );
}
return pNatEntry->m_pSockExternal->BSendRawPacketGather( nChunks, pChunks, adrTo, ecn );
}
};
#endif // STEAMNETWORKINGSOCKETS_ENABLE_MOCK
IRawUDPSocket *OpenRawUDPSocket( CRecvPacketCallback callback, SteamNetworkingErrMsg &errMsg, SteamNetworkingIPAddr *pAddrLocal, int *pnAddressFamilies )
{
#if STEAMNETWORKINGSOCKETS_ENABLE_MOCK
if ( TEST_mocknetwork_active )
{
// Determine the local address to bind to
SteamNetworkingIPAddr addrLocal;
if ( pAddrLocal && pAddrLocal->IsIPv4() && pAddrLocal->GetIPv4() != 0 )
addrLocal = *pAddrLocal;
else
{
Assert( !s_mockNetworkConfig.m_vecInterfaces.empty() );
addrLocal = s_mockNetworkConfig.m_vecInterfaces[0].m_ip;
}
// Create the appropriate mock socket type based on NAT config
CUDPSocketMock *pMock;
switch ( s_mockNetworkConfig.m_natType )
{
default:
case TEST_mocknetwork_nat_type::None:
pMock = new CUDPSocketMock_None();
break;
case TEST_mocknetwork_nat_type::FullCone:
case TEST_mocknetwork_nat_type::RestrictedCone:
case TEST_mocknetwork_nat_type::PortRestrictedCone:
pMock = new CUDPSocketMock_OnePublicPort( s_mockNetworkConfig.m_natType );
break;
case TEST_mocknetwork_nat_type::Symmetric:
pMock = new CUDPSocketMock_Symmetric();
break;
}
if ( !pMock->BindLocal( callback, errMsg, addrLocal ) )
{
delete pMock;
return nullptr;
}
if ( pAddrLocal )
*pAddrLocal = pMock->m_boundAddr;
if ( pnAddressFamilies )
*pnAddressFamilies = k_nAddressFamily_IPv4;
return pMock;
}
#endif
return OpenRawUDPSocketInternal( callback, errMsg, pAddrLocal, pnAddressFamilies );
}
@@ -3474,12 +3831,20 @@ inline bool GetLocalAddresses_IsReserved( const SteamNetworkingIPAddr &ipAddr )
return false;
}
#if IsWindows()
#pragma comment( lib, "iphlpapi.lib" )
bool GetLocalAddresses( CUtlVector< SteamNetworkingIPAddr >* pAddrs )
{
#if STEAMNETWORKINGSOCKETS_ENABLE_MOCK
if ( TEST_mocknetwork_active )
{
for ( const TEST_mocknetwork_interface_t &iface : s_mockNetworkConfig.m_vecInterfaces )
pAddrs->AddToTail( iface.m_ip );
return true;
}
#endif
#if IsWindows()
#pragma comment( lib, "iphlpapi.lib" )
if ( pAddrs == nullptr )
return false;
@@ -3552,10 +3917,9 @@ bool GetLocalAddresses( CUtlVector< SteamNetworkingIPAddr >* pAddrs )
free( pAddrInfo );
return true;
}
#elif IsPosix() && !IsPlaystation() && !IsAndroid() && !IsNintendoSwitch()
bool GetLocalAddresses( CUtlVector< SteamNetworkingIPAddr >* pAddrs )
{
ifaddrs *pMyAddrInfo = NULL;
int r = getifaddrs( &pMyAddrInfo );
if ( r != 0 )
@@ -3592,14 +3956,11 @@ bool GetLocalAddresses( CUtlVector< SteamNetworkingIPAddr >* pAddrs )
}
freeifaddrs( pMyAddrInfo );
return true;
}
#else
bool GetLocalAddresses( CUtlVector< SteamNetworkingIPAddr >* pAddrs )
{
AssertMsg( false, "Write me!" );
return false;
}
#endif
}
} // namespace SteamNetworkingSocketsLib
@@ -3659,3 +4020,39 @@ STEAMNETWORKINGSOCKETS_INTERFACE void SteamNetworkingSockets_SetServiceThreadIni
AssertMsg( !IsServiceThreadRunning(), "Too late!" );
s_fnServiceThreadInitCallback = callback;
}
/////////////////////////////////////////////////////////////////////////////
//
// Mock network — public API
//
/////////////////////////////////////////////////////////////////////////////
#if STEAMNETWORKINGSOCKETS_ENABLE_MOCK
bool TEST_mocknetwork_active = false;
void TEST_mocknetwork_init( const TEST_mocknetwork_config_t &config )
{
AssertMsg( !TEST_mocknetwork_active, "TEST_mocknetwork_init called twice" );
AssertMsg( !config.m_vecInterfaces.empty(), "Mock network must have at least one interface" );
s_mockNetworkConfig = config;
TEST_mocknetwork_active = true;
const char *pszNATType = "???";
switch ( config.m_natType )
{
case TEST_mocknetwork_nat_type::None: pszNATType = "None (public IP)"; break;
case TEST_mocknetwork_nat_type::FullCone: pszNATType = "Full Cone"; break;
case TEST_mocknetwork_nat_type::RestrictedCone: pszNATType = "Restricted Cone"; break;
case TEST_mocknetwork_nat_type::PortRestrictedCone: pszNATType = "Port Restricted Cone"; break;
case TEST_mocknetwork_nat_type::Symmetric: pszNATType = "Symmetric"; break;
}
SpewMsg( "Mock network active. NAT type: %s\n", pszNATType );
if ( config.m_ipv4_gateway.IsIPv4() )
SpewMsg( " Gateway: %s\n", SteamNetworkingIPAddrRender( config.m_ipv4_gateway, false ).c_str() );
for ( const TEST_mocknetwork_interface_t &iface : config.m_vecInterfaces )
SpewMsg( " Adapter: %s\n", SteamNetworkingIPAddrRender( iface.m_ip, false ).c_str() );
}
#endif // STEAMNETWORKINGSOCKETS_ENABLE_MOCK
+1
View File
@@ -12,6 +12,7 @@
#include <steam/steamnetworkingsockets.h>
#include <steam/isteamnetworkingutils.h>
#include "../examples/trivial_signaling_client.h"
#include "../src/steamnetworkingsockets/clientlib/steamnetworkingsockets_mock.h"
HSteamListenSocket g_hListenSock;
HSteamNetConnection g_hConnection;