edit | blame | history | raw

---

title: PRUDP

PRUDP probably stands for Protected Reliable User Datagram Protocol, as it is designed for adding reliability, encryption and compression to UDP. To do this, it adds connections, acknowledgements, ordering, fragmentation, compression, encryption and checksums.

Packets

Base Packet

Every packet, sent by both the client and the server, has the following base information:

Name Description Type
Source The virtual stream that the packet is being sent from. Stream
Destination The virtual stream that the packet is being sent to. Stream
Type and Flags Type of the packet, and flags indicating how the client should treat this packet. See below for how to parse each packet type. Type and Flags
Session ID Unknown. u8
Packet Signature Unknown. u32
Sequence ID A strictly monotonically increasing integer used for acknowledgements and ordering. u16

A 4-byte checksum is appended to the end of every packet, see Checksums.

Packet: SYN

This packet establishes a connection with the server. SYN here likely stands for synchronization, because it synchronizes the client's sequence ID with the server's.

See the synchronize stage of connections.

Client->Server Payload

Name Description Type
Connection Signature Unknown. u32

Server->Client Payload

Name Description Type
Connection Signature Unknown. u32

Packet: CONNECT

This stage is used to execute a Diffie-Hellman key exchange, and then derive a shared secret key.

The key exchange operation is $S = d \times Q$, where $S$ is the resulting key, $d$ is the server's private key, and $Q$ is the client's public key. The resulting y co-ordinate of this exchange is discarded.

The shared key, used for DATA packet encryption and decryption, is derived using the first 16 bytes of a SHA1 hash on the Diffie-Hellman exchange result.

The server may also use this stage to affirm the client's connection signature.

See the key exchange stage of connections.

Client->Server Payload

Name Description Type
Connection Signature Unknown. u32
Public Key The client's public key. Public Key

Server->Client Payload

Name Description Type
Connection Signature Unknown. u32
Public Key Signature The server's public key signed by the server's certification private key. Buffer
Public Key The server's public key. Public Key
Tag A HMAC-SHA256 signature on both the client's key and the server's key concatenated, signed using the Diffie-Hellman exchange result (NOT the shared key). Buffer

Packet: DATA

The whole data is first fragmented, then compressed, then encrypted.

This means that the data in this packet, if fragmented, should be decrypted first, then de-compressed (if applicable), and then re-assembled.

The compressed payload (before encryption) is prefixed with a single byte indicating the compression ratio. 0 means that the remaining data is not compressed. The client appears to only ever use 0x02 for this byte, however.

The fragmented payload (before compression) is suffixed with the 2-byte sequence id of the data packet being sent.

See the Data stage of connections.

Client->Server, Server->Client Payload

Name Description Type
Fragment ID Used to re-assemble fragmented data packets, starting from 1 (if multiple fragments exist), and ending in 0 to indicate a final fragment. u32
... Encrypted and compressed data any

Packet: DISCONNECT

This packet contains no additional information, see the Disconnect stage of connections.

Packet: PING

This packet contains no additional information, see the Pinging stage of connections.

Packet: USER

This packet contains no additional information, see the User stage of connections.

Concepts

Connections

PRUDP adds connections to UDP, meaning that each client is recorded, keeping state, and kept alive using pings and acknowledgements.

Terminology for this section:
| Word | Meaning |
|--------|------------------------------------------------------------|
| Client | A user of this protocol wishing to establish a connection. |
| Host | A user of this protocol willing to accept a connection. |

Virtual Port

Each UDP socket from the client supports multiple virtual streams. This means that the sender of a packet is not just identified by the IP and port that it originated from, but also the virtual source port and the virtual destination port.

It is presumably implementation behaviour how these ports function. For example, for the initial LoadBalancer PRUDP connection, the game seems to open a virtual port 0x01 and sends packets to the host's virtual port 0x0f. This likely means that the host can expect a certain kind of data, e.g. RMC data.

Also, each stream can be identified by its stream type, but it is yet to be seen how this impacts the data.

This article uses connection to refer to these virtual streams, with the knowledge that each UDP socket may have multiple connections assigned to it.

Identification

Similar to TLS, PRUDP defines a system by which the client can ensure the identity of the host being connected to. This is done using a certificate public key provided through another means (e.g. given by Ubi Services). The host's public key is signed using the certificate private key during the key exchange stage.

You can generate this certificate key pair using OpenSSL:
- Private Key: openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:prime256v1 -out key.pem
- Public Key: openssl ec -in key.pem -pubout -out key.out

