#include "test_common.h" #include #include #include #include #include #include #include #include #include #include #include "../examples/trivial_signaling_client.h" #include "../src/steamnetworkingsockets/clientlib/steamnetworkingsockets_mock.h" #define DEFAULT_STUN_SERVER "stun.l.google.com:19302" HSteamListenSocket g_hListenSock; HSteamNetConnection g_hConnection; int g_nRepeat = 1; int g_nConnectionsDone = 0; enum ETestRole { k_ETestRole_Undefined, k_ETestRole_Server, k_ETestRole_Client, k_ETestRole_Symmetric, }; ETestRole g_eTestRole = k_ETestRole_Undefined; int g_nVirtualPortLocal = 0; // Used when listening, and when connecting int g_nVirtualPortRemote = 0; // Only used when connecting ESteamNetworkingSocketsDebugOutputType g_eTestP2PRendezvousLogLevel = k_ESteamNetworkingSocketsDebugOutputType_Verbose; void PrintUsage() { fprintf( stderr, "Usage: test_p2p [options]\n" "\n" " --identity-local Local identity string\n" " --identity-remote Remote identity string (not needed for --server)\n" " --signaling-server Trivial signaling server (default: localhost:10000)\n" " --server Act as server (listen for connection)\n" " --client Act as client (connect to server)\n" " --symmetric Symmetric connect mode\n" " --log Write log to file\n" " --spewlevel Console spew level: msg, verbose, debug\n" " --loglevel-p2prendezvous P2P rendezvous log level: msg, verbose, debug\n" " --stun-server STUN server address (default: " DEFAULT_STUN_SERVER ")\n" " --turn-server TURN relay server address\n" " --ice-implementation ICE implementation: 0=default, 1=native\n" " --repeat Repeat the connection test N times (default: 1)\n" #ifdef STEAMNETWORKINGSOCKETS_ENABLE_MOCK "\n" "Mock network options:\n" " --mock-adapter Add a mock network adapter (repeatable).\n" " Assigned to the most recently declared gateway,\n" " or public (no NAT) if no gateway declared yet.\n" " --mock-latency One-way send latency for the last --mock-adapter.\n" " --mock-disabled Mark the last --mock-adapter as down.\n" " --mock-gateway Declare a NAT gateway with this public IP.\n" " Subsequent --mock-adapters are assigned to it.\n" " --mock-nat NAT type for last gateway: full-cone (default),\n" " restricted-cone, port-restricted-cone, symmetric\n" " --mock-internal-latency VPN-tunnel latency for last gateway (host->exit).\n" " --mock-external-latency WAN latency for last gateway (exit->internet).\n" #endif ); } static ESteamNetworkingSocketsDebugOutputType ParseLogLevelValue( const char *pszArg, const char *pszSwitchName ) { if ( !strcmp( pszArg, "msg" ) ) return k_ESteamNetworkingSocketsDebugOutputType_Msg; if ( !strcmp( pszArg, "verbose" ) ) return k_ESteamNetworkingSocketsDebugOutputType_Verbose; if ( !strcmp( pszArg, "debug" ) ) return k_ESteamNetworkingSocketsDebugOutputType_Debug; TEST_Fatal( "Invalid %s '%s'. Expected one of: msg, verbose, debug", pszSwitchName, pszArg ); return k_ESteamNetworkingSocketsDebugOutputType_Msg; } void Quit( int rc ) { if ( rc == 0 ) { // OK, we cannot just exit the process, because we need to give // the connection time to actually send the last message and clean up. // If this were a TCP connection, we could just bail, because the OS // would handle it. But this is an application protocol over UDP. // So give a little bit of time for good cleanup. (Also note that // we really ought to continue pumping the signaling service, but // in this exampple we'll assume that no more signals need to be // exchanged, since we've gotten this far.) If we just terminated // the program here, our peer could very likely timeout. (Although // it's possible that the cleanup packets have already been placed // on the wire, and if they don't drop, things will get cleaned up // properly.) TEST_Printf( "Waiting for any last cleanup packets.\n" ); std::this_thread::sleep_for( std::chrono::milliseconds( 1000 ) ); } TEST_Kill(); exit(rc); } // Print a parseable route summary for the active connection. // Output format: "TEST ROUTE: addr= type=" void PrintRouteInfo() { SteamNetConnectionInfo_t info; if ( !SteamNetworkingSockets()->GetConnectionInfo( g_hConnection, &info ) ) return; const char *pszType; if ( info.m_nFlags & k_nSteamNetworkConnectionInfoFlags_Relayed ) pszType = "relay"; else if ( info.m_nFlags & k_nSteamNetworkConnectionInfoFlags_Fast ) pszType = "local"; else pszType = "udp"; char szAddr[64]; info.m_addrRemote.ToString( szAddr, sizeof(szAddr), true ); TEST_Printf( "TEST ROUTE: addr=%s type=%s\n", szAddr, pszType ); } // Send a simple string message to out peer, using reliable transport. void SendMessageToPeer( const char *pszMsg ) { TEST_Printf( "Sending msg '%s'\n", pszMsg ); EResult r = SteamNetworkingSockets()->SendMessageToConnection( g_hConnection, pszMsg, (int)strlen(pszMsg)+1, k_nSteamNetworkingSend_Reliable, nullptr ); assert( r == k_EResultOK ); } // Called when a connection undergoes a state transition. void OnSteamNetConnectionStatusChanged( SteamNetConnectionStatusChangedCallback_t *pInfo ) { // What's the state of the connection? switch ( pInfo->m_info.m_eState ) { case k_ESteamNetworkingConnectionState_ClosedByPeer: case k_ESteamNetworkingConnectionState_ProblemDetectedLocally: TEST_Printf( "[%s] %s, reason %d: %s\n", pInfo->m_info.m_szConnectionDescription, ( pInfo->m_info.m_eState == k_ESteamNetworkingConnectionState_ClosedByPeer ? "closed by peer" : "problem detected locally" ), pInfo->m_info.m_eEndReason, pInfo->m_info.m_szEndDebug ); // Close our end SteamNetworkingSockets()->CloseConnection( pInfo->m_hConn, 0, nullptr, false ); if ( g_hConnection == pInfo->m_hConn ) { g_hConnection = k_HSteamNetConnection_Invalid; bool bError = ( pInfo->m_info.m_eState == k_ESteamNetworkingConnectionState_ProblemDetectedLocally ) || ( pInfo->m_info.m_eEndReason != k_ESteamNetConnectionEnd_App_Generic ); if ( bError ) Quit( 1 ); // Clean close — the main loop will start the next iteration or exit. ++g_nConnectionsDone; } else { // Stale handle from a previous iteration being cleaned up — ignore. } break; case k_ESteamNetworkingConnectionState_None: // Notification that a connection was destroyed. (By us, presumably.) // We don't need this, so ignore it. break; case k_ESteamNetworkingConnectionState_Connecting: // Is this a connection we initiated, or one that we are receiving? if ( g_hListenSock != k_HSteamListenSocket_Invalid && pInfo->m_info.m_hListenSocket == g_hListenSock ) { // Somebody's knocking. With --repeat, the new connection request (signaled // via TCP) can race ahead of the close notification for the previous connection // (sent via UDP). If the old handle is still around, clean it up now. if ( g_hConnection != k_HSteamNetConnection_Invalid ) { TEST_Printf( "Got new connection request while previous connection was still active. Closing previous connection\n" ); SteamNetworkingSockets()->CloseConnection( g_hConnection, 0, nullptr, false ); g_hConnection = k_HSteamNetConnection_Invalid; ++g_nConnectionsDone; } TEST_Printf( "[%s] Accepting\n", pInfo->m_info.m_szConnectionDescription ); g_hConnection = pInfo->m_hConn; SteamNetworkingSockets()->AcceptConnection( pInfo->m_hConn ); } else { // Note that we will get notification when our own connection that // we initiate enters this state. assert( g_hConnection == pInfo->m_hConn ); TEST_Printf( "[%s] Entered connecting state\n", pInfo->m_info.m_szConnectionDescription ); } break; case k_ESteamNetworkingConnectionState_FindingRoute: // P2P connections will spend a brief time here where they swap addresses // and try to find a route. TEST_Printf( "[%s] finding route\n", pInfo->m_info.m_szConnectionDescription ); break; case k_ESteamNetworkingConnectionState_Connected: // We got fully connected assert( pInfo->m_hConn == g_hConnection ); // We don't initiate or accept any other connections, so this should be out own connection TEST_Printf( "[%s] connected\n", pInfo->m_info.m_szConnectionDescription ); break; default: assert( false ); break; } } #ifdef _MSC_VER #pragma warning( disable: 4702 ) /* unreachable code */ #endif int main( int argc, const char **argv ) { SteamNetworkingIdentity identityLocal; identityLocal.Clear(); SteamNetworkingIdentity identityRemote; identityRemote.Clear(); const char *pszTrivialSignalingService = "localhost:10000"; const char *pszSTUNServer = DEFAULT_STUN_SERVER; const char *pszTURNServer = nullptr; int g_nICEImplementation = -1; // -1 = not set, use library default #ifdef STEAMNETWORKINGSOCKETS_ENABLE_MOCK TEST_mocknetwork_config_t mockConfig; #endif // Parse the command line for ( int idxArg = 1 ; idxArg < argc ; ++idxArg ) { const char *pszSwitch = argv[idxArg]; auto GetArg = [&]() -> const char * { if ( idxArg + 1 >= argc ) TEST_Fatal( "Expected argument after %s", pszSwitch ); return argv[++idxArg]; }; auto ParseIdentity = [&]( SteamNetworkingIdentity &x ) { const char *pszArg = GetArg(); if ( !x.ParseString( pszArg ) ) TEST_Fatal( "'%s' is not a valid identity string", pszArg ); }; if ( !strcmp( pszSwitch, "--identity-local" ) ) ParseIdentity( identityLocal ); else if ( !strcmp( pszSwitch, "--identity-remote" ) ) ParseIdentity( identityRemote ); else if ( !strcmp( pszSwitch, "--signaling-server" ) ) pszTrivialSignalingService = GetArg(); else if ( !strcmp( pszSwitch, "--stun-server" ) ) pszSTUNServer = GetArg(); else if ( !strcmp( pszSwitch, "--turn-server" ) ) pszTURNServer = GetArg(); else if ( !strcmp( pszSwitch, "--ice-implementation" ) ) g_nICEImplementation = atoi( GetArg() ); else if ( !strcmp( pszSwitch, "--repeat" ) ) g_nRepeat = atoi( GetArg() ); else if ( !strcmp( pszSwitch, "--client" ) ) g_eTestRole = k_ETestRole_Client; else if ( !strcmp( pszSwitch, "--server" ) ) g_eTestRole = k_ETestRole_Server; else if ( !strcmp( pszSwitch, "--symmetric" ) ) g_eTestRole = k_ETestRole_Symmetric; else if ( !strcmp( pszSwitch, "--log" ) ) { const char *pszArg = GetArg(); TEST_InitLog( pszArg ); } else if ( !strcmp( pszSwitch, "--spewlevel" ) || !strncmp( pszSwitch, "--spewlevel=", 12 ) ) { const char *pszArg = pszSwitch[11] == '=' ? pszSwitch + 12 : GetArg(); ESteamNetworkingSocketsDebugOutputType eLogLevel = ParseLogLevelValue( pszArg, "--spewlevel" ); TEST_SetStdoutDetailLevel( eLogLevel ); } else if ( !strcmp( pszSwitch, "--loglevel-p2prendezvous" ) || !strncmp( pszSwitch, "--loglevel-p2prendezvous=", 25 ) ) { const char *pszArg = pszSwitch[24] == '=' ? pszSwitch + 25 : GetArg(); g_eTestP2PRendezvousLogLevel = ParseLogLevelValue( pszArg, "--loglevel-p2prendezvous" ); } #ifdef STEAMNETWORKINGSOCKETS_ENABLE_MOCK else if ( !strcmp( pszSwitch, "--mock-gateway" ) ) { const char *pszArg = GetArg(); TEST_mocknetwork_gateway_t gw; if ( !gw.m_public_ip.ParseString( pszArg ) ) TEST_Fatal( "'%s' is not a valid IP address for --mock-gateway", pszArg ); gw.m_public_ip.m_port = 0; mockConfig.m_vecGateways.push_back( gw ); } else if ( !strcmp( pszSwitch, "--mock-nat" ) ) { if ( mockConfig.m_vecGateways.empty() ) TEST_Fatal( "--mock-nat must follow --mock-gateway" ); const char *pszArg = GetArg(); TEST_mocknetwork_nat_type eNATType; if ( !strcmp( pszArg, "full-cone" ) ) eNATType = TEST_mocknetwork_nat_type::FullCone; else if ( !strcmp( pszArg, "restricted-cone" ) ) eNATType = TEST_mocknetwork_nat_type::RestrictedCone; else if ( !strcmp( pszArg, "port-restricted-cone" ) ) eNATType = TEST_mocknetwork_nat_type::PortRestrictedCone; else if ( !strcmp( pszArg, "symmetric" ) ) eNATType = TEST_mocknetwork_nat_type::Symmetric; else TEST_Fatal( "Invalid --mock-nat '%s'. Expected: full-cone, restricted-cone, port-restricted-cone, symmetric", pszArg ); mockConfig.m_vecGateways.back().m_natType = eNATType; } else if ( !strcmp( pszSwitch, "--mock-internal-latency" ) ) { if ( mockConfig.m_vecGateways.empty() ) TEST_Fatal( "--mock-internal-latency must follow --mock-gateway" ); mockConfig.m_vecGateways.back().m_nInternalLatencyMS = atoi( GetArg() ); } else if ( !strcmp( pszSwitch, "--mock-external-latency" ) ) { if ( mockConfig.m_vecGateways.empty() ) TEST_Fatal( "--mock-external-latency must follow --mock-gateway" ); mockConfig.m_vecGateways.back().m_nExternalLatencyMS = atoi( GetArg() ); } else if ( !strcmp( pszSwitch, "--mock-adapter" ) ) { const char *pszArg = GetArg(); TEST_mocknetwork_interface_t iface; if ( !iface.m_ip.ParseString( pszArg ) ) TEST_Fatal( "'%s' is not a valid IP address for --mock-adapter", pszArg ); iface.m_ip.m_port = 0; iface.m_iGateway = mockConfig.m_vecGateways.empty() ? -1 : (int)mockConfig.m_vecGateways.size() - 1; if ( iface.m_iGateway >= 0 ) { const SteamNetworkingIPAddr &gwIP = mockConfig.m_vecGateways[ iface.m_iGateway ].m_public_ip; if ( iface.m_ip.IsIPv4() != gwIP.IsIPv4() ) TEST_Fatal( "--mock-adapter '%s' address family does not match its gateway '%s'", pszArg, SteamNetworkingIPAddrRender( gwIP, false ).c_str() ); } mockConfig.m_vecInterfaces.push_back( iface ); } else if ( !strcmp( pszSwitch, "--mock-latency" ) ) { if ( mockConfig.m_vecInterfaces.empty() ) TEST_Fatal( "--mock-latency must follow --mock-adapter" ); mockConfig.m_vecInterfaces.back().m_nSendLatencyMS = atoi( GetArg() ); } else if ( !strcmp( pszSwitch, "--mock-disabled" ) ) { if ( mockConfig.m_vecInterfaces.empty() ) TEST_Fatal( "--mock-disabled must follow --mock-adapter" ); mockConfig.m_vecInterfaces.back().m_bEnabled = false; } #endif else if ( !strcmp( pszSwitch, "--help" ) || !strcmp( pszSwitch, "-h" ) ) { PrintUsage(); exit(0); } else TEST_Fatal( "Unexpected command line argument '%s'", pszSwitch ); } if ( g_eTestRole == k_ETestRole_Undefined ) TEST_Fatal( "Must specify test role (--server, --client, or --symmetric" ); if ( identityLocal.IsInvalid() ) TEST_Fatal( "Must specify local identity using --identity-local" ); if ( identityRemote.IsInvalid() && g_eTestRole != k_ETestRole_Server ) TEST_Fatal( "Must specify remote identity using --identity-remote" ); #ifdef STEAMNETWORKINGSOCKETS_ENABLE_MOCK if ( !mockConfig.m_vecInterfaces.empty() ) TEST_mocknetwork_init( mockConfig ); #endif // Initialize library, with the desired local identity TEST_Init( &identityLocal ); SteamNetworkingUtils()->SetGlobalConfigValueString( k_ESteamNetworkingConfig_P2P_STUN_ServerList, pszSTUNServer ); if ( pszTURNServer != nullptr ) SteamNetworkingUtils()->SetGlobalConfigValueString( k_ESteamNetworkingConfig_P2P_TURN_ServerList, pszTURNServer ); if ( g_nICEImplementation >= 0 ) SteamNetworkingUtils()->SetGlobalConfigValueInt32( k_ESteamNetworkingConfig_P2P_Transport_ICE_Implementation, g_nICEImplementation ); // Allow sharing of any kind of ICE address. // We don't have any method of relaying (TURN) in this example, so we are essentially // forced to disclose our public address if we want to pierce NAT. But if we // had relay fallback, or if we only wanted to connect on the LAN, we could restrict // to only sharing private addresses. SteamNetworkingUtils()->SetGlobalConfigValueInt32(k_ESteamNetworkingConfig_P2P_Transport_ICE_Enable, k_nSteamNetworkingConfig_P2P_Transport_ICE_Enable_All ); // Create the signaling service SteamNetworkingErrMsg errMsg; ITrivialSignalingClient *pSignaling = CreateTrivialSignalingClient( pszTrivialSignalingService, SteamNetworkingSockets(), errMsg ); if ( pSignaling == nullptr ) TEST_Fatal( "Failed to initializing signaling client. %s", errMsg ); SteamNetworkingUtils()->SetGlobalCallback_SteamNetConnectionStatusChanged( OnSteamNetConnectionStatusChanged ); // Comment this line in for more detailed spew about signals, route finding, ICE, etc SteamNetworkingUtils()->SetGlobalConfigValueInt32( k_ESteamNetworkingConfig_LogLevel_P2PRendezvous, g_eTestP2PRendezvousLogLevel ); // Create listen socket to receive connections on, unless we are the client if ( g_eTestRole == k_ETestRole_Server ) { TEST_Printf( "Creating listen socket, local virtual port %d\n", g_nVirtualPortLocal ); g_hListenSock = SteamNetworkingSockets()->CreateListenSocketP2P( g_nVirtualPortLocal, 0, nullptr ); assert( g_hListenSock != k_HSteamListenSocket_Invalid ); } else if ( g_eTestRole == k_ETestRole_Symmetric ) { // Currently you must create a listen socket to use symmetric mode, // even if you know that you will always create connections "both ways". // In the future we might try to remove this requirement. It is a bit // less efficient, since it always triggered the race condition case // where both sides create their own connections, and then one side // decides to their theirs away. If we have a listen socket, then // it can be the case that one peer will receive the incoming connection // from the other peer, and since he has a listen socket, can save // the connection, and then implicitly accept it when he initiates his // own connection. Without the listen socket, if an incoming connection // request arrives before we have started connecting out, then we are forced // to ignore it, as the app has given no indication that it desires to // receive inbound connections at all. TEST_Printf( "Creating listen socket in symmetric mode, local virtual port %d\n", g_nVirtualPortLocal ); SteamNetworkingConfigValue_t opt; opt.SetInt32( k_ESteamNetworkingConfig_SymmetricConnect, 1 ); // << Note we set symmetric mode on the listen socket g_hListenSock = SteamNetworkingSockets()->CreateListenSocketP2P( g_nVirtualPortLocal, 1, &opt ); assert( g_hListenSock != k_HSteamListenSocket_Invalid ); } // Lambda to initiate a new outbound connection and send the first message. auto ConnectToPeer = [&]() { std::vector< SteamNetworkingConfigValue_t > vecOpts; // If we want the local and virtual port to differ, we must set // an option. This is a pretty rare use case, and usually not needed. // The local virtual port is only usually relevant for symmetric // connections, and then, it almost always matches. Here we are // just showing in this example code how you could handle this if you // needed them to differ. if ( g_nVirtualPortRemote != g_nVirtualPortLocal ) { SteamNetworkingConfigValue_t opt; opt.SetInt32( k_ESteamNetworkingConfig_LocalVirtualPort, g_nVirtualPortLocal ); vecOpts.push_back( opt ); } // Symmetric mode? Noce that since we created a listen socket on this local // virtual port and tagged it for symmetric connect mode, any connections // we create that use the same local virtual port will automatically inherit // this setting. However, this is really not recommended. It is best to be // explicit. if ( g_eTestRole == k_ETestRole_Symmetric ) { SteamNetworkingConfigValue_t opt; opt.SetInt32( k_ESteamNetworkingConfig_SymmetricConnect, 1 ); vecOpts.push_back( opt ); TEST_Printf( "Connecting to '%s' in symmetric mode, virtual port %d, from local virtual port %d.\n", SteamNetworkingIdentityRender( identityRemote ).c_str(), g_nVirtualPortRemote, g_nVirtualPortLocal ); } else { TEST_Printf( "Connecting to '%s', virtual port %d, from local virtual port %d.\n", SteamNetworkingIdentityRender( identityRemote ).c_str(), g_nVirtualPortRemote, g_nVirtualPortLocal ); } // Connect using the "custom signaling" path. Note that when // you are using this path, the identity is actually optional, // since we don't need it. (Your signaling object already // knows how to talk to the peer) and then the peer identity // will be confirmed via rendezvous. ISteamNetworkingConnectionSignaling *pConnSignaling = pSignaling->CreateSignalingForConnection( identityRemote, errMsg ); assert( pConnSignaling ); g_hConnection = SteamNetworkingSockets()->ConnectP2PCustomSignaling( pConnSignaling, &identityRemote, g_nVirtualPortRemote, (int)vecOpts.size(), vecOpts.data() ); assert( g_hConnection != k_HSteamNetConnection_Invalid ); // Go ahead and send a message now. The message will be queued until route finding // completes. SendMessageToPeer( "Greetings!" ); }; // Begin connecting to peer, unless we are the server if ( g_eTestRole != k_ETestRole_Server ) ConnectToPeer(); // Main test loop for (;;) { // Check for incoming signals, and dispatch them pSignaling->Poll(); // Check callbacks TEST_PumpCallbacks(); // If we have a connection, then poll it for messages if ( g_hConnection != k_HSteamNetConnection_Invalid ) { SteamNetworkingMessage_t *pMessage; int r = SteamNetworkingSockets()->ReceiveMessagesOnConnection( g_hConnection, &pMessage, 1 ); assert( r == 0 || r == 1 ); // <0 indicates an error if ( r == 1 ) { // In this example code we will assume all messages are '\0'-terminated strings. // Obviously, this is not secure. TEST_Printf( "Received message '%s'\n", pMessage->GetData() ); // Free message struct and buffer. pMessage->Release(); PrintRouteInfo(); // If we're the client, go ahead and shut down. In this example we just // wanted to establish a connection and exchange a message, and we've done that. // Note that we use "linger" functionality. This flushes out any remaining // messages that we have queued. Essentially to us, the connection is closed, // but on thew wire, we will not actually close it until all reliable messages // have been confirmed as received by the client. (Or the connection is closed // by the peer or drops.) If we are the "client" role, then we know that no such // messages are in the pipeline in this test. But in symmetric mode, it is // possible that we need to flush out our message that we sent. if ( g_eTestRole != k_ETestRole_Server ) { // Close this connection. Use linger on the final iteration so the // server has time to receive our close before we exit. TEST_Printf( "Closing connection\n" ); SteamNetworkingSockets()->CloseConnection( g_hConnection, 0, "Test completed OK", true ); g_hConnection = k_HSteamNetConnection_Invalid; ++g_nConnectionsDone; if ( g_nConnectionsDone >= g_nRepeat ) break; TEST_Printf( "Starting next iteration\n" ); ConnectToPeer(); } else { // We're the server. Send a reply. SendMessageToPeer( "I got your message" ); } } } // Server exits once it has handled the expected number of connections. if ( g_eTestRole == k_ETestRole_Server && g_nConnectionsDone >= g_nRepeat ) break; } TEST_Printf( "Shutting down\n" ); Quit(0); return 0; }