Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions include/steam/steamnetworkingtypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 );

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};


Expand Down
2 changes: 2 additions & 0 deletions src/steamnetworkingsockets/steamnetworkingsockets_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,8 @@ struct ConnectionConfig
ConfigValue<int32> RecvBufferMessages;
ConfigValue<int32> RecvMaxMessageSize; // NOTE - use GetEffectiveRecvMaxMessageSize()!
ConfigValue<int32> RecvMaxSegmentsPerPacket;
ConfigValue<int32> RecvRateLimit_PacketRate;
ConfigValue<int32> RecvRateLimit_PacketBurst;
ConfigValue<int32> SendRateMin;
ConfigValue<int32> SendRateMax;
ConfigValue<int32> MTU_PacketSize;
Expand Down
87 changes: 85 additions & 2 deletions tests/test_connection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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" );
Expand Down Expand Up @@ -1580,15 +1662,16 @@ 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 {
const char *m_pszName;
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 )
Expand Down