Sequence

1. Synchronize

The initial packet is sent by the client, who sends a SYN packet to indicate that they wish to establish a connection with the host. This packet has two purposes:
- It has an initial sequence id, so that the host can begin ordering packets sent by the client.
- To identify that the client is a PRUDP user, and the type of connection being made.

The host then sends back a SYN of its own, and can register this client and begin pinging it to keep the connection alive.

2. Pinging

Both the client and host ping eachother, using a PING packet. These seem to be sent roughly every 10 seconds, although presumably may be sent whenever.
Also, the game appears to consider the missing acknowledgement of 2 consecutive pings as a connection failure, and will silently close the connection.

3. Key Exchange

After receiving a SYN packet from the host, the client will attempt to establish a basis for encrypted communication to be used over DATA packets, sending its own ephemeral public key in a CONNECT packet. The host performs a Diffie-Hellman key exchange using its secret key, and the shared secret used for encryption is derived using the first 16 bytes of the SHA1 hash of the result of this exchange.

The host then sends back its own ephemeral public key, prefixed with its signature signed using the signing private key, for the client to verify using the host's public certificate. The response also contains a 32 byte tag, which is computed using a SHA256 HMAC signature of the client's public key concatenated with the host's public key, using the result of the Diffie-Hellman key exchange as a key (NOT the SHA1 derived key used for encryption).

Pseudo-Code Example
def exchange_keys(client_public_key):
    exchange_result = diffie_hellman_exchange(client_public_key, host_secret_key)

    signature = sign(host_public_key, signing_private_key)

    combined_keys = client_public_key + host_public_key
    tag = hmac(combined_keys, exchange_result, "sha256")

    derived_secret = hash(exchange_result, "sha1")

    return signature, host_public_key, tag, derived_secret

4. "User"

It is not clear what the point of this stage is, but after a key exchange has been performed and a derived shared secret has been established, the client sends a USER packet, which the host acknowledges.

5. Data

The client and host can now both begin sending DATA packets, encrypted using the derived secret established during the key exchange stage. The data may also be fragmented and compressed.

The unfragmented, uncompressed and unencrypted whole payload is not defined, and may use any protocol. This is because PRUDP is a reliable transportation layer on top of UDP. The application defines the protocol used for the communicated information. For example, the RMC protocol may be used on top of the PRUDP protocol.

6. Disconnect

When the client or host wishes to close a connection, for example in the case of missing acknowledgements (dead connection), or when manually disconnecting, they can simply send a DISCONNECT packet to the other. This does not need to be acknowledged, and it is not required for a client or host to send a disconnect packet, they can also simply refuse to stop accepting packets.

Consider it courtesy to send a disconnect packet.

Acknowledgements

Clients receiving packets with the Need Ack flag are required to send back a packet of the same type, with the Ack flag set and the sequence ID set to the received packet's sequence ID. The packet in response may optionally contain data, so that responses can be given and the original packet acknowledged in the same packet. Note that this requires sequence ID synchronization, and therefore may not be possible once the client and server start sending disjointed DATA packets.

For example, when a connecting client sends a CONNECT packet, it is set with the Need Ack flag. A receiving server can then exchange the client's public key for a shared secret, and then can respond with its own public key in another CONNECT packet with the Ack flag set.

Reliable-flagged packets are re-sent by the sender if an acknowledgement is not received within a certain time frame.

Some packets with the Need Ack flag also have the Multi Ack flag. The purpose of this flag is not entirely clear, but the recipient must respond to it with both the Ack flag and the Multi Ack flag set.

Ordering

A receiver may choose to keep track of which sequence ID a sender sent last, so as to know which to expect next from the sender. If a sequence ID is sent that is greater than the one expected, the receiver may also choose to retain the packet until the expected sequence ID has been sent by the sender. This is recommended because UDP is inherently unreliable, and may deliver packets out of order than was sent by the client.

Sequence IDs that are less than the one expected can be rejected, and therefore treated as a nonce.

[!IMPORTANT]
Packets should always be acknowledged regardless of whether they are out of order or not, as the sender may be re-sending it on purpose, having not received an acknowledgement from the recipient.

[!WARNING]
Care should be made to only handle or retain packets with sequence IDs that have not already been retained, so as to not double count repeated sending of the same packet.

Pings

Pings appear to use a difference sequence to the main reliable packets. This may be the case for all unreliable packets.

Fragmentation

DATA packets may be fragmented. The maximum packet size is not defined, but generally should be kept less than 1024 bytes for best deliverability.

