UDocumentation UE5.7 10.02.2026 (Source)
API documentation for Unreal Engine 5.7
StatelessConnectHandlerComponent.cpp File Reference

Namespaces

namespace  UE
 
namespace  UE::Net
 

Macros

#define PACKETLOSS_TEST   0
 
#define BASE_PACKET_SIZE_BITS   82
 
#define HANDSHAKE_PACKET_SIZE_BITS   (BASE_PACKET_SIZE_BITS + 225)
 
#define RESTART_HANDSHAKE_PACKET_SIZE_BITS   BASE_PACKET_SIZE_BITS
 
#define RESTART_RESPONSE_SIZE_BITS   (BASE_PACKET_SIZE_BITS + 385)
 
#define VERSION_UPGRADE_SIZE_BITS   BASE_PACKET_SIZE_BITS
 
#define SECRET_UPDATE_TIME   15.f
 
#define SECRET_UPDATE_TIME_VARIANCE   5.f
 
#define MAX_COOKIE_LIFETIME   ((SECRET_UPDATE_TIME + SECRET_UPDATE_TIME_VARIANCE) * (float)SECRET_COUNT)
 
#define MIN_COOKIE_LIFETIME   SECRET_UPDATE_TIME
 

Functions

 DEFINE_LOG_CATEGORY (LogHandshake)
 
const TCHARUE::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."))
 

Macro Definition Documentation

◆ BASE_PACKET_SIZE_BITS

#define BASE_PACKET_SIZE_BITS   82

Defines

◆ HANDSHAKE_PACKET_SIZE_BITS

#define HANDSHAKE_PACKET_SIZE_BITS   (BASE_PACKET_SIZE_BITS + 225)

◆ MAX_COOKIE_LIFETIME

◆ MIN_COOKIE_LIFETIME

#define MIN_COOKIE_LIFETIME   SECRET_UPDATE_TIME

◆ PACKETLOSS_TEST

#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.

Handshake Process/Protocol:

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.

Restart Handshake Process/Protocol:

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.

  • MagicHeader: An optional static/predefined header, between 0-32 bits in size. Serves no purpose for the handshake code.
  • SessionID: Session id incremented serverside every non-seamless server travel, to prevent non-ephemeral old/new-session crosstalk.
  • ClientID: Connection id incremented clientside every connection per-NetDriver, to prevent non-ephemeral old/new-connection crosstalk.
  • HandshakeBit: Bit signifying whether a packet is a handshake packet. Applied to all game packets.
  • SecretIdBit: For handshake packets, specifies which HandshakeSecret array was used to generate Cookie.
  • RestartHandshakeBit: Sent by the server when it detects normal game traffic from an unknown IP/port combination.
  • MinVersion: The minimum handshake protocol version supported by the remote side
  • CurVersion: The currently active protocol version used by the remote side (determines received packet format)
  • HandshakePacketType: Number indicating the type of handshake packet, based on EHandshakePacketType
  • SentPacketCount: The number of handshake packets sent - for packet-analysis/debugging purposes
  • NetworkVersion: The Network CL version, according to FNetworkVersion::GetLocalNetworkVersion
  • NetworkFeatures The runtime Network Features, according to UNetDriver::GetNetworkRuntimeFeatures
  • Timestamp: Server timestamp, from the moment the handshake challenge was sent.
  • Cookie: Cookie generated by the server, which the client must reply with.
  • AlignPad: Handshake packets and PacketHandler's in general, require complex padding of packets. See ParseHandshakePacket.
  • RandomData: Data of random length/content appended to handshake packets, to work around potential faulty ISP packet filtering
  • 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

◆ RESTART_HANDSHAKE_PACKET_SIZE_BITS

#define RESTART_HANDSHAKE_PACKET_SIZE_BITS   BASE_PACKET_SIZE_BITS

◆ RESTART_RESPONSE_SIZE_BITS

#define RESTART_RESPONSE_SIZE_BITS   (BASE_PACKET_SIZE_BITS + 385)

◆ SECRET_UPDATE_TIME

#define SECRET_UPDATE_TIME   15.f

◆ SECRET_UPDATE_TIME_VARIANCE

#define SECRET_UPDATE_TIME_VARIANCE   5.f

◆ VERSION_UPGRADE_SIZE_BITS

#define VERSION_UPGRADE_SIZE_BITS   BASE_PACKET_SIZE_BITS

Function Documentation

◆ DEFINE_LOG_CATEGORY()

DEFINE_LOG_CATEGORY ( LogHandshake  )

Variable Documentation

◆ CVarNetMagicHeader

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