![]() |
UDocumentation UE5.7 10.02.2026 (Source)
API documentation for Unreal Engine 5.7
|
#include "PacketHandlers/StatelessConnectHandlerComponent.h"#include "Serialization/MemoryWriter.h"#include "EngineStats.h"#include "Misc/SecureHash.h"#include "Engine/NetConnection.h"#include "Net/Core/Misc/PacketAudit.h"#include "Misc/ConfigCacheIni.h"#include "Stats/StatsTrace.h"#include <limits>Namespaces | |
| namespace | UE |
| namespace | UE::Net |
Functions | |
| DEFINE_LOG_CATEGORY (LogHandshake) | |
| const TCHAR * | UE::Net::LexToString (EHandshakePacketType PacketType) |
Variables | |
| TAutoConsoleVariable< FString > | CVarNetMagicHeader (TEXT("net.MagicHeader"), TEXT(""), TEXT("String representing binary bits which are prepended to every packet sent by the game. Max length: 32 bits.")) |
| #define BASE_PACKET_SIZE_BITS 82 |
Defines
| #define HANDSHAKE_PACKET_SIZE_BITS (BASE_PACKET_SIZE_BITS + 225) |
| #define MAX_COOKIE_LIFETIME ((SECRET_UPDATE_TIME + SECRET_UPDATE_TIME_VARIANCE) * (float)SECRET_COUNT) |
| #define MIN_COOKIE_LIFETIME SECRET_UPDATE_TIME |
| #define PACKETLOSS_TEST 0 |
Purpose:
UDP connections are vulnerable to various types of DoS attacks, particularly spoofing the IP address in UDP packets, and to protect against this a handshake is needed to verify that the IP is really owned by the client.
This handshake can be implemented in two ways: Stateful: Here the server stores connection state information (i.e. maintains a UNetConnection) while the handshake is underway, allowing spoofed packets to allocate server memory space, prior to handshake verification.
Stateless: Here the server does not store any connection state information, until the handshake completes, preventing spoofed packets from allocating server memory space until after the handshake.
Stateful handshakes are vulnerable to DoS attacks through server memory usage, whereas stateless handshakes are not, so this implementation uses stateless handshakes.
The protocol for the handshake involves the client sending an initial packet to the server, and the server responding with a unique 'Cookie' value, which the client has to respond with.
Client - Initial Connect:
[?:MagicHeader][2:SessionID][3:ClientID][HandshakeBit][RestartHandshakeBit] [8:MinVersion][8:CurVersion][8:HandshakePacketType][8:SentPacketCount][32:NetworkVersion] [16:NetworkFeatures][SecretIdBit][28:PacketSizeFiller][AlignPad][?:RandomData] —> Server - Stateless Handshake Challenge:
[?:MagicHeader][2:SessionID][3:ClientID][HandshakeBit][RestartHandshakeBit] [8:MinVersion][8:CurVersion][8:HandshakePacketType][8:SentPacketCount][32:NetworkVersion] [16:NetworkFeatures][SecretIdBit][8:Timestamp][20:Cookie][AlignPad][?:RandomData] <— Client - Stateless Challenge Response:
[?:MagicHeader][2:SessionID][3:ClientID][HandshakeBit][RestartHandshakeBit] [8:MinVersion][8:CurVersion][8:HandshakePacketType][8:SentPacketCount][32:NetworkVersion] [16:NetworkFeatures][SecretIdBit][8:Timestamp][20:Cookie][AlignPad][?:RandomData] —> Server: Ignore, or create UNetConnection.
Server - Stateless Handshake Ack:
[?:MagicHeader][2:SessionID][3:ClientID][HandshakeBit][RestartHandshakeBit] [8:MinVersion][8:CurVersion][8:HandshakePacketType][8:SentPacketCount][32:NetworkVersion] [16:NetworkFeatures][SecretIdBit][8:Timestamp][20:Cookie][AlignPad][?:RandomData] <— Client: Handshake Complete.
The Restart Handshake process is triggered by receiving a (possibly spoofed) non-handshake packet from an unknown IP, so the protocol has been crafted so the server sends only a minimal (1 byte) response, to minimize DRDoS reflection amplification.
Server - Restart Handshake Request:
[?:MagicHeader][2:SessionID][3:ClientID][HandshakeBit][RestartHandshakeBit]
[8:HandshakePacketType][8:SentPacketCount][32:NetworkVersion][16:NetworkFeatures]
[AlignPad][?:RandomData]
<--
Client - Initial Connect (as above) --> Server - Stateless Handshake Challenge (as above) <– Client - Stateless Challenge Response + Original Cookie:
[?:MagicHeader][2:SessionID][3:ClientID][HandshakeBit][RestartHandshakeBit] [8:MinVersion][8:CurVersion][8:HandshakePacketType][8:SentPacketCount][32:NetworkVersion] [16:NetworkFeatures][SecretIdBit][8:Timestamp][20:Cookie][20:OriginalCookie] [AlignPad][?:RandomData] --> Server: Ignore, or restore UNetConnection.
Server - Stateless Handshake Ack (as above) <– Client: Handshake Complete. Connection restored.
PacketSizeFiller: Pads the client packet with blank information, so that the initial client packet, is the same size as the server response packet.
The server will ignore initial packets below/above this length. This prevents hijacking of game servers, for use in 'DRDoS' reflection amplification attacks.
Game Protocol Changes:
Every packet (game and handshake) starts with the MagicHeader bits (if set), then 2 SessionID bits and 3 ClientID bits, and finally HandshakeBit (which is 0 for game packets, going through normal PacketHandler/NetConnection protocol processing, and 1 for handshake packets, going through the separate protocol documented above).
HandshakeSecret/Cookie:
The Cookie value is used to uniquely identify and perform a handshake with a connecting client, but only the server can generate and recognize valid cookies, and the server must do this without storing any connection state data.
To do this, the server stores 2 large random HandshakeSecret values, that only the server knows, and combines that with data unique to the client connection (IP and Port), plus a server Timestamp, as part of generating the cookie.
This data is then combined using a special HMAC hashing function, used specifically for authentication, to generate the cookie: Cookie = HMAC(HandshakeSecret, Timestamp + Client IP + Client Port)
When the client responds to the handshake challenge, sending back TimeStamp and the Cookie, the server will be able to collect all the remaining information it needs from the client packet (Client IP, Client Port), plus the HandshakeSecret, to be able to regenerate the Cookie from scratch, and verify that the regenerated cookie, is the same as the one the client sent back.
No connection state data needs to be stored in order to do this, so this allows a stateless handshake.
In addition, HandshakeSecret updates every 15 + Rand(0,5) seconds (with previous value being stored/accepted for same amount of time) in order to limit packet replay attacks, where a valid cookie can be reused multiple times.
Checks on the handshake Timestamp, especially when combined with 5 second variance above, compliment this in limiting replay attacks.
IP/Port Switching:
Rarely, some routers have a bug where they suddenly change the port they send traffic from. The consequence of this is the server starts receiving traffic from a new IP/port combination from an already connected player. When this happens, it tells the client via the RestartHandshakeBit to restart the handshake process.
The client carries on with the handshake as normal, but when completing the handshake, the client also sends the cookie it previously connected with. The server looks up the NetConnection associated with that cookie, and then updates the address for the connection.
SessionID/ClientID and non-ephemeral sockets
The packet protocol has a reliance on IP packet ephemeral ports (the random client-specified source port) to differentiate client connections, but not all socket subsystems provide something which serves this role - some socket subsystems only provide a static address without a port, where the address remains indistinguishable between old/new client connections, e.g. when performing a non-seamless travel between levels.
The SessionID value solves this for the case of non-seamless travel, by specifying an incrementing server-authoritative ID for the game session (which changes upon non-seamless travel).
The ClientID solves this for the case of clients reconnecting to a server they are currently connected to, or which their old connection is pending timeout from (e.g. after a crash or other fault requiring a reconnect), by specifying an incrementing client-authoritative ID for the connection (per-NetDriver - so e.g. Game and Beacon drivers increment separately).
This is not a complete solution, however - multiple clients from the same address will not work, presently. ClientID has enough bits to implement this in the future, while keeping net compatibility, but is non-trivial and not guaranteed to be added.
If this is to be added though, it will require adjustments to the serverside NetDriver receive code, to set the ClientID as the address port, and will require adjustments to the clientside setting of ClientID, to perform inter-process communication when picking the ClientID, so that the value is unique for every NetConnection across every game process, connecting to the same server address + port.
Also, if the server and client are using the same *Engine.ini file, the last process to close will clobber the GlobalNetTravelCount/CachedClientID increments from the other process, which are used for SessionID/ClientID. Debug Defines
| #define RESTART_HANDSHAKE_PACKET_SIZE_BITS BASE_PACKET_SIZE_BITS |
| #define RESTART_RESPONSE_SIZE_BITS (BASE_PACKET_SIZE_BITS + 385) |
| #define SECRET_UPDATE_TIME 15.f |
| #define SECRET_UPDATE_TIME_VARIANCE 5.f |
| #define VERSION_UPGRADE_SIZE_BITS BASE_PACKET_SIZE_BITS |
| DEFINE_LOG_CATEGORY | ( | LogHandshake | ) |
| TAutoConsoleVariable< FString > CVarNetMagicHeader(TEXT("net.MagicHeader"), TEXT(""), TEXT("String representing binary bits which are prepended to every packet sent by the game. Max length: 32 bits.")) | ( | TEXT("net.MagicHeader") | , |
| TEXT("") | , | ||
| TEXT("String representing binary bits which are prepended to every packet sent by the game. Max length: 32 bits.") | |||
| ) |
CVars