Skip to content

Commit 3060bfd

Browse files
committed
Add IPv6 support to p2p mock framework
1 parent f4eb1c6 commit 3060bfd

4 files changed

Lines changed: 186 additions & 48 deletions

File tree

src/steamnetworkingsockets/clientlib/steamnetworkingsockets_mock.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ enum class TEST_mocknetwork_nat_type
3030

3131
struct TEST_mocknetwork_gateway_t
3232
{
33-
SteamNetworkingIPAddr m_ipv4_public; // NAT public IP (127.0.100.x); port must be zero
33+
SteamNetworkingIPAddr m_public_ip; // NAT public IP (127.0.100.x for IPv4, fd7f:0:100::x for IPv6); port must be zero
3434

3535
TEST_mocknetwork_nat_type m_natType = TEST_mocknetwork_nat_type::FullCone;
3636

@@ -71,9 +71,9 @@ struct TEST_mocknetwork_config_t
7171
// the corresponding entry here.
7272
std::vector<TEST_mocknetwork_gateway_t> m_vecGateways;
7373

74-
// Network interfaces on this host. Public interfaces (m_iGateway == -1) must use
75-
// addresses in the 127.0.100.x range. Private interfaces use 127.0.X.x (X != 100).
76-
// The same 127.0.X subnet can be shared across interfaces to model hosts on the same LAN.
74+
// Network interfaces on this host. Public interfaces (m_iGateway == -1) use
75+
// 127.0.100.x (IPv4) or fd7f:0:100::x (IPv6). Private interfaces use 127.0.X.x / fd7f:0:X::x
76+
// (X != 100). The same subnet can be shared across interfaces to model hosts on the same LAN.
7777
std::vector<TEST_mocknetwork_interface_t> m_vecInterfaces;
7878
};
7979

src/steamnetworkingsockets/clientlib/steamnetworkingsockets_socketthread.cpp