Each DATA is tagged with a fragment id, which starts at 1, incrementing each fragment. The final fragment should have a fragment id of 0.

Each fragment data, a slice of the whole data, is suffixed with a 2 byte LE integer, which is the same as the sequence ID of the fragment packet being sent.

Example

Fragment # Fragment ID
1/4 1
2/4 2
3/4 3
4/4 0

Example

Fragment # Fragment ID
1/1 0

[!NOTE]
This means that non-fragmented data is sent with a fragment id of 0.

Pseudo-Code Example

def send_fragmented(whole_data, max_size, next_sequence_id):
    frag_id = 1
    i = 0
    while i < len(whole_data):
        i += max_size
        # frag_id = 0 indicates that this is the last fragment that the recipient should expect
        is_last = i >= len(whole_data)
        if (is_last) frag_id = 0
        # next_sequence_id = 2 byte LE integer, should be same as the packet sequence id
        fragmented_data = whole_data[i-max_size : i] + next_sequence_id

        # note that each data fragment is otherwise a completely standalone packet with its own sequence id
        send_data(encrypt(compress(fragmented_data)), frag_id)
        frag_id += 1
        next_sequence_id += 1

    return next_sequence_id

Compression

DATA packets may be compressed using zlib. This always comes before encryption and after fragmentation, meaning that the fragmented data is compressed.

After compression, the compressed data is prefixed with a 1-byte compression ratio, which seems to always be set to 0x02.

[!NOTE]
Packets do not have to be compressed, in which case the compression ratio should read 0x00 so that the recipient can skip compression and read directly.

Pseudo-Code Example

def compress(fragmented_data):
    compressed_data = zlib_deflate(fragmented_data, level=default)

    # compression_ratio = 1 byte integer
    # seems to always be: compression_ratio = 2
    return compression_ratio + compressed_data

Encryption

DATA packets must be encrypted using 128 bit AES-[CBC](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_block_chaining_(CBC)). This always comes after compression, meaning that the fragmented, compressed data is encrypted.

The 16 byte IV for each packet is generated randomly using a CRPNG, and prefixes the encrypted data in the payload.

The key used for the encryption is derived using the shared key established from the key exchange stage.

Pseudo-Code Example

def encrypt(data, derived_key):
    iv = crypto_random_bytes(16)
    cipher_text = aes_cbc_encrypt(data, derived_key, iv)

    return iv + cipher_text

Checksums

Every packet has a 4-byte checksum at the end. The algorithm used is adding each 4 bytes of the packet interpreted as u32 LE. The data is padded to have a length divisible by 4 using 0s.

Pseudo-Code Example

def append_checksum(data):
    # sum of u32 LE words (zero-padded to 4-byte boundary).
    pad_len = (4 - len(data) % 4) % 4
    padded = data + b'\x00' * pad_len
    words = struct.unpack('<%dI' % (len(padded) // 4), padded)
    # checksum is u32 little-endian
    return data + (sum(words) & 0xFFFFFFFF)

Structures

Structure: Public Key

An uncompressed SEC1 formatted key, without the preceeding compression tag (0x04).

Structure: Stream

A packed u8 containing
| Name | Description | Type |
|------|---------------------------------------------------|------------------------------------|
| Port | The virtual port for this stream. | u4 |
| Port | The type of stream that the connection is using. | Stream Type |

Structure: Type and Flags

A packed u8 containing
| Name | Description | Type |
|-----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------|
| Type | The type of packet that is being sent. | Packet Type |
| Ack | This packet is an acknowledgement packet for a reliable packet sent by the recipient. | bool |
| Reliable | This packet must be acknowledged by the recipient, or it will be re-sent. | bool |
| Need Ack | The sender is expecting an acknowledgement for this packet. | bool |
| Has Size | The packet contains a u16 size following the packet header (or, for SYN and CONNECT, after the connection signature, and for DATA, after the fragment ID). | bool |
| Multi Ack | Unknown, doesn't appear to be supported, but if a packet flagged with Need Ack has this flag, the recipient should send an Ack with this flag set too. | bool |

Enum: Stream Type

A u4 containing these values:

Name Value
RV Authentication 2
RV Secure 3
Sandbox Management 4
NAT 5
Session Discovery 6
Nat Echo 7

[!NOTE]
Hyperscape appears to exclusively use the RV Secure (3) type.

Enum: Packet Type

A u3 containing these values:

Name Value
SYN 0
CONNECT 1
DATA 2
DISCONNECT 3
PING 4
USER 6