diff --git a/include/steam/steamnetworkingtypes.h b/include/steam/steamnetworkingtypes.h index 5b612802..40dd449d 100644 --- a/include/steam/steamnetworkingtypes.h +++ b/include/steam/steamnetworkingtypes.h @@ -1197,6 +1197,21 @@ enum ESteamNetworkingConfigValue /// disables Nagle! k_ESteamNetworkingConfig_RecvMaxSegmentsPerPacket = 50, + /// [connection int32] Max packet rate allowed from a peer, in packets per second. + /// If a peer exceeds this rate, excess packets are dropped. This protects basic + /// UDPv4 and P2P connections from malicious peers flooding packets (relayed connections + /// on Steam are already rate limited by SDR relays). + /// + /// The rate limit is enforced using a token bucket after cryptographic verification + /// (the nonce/auth tag), so spoofed packets will not consume tokens of a legitimate peer. + /// 0 disables rate limiting (the default). + k_ESteamNetworkingConfig_RecvRateLimit_PacketRate = 61, + + /// [connection int32] Max burst of packets allowed from a peer. + /// Used in conjunction with k_ESteamNetworkingConfig_RecvRateLimit_PacketRate. + /// 0 (the default) will default the burst capacity to RecvRateLimit_PacketRate. + k_ESteamNetworkingConfig_RecvRateLimit_PacketBurst = 62, + /// [connection int64] Get/set userdata as a configuration option. /// The default value is -1. You may want to set the user data as /// a config value, instead of using ISteamNetworkingSockets::SetConnectionUserData diff --git a/src/steamnetworkingsockets/clientlib/csteamnetworkingsockets.cpp b/src/steamnetworkingsockets/clientlib/csteamnetworkingsockets.cpp index 9437078d..0f7bc3cf 100644 --- a/src/steamnetworkingsockets/clientlib/csteamnetworkingsockets.cpp +++ b/src/steamnetworkingsockets/clientlib/csteamnetworkingsockets.cpp @@ -80,6 +80,8 @@ DEFINE_CONNECTON_DEFAULT_CONFIGVAL( int32, RecvBufferSize, 1024*1024, 4*1024, 0x DEFINE_CONNECTON_DEFAULT_CONFIGVAL( int32, RecvBufferMessages, 1000, 2, 0x10000000 ); DEFINE_CONNECTON_DEFAULT_CONFIGVAL( int32, RecvMaxMessageSize, 512*1024, 64, 0x10000000 ); DEFINE_CONNECTON_DEFAULT_CONFIGVAL( int32, RecvMaxSegmentsPerPacket, k_cbSteamNetworkingSocketsMaxUDPMsgLen, 1, k_cbSteamNetworkingSocketsMaxUDPMsgLen ); +DEFINE_CONNECTON_DEFAULT_CONFIGVAL( int32, RecvRateLimit_PacketRate, 0, 0, INT32_MAX ); +DEFINE_CONNECTON_DEFAULT_CONFIGVAL( int32, RecvRateLimit_PacketBurst, 0, 0, INT32_MAX ); DEFINE_CONNECTON_DEFAULT_CONFIGVAL( int64, ConnectionUserData, -1 ); // no limits here DEFINE_CONNECTON_DEFAULT_CONFIGVAL( int32, SendRateMin, 256*1024, 1024, 0x10000000 ); DEFINE_CONNECTON_DEFAULT_CONFIGVAL( int32, SendRateMax, 256*1024, 1024, 0x10000000 ); diff --git a/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_udp.cpp b/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_udp.cpp index 4bfcef3e..26447f35 100644 --- a/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_udp.cpp +++ b/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_udp.cpp @@ -800,6 +800,24 @@ void CConnectionTransportUDPBase::Received_Data( const uint8 *pPkt, int cbPkt, S if ( !m_connection.DecryptDataChunk( nWirePktNumber, cbPkt, pChunk, cbChunk, ctx ) ) return; + // Check packet rate limit. Only authenticated packets from connected peers + // consume tokens, preventing spoofed packet DoS. + int nRate = m_connection.m_connectionConfig.RecvRateLimit_PacketRate.Get(); + if ( nRate > 0 ) + { + int nBurst = m_connection.m_connectionConfig.RecvRateLimit_PacketBurst.Get(); + if ( nBurst <= 0 ) + { + nBurst = Max( nRate, 1 ); + } + + if ( !m_rateLimiterRecvPackets.BCheck( usecNow, (float)nRate, (float)nBurst ) ) + { + SpewWarningRateLimited( usecNow, "[%s] Packet rate limit exceeded, dropping packet\n", ConnectionDescription() ); + return; + } + } + // This is a valid packet. P2P connections might want to make a note of this RecvValidUDPDataPacket( ctx ); diff --git a/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_udp.h b/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_udp.h index 86581736..eac72c68 100644 --- a/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_udp.h +++ b/src/steamnetworkingsockets/clientlib/steamnetworkingsockets_udp.h @@ -156,6 +156,9 @@ class CConnectionTransportUDPBase : public CConnectionTransport virtual void TrackSentStats( UDPSendPacketContext_t &ctx ); virtual void RecvValidUDPDataPacket( UDPRecvPacketContext_t &ctx ); + + // Rate limiter for incoming authenticated UDP data packets + TokenBucketRateLimiter m_rateLimiterRecvPackets; }; diff --git a/src/steamnetworkingsockets/steamnetworkingsockets_internal.h b/src/steamnetworkingsockets/steamnetworkingsockets_internal.h index 85c8efe7..b5e1daa8 100644 --- a/src/steamnetworkingsockets/steamnetworkingsockets_internal.h +++ b/src/steamnetworkingsockets/steamnetworkingsockets_internal.h @@ -873,6 +873,8 @@ struct ConnectionConfig ConfigValue RecvBufferMessages; ConfigValue RecvMaxMessageSize; // NOTE - use GetEffectiveRecvMaxMessageSize()! ConfigValue RecvMaxSegmentsPerPacket; + ConfigValue RecvRateLimit_PacketRate; + ConfigValue RecvRateLimit_PacketBurst; ConfigValue SendRateMin; ConfigValue SendRateMax; ConfigValue MTU_PacketSize; diff --git a/tests/test_connection.cpp b/tests/test_connection.cpp index 44284160..0f824637 100644 --- a/tests/test_connection.cpp +++ b/tests/test_connection.cpp @@ -1203,6 +1203,88 @@ void Test_recv_buf_full() SteamNetworkingSockets()->CloseConnection( hClient, 0, nullptr, false ); } +void Test_recv_rate_limit() +{ + TEST_Printf( "***************************************************\n" ); + TEST_Printf( "Test: recv packet rate limit / token bucket rate limiter\n" ); + TEST_Printf( "***************************************************\n" ); + + // Network loopback so packets go through the full SNP encode/decode path. + HSteamNetConnection hServer, hClient; + assert( SteamNetworkingSockets()->CreateSocketPair( &hServer, &hClient, true, nullptr, nullptr ) ); + SteamNetworkingSockets()->SetConnectionName( hServer, "Server" ); + SteamNetworkingSockets()->SetConnectionName( hClient, "Client" ); + + // Configure rate limiting on the receiver: 10 pkts/sec steady rate, 5 packet burst + const int k_nRateLimit = 10; + const int k_nBurstLimit = 5; + SteamNetworkingUtils()->SetConnectionConfigValueInt32( hServer, + k_ESteamNetworkingConfig_RecvRateLimit_PacketRate, k_nRateLimit ); + SteamNetworkingUtils()->SetConnectionConfigValueInt32( hServer, + k_ESteamNetworkingConfig_RecvRateLimit_PacketBurst, k_nBurstLimit ); + + // Let handshake complete + for ( int i = 0; i < 20; ++i ) + { + TEST_PumpCallbacks(); + std::this_thread::sleep_for( std::chrono::milliseconds( 10 ) ); + } + + // Send immediate burst of 15 packets. + // We use 1000-byte messages to prevent packing into single UDP packet + // (MTU is ~1300), ensures 1 message per 1 UDP packet on the wire. + static const char kData[ 1000 ] = "hello"; + const int k_nPacketsToSend = 15; + for ( int i = 0; i < k_nPacketsToSend; ++i ) + { + EResult r = SteamNetworkingSockets()->SendMessageToConnection( hClient, kData, sizeof( kData ), k_nSteamNetworkingSend_UnreliableNoNagle, nullptr ); + assert( r == k_EResultOK ); + SteamNetworkingSockets()->FlushMessagesOnConnection( hClient ); + std::this_thread::sleep_for( std::chrono::milliseconds( 2 ) ); + } + + std::this_thread::sleep_for( std::chrono::milliseconds( 50 ) ); + TEST_PumpCallbacks(); + + // Verify server received only up to the burst limit, dropping the excess + SteamNetworkingMessage_t *pMsg = nullptr; + int nRecvCount = 0; + while ( SteamNetworkingSockets()->ReceiveMessagesOnConnection( hServer, &pMsg, 1 ) == 1 ) + { + ++nRecvCount; + pMsg->Release(); + } + TEST_Printf( "Sent %d packets in quick burst: server received %d (burst limit = %d)\n", + k_nPacketsToSend, nRecvCount, k_nBurstLimit ); + assert( nRecvCount <= k_nBurstLimit + 1 ); + assert( nRecvCount < k_nPacketsToSend ); + + // Pause 300ms to allow tokens to refill, then send 1 more packet + std::this_thread::sleep_for( std::chrono::milliseconds( 300 ) ); + TEST_PumpCallbacks(); + + // Send 1 more packet. This should succeed now that tokens have replenished + EResult r = SteamNetworkingSockets()->SendMessageToConnection( + hClient, kData, sizeof( kData ), k_nSteamNetworkingSend_UnreliableNoNagle, nullptr ); + assert( r == k_EResultOK ); + SteamNetworkingSockets()->FlushMessagesOnConnection( hClient ); + std::this_thread::sleep_for( std::chrono::milliseconds( 50 ) ); + TEST_PumpCallbacks(); + int nRecvAfterPause = 0; + while ( SteamNetworkingSockets()->ReceiveMessagesOnConnection( hServer, &pMsg, 1 ) == 1 ) + { + ++nRecvAfterPause; + pMsg->Release(); + } + TEST_Printf( "After pause to replenish tokens: server received %d packet(s)\n", nRecvAfterPause ); + assert( nRecvAfterPause == 1 ); + + // Cleanup + SteamNetworkingSockets()->CloseConnection( hServer, 0, nullptr, false ); + SteamNetworkingSockets()->CloseConnection( hClient, 0, nullptr, false ); + TEST_Printf( "Test_recv_rate_limit: PASSED!\n" ); +} + void Test_pipe() { TEST_Printf( "***************************************************\n" ); @@ -1580,7 +1662,8 @@ int main( int argc, const char **argv ) TEST(lane_quick_priority_and_background), TEST(pipe), TEST(send_buffer_full), - TEST(recv_buf_full) + TEST(recv_buf_full), + TEST(recv_rate_limit) }; struct Suite_t { @@ -1588,7 +1671,7 @@ int main( int argc, const char **argv ) std::vector< Test_t > m_vecTests; }; static const Suite_t test_suites[] = { - { "suite-quick", { TEST(identity), TEST(quick), TEST(lane_quick_queueanddrain), TEST(lane_quick_priority_and_background), TEST(pipe), TEST(send_buffer_full), TEST(recv_buf_full) } } + { "suite-quick", { TEST(identity), TEST(quick), TEST(lane_quick_queueanddrain), TEST(lane_quick_priority_and_background), TEST(pipe), TEST(send_buffer_full), TEST(recv_buf_full), TEST(recv_rate_limit) } } }; if ( argc < 2 )