Lines changed: 109 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,22 @@ bool IsRouteToAddressProbablyLocal( netadr_t addr )
109109
{
110110

111111
#ifdef STEAMNETWORKINGSOCKETS_ENABLE_MOCK
112-
// In mock mode 127.0.100.x is the simulated public internet, not a local route,
113-
// even though it falls in the reserved 127.x.x.x range.
114-
if ( TEST_mocknetwork_active && addr.GetType() == k_EIPTypeV4
115-
&& ( addr.GetIPv4() & 0xFF00 ) == ( 100 << 8 ) )
116-
return false;
112+
// In mock mode the public-network ranges are simulated internet, not local routes,
113+
// even though they fall in reserved address space.
114+
// IPv4 public: 127.0.100.x (third octet == 100)
115+
// IPv6 public: fd7f:0:100::x (bytes [4:6] == 0x0100)
116+
if ( TEST_mocknetwork_active )
117+
{
118+
if ( addr.GetType() == k_EIPTypeV4 && ( addr.GetIPv4() & 0xFF00 ) == ( 100 << 8 ) )
119+
return false;
120+
if ( addr.GetType() == k_EIPTypeV6 )
121+
{
122+
const uint8 *b = addr.GetIPV6Bytes();
123+
if ( b[0] == 0xfd && b[1] == 0x7f && b[2] == 0x00 && b[3] == 0x00
124+
&& b[4] == 0x01 && b[5] == 0x00 )
125+
return false;
126+
}
127+
}
117128
#endif
118129

119130
// Assume that if we are able to send to any "reserved" route, that is is local.
@@ -1726,6 +1737,19 @@ static TEST_mocknetwork_config_t s_mockNetworkConfig;
17261737
// Third octet = 100 means public/gateway network (127.0.100.x)
17271738
const uint32 k_nMockPublicIPv4Net = (100 << 8);
17281739

1740+
// IPv6 mock addresses use fd7f:0:X::Y. Groups [4:6] (bytes 4-5) identify the network,
1741+
// mirroring the IPv4 third-octet scheme. 0x0100 ("100" in hex) = public network.
1742+
const uint16 k_nMockPublicIPv6NetID = 0x0100;
1743+
1744+
// Returns the network ID from bytes [4:6] of a mock IPv6 address (fd7f:0:X::Y),
1745+
// or 0 if the address is not in the mock IPv6 range.
1746+
static inline uint16 GetMockIPv6NetID( const uint8 *b )
1747+
{
1748+
if ( b[0] != 0xfd || b[1] != 0x7f || b[2] != 0x00 || b[3] != 0x00 )
1749+
return 0;
1750+
return ( uint16(b[4]) << 8 ) | b[5];
1751+
}
1752+
17291753
// Custom implementation of IRawUDPSocket that applies the appropriate routing
17301754
// rules from the mocked network environment
17311755
class CUDPSocketMock : public IRawUDPSocket
@@ -1782,6 +1806,29 @@ class CUDPSocketMock : public IRawUDPSocket
17821806
return SendDelayed( m_pSockLocal, nChunks, pChunks, adrTo, ecn, m_ifaceConfig.m_nSendLatencyMS );
17831807
}
17841808
}
1809+
else if ( adrTo.GetType() == k_EIPTypeV6 )
1810+
{
1811+
if ( m_boundAddr.IsIPv4() )
1812+
{
1813+
// Can't send to IPv6 address from an IPv4 socket
1814+
return false;
1815+
}
1816+
1817+
const uint16 net_remote = GetMockIPv6NetID( adrTo.GetIPV6Bytes() );
1818+
if ( net_remote == 0 )
1819+
return false; // Not a mock IPv6 address
1820+
1821+
// Check if this is a 'public IP'; if so, route via NAT
1822+
if ( net_remote == k_nMockPublicIPv6NetID )
1823+
return const_cast<CUDPSocketMock *>(this)->BCreateNATAndSend( nChunks, pChunks, adrTo, ecn );
1824+
1825+
const uint16 net_local = GetMockIPv6NetID( m_ifaceConfig.m_ip.m_ipv6 );
1826+
if ( net_local == net_remote )
1827+
{
1828+
// Same LAN — send directly
1829+
return SendDelayed( m_pSockLocal, nChunks, pChunks, adrTo, ecn, m_ifaceConfig.m_nSendLatencyMS );
1830+
}
1831+
}
17851832

17861833
// No route
17871834
return false;
@@ -1847,9 +1894,8 @@ class CUDPSocketMock : public IRawUDPSocket
18471894
CRawUDPSocketImpl *CreateExternalSock( CRecvPacketCallback callback )
18481895
{
18491896
Assert( m_pGatewayConfig );
1850-
Assert( m_pGatewayConfig->m_ipv4_public.IsIPv4() );
1851-
Assert( m_pGatewayConfig->m_ipv4_public.m_port == 0 );
1852-
SteamNetworkingIPAddr addrGateway = m_pGatewayConfig->m_ipv4_public;
1897+
Assert( m_pGatewayConfig->m_public_ip.m_port == 0 );
1898+
SteamNetworkingIPAddr addrGateway = m_pGatewayConfig->m_public_ip;
18531899
SteamNetworkingErrMsg errMsg;
18541900
CRawUDPSocketImpl *pSock = OpenRawUDPSocketInternal( callback, errMsg, &addrGateway, nullptr );
18551901
if ( !pSock )
@@ -2067,24 +2113,55 @@ IRawUDPSocket *OpenRawUDPSocket( CRecvPacketCallback callback, SteamNetworkingEr
20672113
{
20682114
// Find the matching interface config by address
20692115
const TEST_mocknetwork_interface_t *pIfaceConfig = nullptr;
2070-
if ( pAddrLocal && pAddrLocal->IsIPv4() && pAddrLocal->GetIPv4() != 0 )
2116+
if ( pAddrLocal )
20712117
{
2072-
uint32 nLookupIP = pAddrLocal->GetIPv4();
2073-
for ( const TEST_mocknetwork_interface_t &iface : s_mockNetworkConfig.m_vecInterfaces )
2118+
if ( pAddrLocal->IsIPv4() )
20742119
{
2075-
if ( iface.m_ip.IsIPv4() && iface.m_ip.GetIPv4() == nLookupIP )
2120+
uint32 nLookupIP = pAddrLocal->GetIPv4();
2121+
if ( nLookupIP != 0 )
20762122
{
2077-
pIfaceConfig = &iface;
2078-
break;
2123+
for ( const TEST_mocknetwork_interface_t &iface : s_mockNetworkConfig.m_vecInterfaces )
2124+
{
2125+
if ( iface.m_ip.IsIPv4() && iface.m_ip.GetIPv4() == nLookupIP )
2126+
{
2127+
pIfaceConfig = &iface;
2128+
break;
2129+
}
2130+
}
2131+
if ( !pIfaceConfig )
2132+
{
2133+
V_sprintf_safe( errMsg, "Mock: no interface configured for %s", SteamNetworkingIPAddrRender( *pAddrLocal ).c_str() );
2134+
return nullptr;
2135+
}
20792136
}
20802137
}
2081-
if ( !pIfaceConfig )
2138+
else
20822139
{
2083-
V_sprintf_safe( errMsg, "Mock: no interface configured for %s", SteamNetworkingIPAddrRender( *pAddrLocal ).c_str() );
2084-
return nullptr;
2140+
// IPv6: check if not the unspecified address (all zeros)
2141+
bool bHasAddr = false;
2142+
for ( int i = 0; i < 16; ++i )
2143+
{
2144+
if ( pAddrLocal->m_ipv6[i] != 0 ) { bHasAddr = true; break; }
2145+
}
2146+
if ( bHasAddr )
2147+
{
2148+
for ( const TEST_mocknetwork_interface_t &iface : s_mockNetworkConfig.m_vecInterfaces )
2149+
{
2150+
if ( !iface.m_ip.IsIPv4() && memcmp( iface.m_ip.m_ipv6, pAddrLocal->m_ipv6, 16 ) == 0 )
2151+
{
2152+
pIfaceConfig = &iface;
2153+
break;
2154+
}
2155+
}
2156+
if ( !pIfaceConfig )
2157+
{
2158+
V_sprintf_safe( errMsg, "Mock: no interface configured for %s", SteamNetworkingIPAddrRender( *pAddrLocal ).c_str() );
2159+
return nullptr;
2160+
}
2161+
}
20852162
}
20862163
}
2087-
else
2164+
if ( !pIfaceConfig )
20882165
{
20892166
Assert( !s_mockNetworkConfig.m_vecInterfaces.empty() );
20902167
pIfaceConfig = &s_mockNetworkConfig.m_vecInterfaces[0];
@@ -2129,7 +2206,7 @@ IRawUDPSocket *OpenRawUDPSocket( CRecvPacketCallback callback, SteamNetworkingEr
21292206
if ( pAddrLocal )
21302207
*pAddrLocal = pMock->m_boundAddr;
21312208
if ( pnAddressFamilies )
2132-
*pnAddressFamilies = k_nAddressFamily_IPv4;
2209+
*pnAddressFamilies = pIfaceConfig->m_ip.IsIPv4() ? k_nAddressFamily_IPv4 : k_nAddressFamily_IPv6;
21332210

21342211
return pMock;
21352212
}
@@ -3966,10 +4043,18 @@ bool GetLocalAddresses( CUtlVector<LocalAddress_t> *pAddrs )
39664043
{
39674044
LocalAddress_t &entry = *pAddrs->AddToTailGetPtr();
39684045
entry.m_addr = iface.m_ip;
3969-
// All mock private LANs are /24s identified by the third octet.
3970-
// The public "internet" range (third octet == 100) is not a local LAN.
3971-
bool bPublic = iface.m_ip.IsIPv4() && ( ( iface.m_ip.GetIPv4() & 0xFF00 ) == k_nMockPublicIPv4Net );
3972-
entry.m_nPrefixLen = bPublic ? 0 : 24;
4046+
if ( iface.m_ip.IsIPv4() )
4047+
{
4048+
// IPv4 mock private LANs are /24s; public range (third octet == 100) is not local.
4049+
bool bPublic = ( iface.m_ip.GetIPv4() & 0xFF00 ) == k_nMockPublicIPv4Net;
4050+
entry.m_nPrefixLen = bPublic ? 0 : 24;
4051+
}
4052+
else
4053+
{
4054+
// IPv6 mock private LANs are /112s; public range (net ID == 0x0100) is not local.
4055+
bool bPublic = GetMockIPv6NetID( iface.m_ip.m_ipv6 ) == k_nMockPublicIPv6NetID;
4056+
entry.m_nPrefixLen = bPublic ? 0 : 112;
4057+
}
39734058
}
39744059
}
39754060
return true;
@@ -4200,7 +4285,7 @@ void TEST_mocknetwork_init( const TEST_mocknetwork_config_t &config )
42004285
case TEST_mocknetwork_nat_type::Symmetric: pszNATType = "symmetric"; break;
42014286
}
42024287
SpewMsg( " Gateway[%d]: %s NAT=%s int=%dms ext=%dms\n",
4203-
i, SteamNetworkingIPAddrRender( gw.m_ipv4_public, false ).c_str(),
4288+
i, SteamNetworkingIPAddrRender( gw.m_public_ip, false ).c_str(),
42044289
pszNATType, gw.m_nInternalLatencyMS, gw.m_nExternalLatencyMS );
42054290
}
42064291
for ( const TEST_mocknetwork_interface_t &iface : config.m_vecInterfaces )

tests/test_p2p.cpp

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -281,9 +281,9 @@ int main( int argc, const char **argv )
281281
{
282282
const char *pszArg = GetArg();
283283
TEST_mocknetwork_gateway_t gw;
284-
if ( !gw.m_ipv4_public.ParseString( pszArg ) || !gw.m_ipv4_public.IsIPv4() )
285-
TEST_Fatal( "'%s' is not a valid IPv4 address for --mock-gateway", pszArg );
286-
gw.m_ipv4_public.m_port = 0;
284+
if ( !gw.m_public_ip.ParseString( pszArg ) )
285+
TEST_Fatal( "'%s' is not a valid IP address for --mock-gateway", pszArg );
286+
gw.m_public_ip.m_port = 0;
287287
mockConfig.m_vecGateways.push_back( gw );
288288
}
289289
else if ( !strcmp( pszSwitch, "--mock-nat" ) )
@@ -320,10 +320,17 @@ int main( int argc, const char **argv )
320320
{
321321
const char *pszArg = GetArg();
322322
TEST_mocknetwork_interface_t iface;
323-
if ( !iface.m_ip.ParseString( pszArg ) || !iface.m_ip.IsIPv4() )
324-
TEST_Fatal( "'%s' is not a valid IPv4 address for --mock-adapter", pszArg );
323+
if ( !iface.m_ip.ParseString( pszArg ) )
324+
TEST_Fatal( "'%s' is not a valid IP address for --mock-adapter", pszArg );
325325
iface.m_ip.m_port = 0;
326326
iface.m_iGateway = mockConfig.m_vecGateways.empty() ? -1 : (int)mockConfig.m_vecGateways.size() - 1;
327+
if ( iface.m_iGateway >= 0 )
328+
{
329+
const SteamNetworkingIPAddr &gwIP = mockConfig.m_vecGateways[ iface.m_iGateway ].m_public_ip;
330+
if ( iface.m_ip.IsIPv4() != gwIP.IsIPv4() )
331+
TEST_Fatal( "--mock-adapter '%s' address family does not match its gateway '%s'",
332+
pszArg, SteamNetworkingIPAddrRender( gwIP, false ).c_str() );
333+
}
327334
mockConfig.m_vecInterfaces.push_back( iface );
328335
}
329336
else if ( !strcmp( pszSwitch, "--mock-latency" ) )

tests/test_p2p.py

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,8 @@ def StartClientInThread( role, local, remote, extra_args=[] ):
169169

170170
return StartProcessInThread( local, cmdline, env );
171171

172-
# Mock network address constants
172+
# Mock network address constants — IPv4
173+
# Public range: 127.0.100.x Private LANs: 127.0.X.x (X != 100)
173174
_SRV_GW = '127.0.100.2' # server-side NAT gateway (public)
174175
_CLI_GW = '127.0.100.3' # client-side NAT gateway (public)
175176
_SRV_GW2 = '127.0.100.4' # second server gateway (public)
@@ -181,44 +182,79 @@ def StartClientInThread( role, local, remote, extra_args=[] ):
181182
_DEAD_INT = '127.0.9.2' # address used for disabled adapters
182183
_CLI_SAME_LAN = '127.0.1.3' # client on the same /24 private LAN as _SRV_INT
183184

185+
# Mock network address constants — IPv6
186+
# Mirrors the IPv4 layout: fd7f:0:100::x = public, fd7f:0:X::x = private LAN X
187+
_SRV_GW_V6 = 'fd7f:0:100::2' # server-side NAT gateway (public, IPv6)
188+
_CLI_GW_V6 = 'fd7f:0:100::3' # client-side NAT gateway (public, IPv6)
189+
_SRV_INT_V6 = 'fd7f:0:1::2' # server internal address behind NAT (IPv6)
190+
_CLI_INT_V6 = 'fd7f:0:2::2' # client internal address behind NAT (IPv6)
191+
184192
# All addresses that the mock network needs to be able to bind sockets to.
185193
_ALL_MOCK_ADDRS = [
186194
g_stun_ip,
187195
_SRV_GW, _CLI_GW, _SRV_GW2, _CLI_GW2,
188196
_SRV_INT, _CLI_INT, _SRV_INT2, _CLI_INT2, _DEAD_INT, _CLI_SAME_LAN,
197+
_SRV_GW_V6, _CLI_GW_V6, _SRV_INT_V6, _CLI_INT_V6,
189198
]
190199

191200
def _IsAddressBindable( addr ):
192201
import socket
202+
family = socket.AF_INET6 if ':' in addr else socket.AF_INET
193203
try:
194-
s = socket.socket( socket.AF_INET, socket.SOCK_DGRAM )
204+
s = socket.socket( family, socket.SOCK_DGRAM )
195205
s.bind( ( addr, 0 ) )
196206
s.close()
197207
return True
198208
except OSError:
199209
return False
200210

211+
def _AddLoopbackAddr( addr ):
212+
is_ipv6 = ':' in addr
213+
sys_name = platform.system()
214+
if sys_name == 'Darwin':
215+
if is_ipv6:
216+
print( "Running 'ifconfig lo0 inet6 %s'" % addr )
217+
subprocess.run( [ 'ifconfig', 'lo0', 'inet6', addr ], check=True )
218+
else:
219+
print( "Running 'ifconfig lo0 alias %s'" % addr )
220+
subprocess.run( [ 'ifconfig', 'lo0', 'alias', addr ], check=True )
221+
elif sys_name == 'Linux':
222+
if is_ipv6:
223+
print( "Running 'ip -6 addr add %s/112 dev lo'" % addr )
224+
subprocess.run( [ 'ip', '-6', 'addr', 'add', addr + '/112', 'dev', 'lo' ], check=True )
225+
# IPv4 on Linux: the entire 127/8 block is routable on lo, nothing to do.
226+
# Windows: TBD when needed
227+
228+
def _RemoveLoopbackAddr( addr ):
229+
is_ipv6 = ':' in addr
230+
sys_name = platform.system()
231+
if sys_name == 'Darwin':
232+
if is_ipv6:
233+
print( "Running 'ifconfig lo0 inet6 %s delete'" % addr )
234+
subprocess.run( [ 'ifconfig', 'lo0', 'inet6', addr, 'delete' ], check=False )
235+
else:
236+
print( "Running 'ifconfig lo0 -alias %s'" % addr )
237+
subprocess.run( [ 'ifconfig', 'lo0', '-alias', addr ], check=False )
238+
elif sys_name == 'Linux':
239+
if is_ipv6:
240+
print( "Running 'ip -6 addr del %s/112 dev lo'" % addr )
241+
subprocess.run( [ 'ip', '-6', 'addr', 'del', addr + '/112', 'dev', 'lo' ], check=False )
242+
# IPv4 on Linux: nothing was added, nothing to remove.
243+
# Windows: TBD when needed
244+
201245
def SetupMockIPs():
202246
"""Add loopback aliases for every mock address that is not already bindable."""
203-
if platform.system() != 'Darwin':
204-
print( "Nothing to do on this platform." )
205-
return
206247
for addr in _ALL_MOCK_ADDRS:
207-
print( "Running 'ifconfig lo0 alias %s'" % addr )
208-
subprocess.run( [ 'ifconfig', 'lo0', 'alias', addr ], check=True )
209-
print( "Added %d loopback alias(es)." % len( _ALL_MOCK_ADDRS ) )
248+
_AddLoopbackAddr( addr )
249+
print( "Setup complete." )
210250

211251
def CleanupMockIPs():
212252
"""Remove loopback aliases added by --setup-mock-ips."""
213-
if platform.system() != 'Darwin':
214-
print( "Nothing to do on this platform." )
215-
return
216253
for addr in _ALL_MOCK_ADDRS:
217-
if addr == '127.0.0.1':
254+
if addr in ( '127.0.0.1', '::1' ):
218255
continue
219-
print( "Running 'ifconfig lo0 -alias %s'" % addr )
220-
subprocess.run( [ 'ifconfig', 'lo0', '-alias', addr ], check=False )
221-
print( "Removed loopback aliases." )
256+
_RemoveLoopbackAddr( addr )
257+
print( "Cleanup complete." )
222258

223259
def CheckMockIPsBindable():
224260
"""Verify all mock addresses are bindable; exit with an error if any are not."""
@@ -321,6 +357,16 @@ def _slow_nat( internal, gateway, nat_type, latency_ms ):
321357
[ '--mock-adapter', _SRV_GW ] + _slow_nat( _SRV_INT2, _SRV_GW2, 'full-cone', 50 ),
322358
[ '--mock-adapter', _CLI_GW ] + _slow_nat( _CLI_INT2, _CLI_GW2, 'full-cone', 50 ),
323359
'udp', 1 ),
360+
361+
# IPv6 host candidates: both endpoints have a public IPv6 address, no NAT.
362+
# fd7f:0:100::x is the mock public IPv6 network (not classified as 'local').
363+
( 'IPv6 no-nat (both public)',
364+
[ '--mock-adapter', _SRV_GW_V6 ],
365+
[ '--mock-adapter', _CLI_GW_V6 ],
366+
'udp', 1 ),
367+
368+
# NOTE: IPv6 NAT cases require STUN to discover server-reflexive candidates.
369+
# The STUN server does not yet support IPv6, so those tests are deferred.
324370
]
325371

326372
def ClientServerTest( server_extra_args=[], client_extra_args=[], expected_route=None, ice_impl=1 ):

0 commit comments

Comments
 (0)