edit | blame | history | raw
---
title: PRUDP
---

PRUDP probably stands for **P**rotected **R**eliable **U**ser **D**atagram **P**rotocol, as it is designed for adding reliability, encryption and compression to [UDP](https://en.wikipedia.org/wiki/User_Datagram_Protocol). To do this, it adds [connections](#connections), [acknowledgements](#acknowledgements), [ordering](#ordering), [fragmentation](#fragmentation), [compression](#compression), [encryption](#encryption) and [checksums](#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](#virtual-port) that the packet is being sent from.                                                        | [`Stream`](#structure-stream)                 |
| Destination      | The [virtual stream](#virtual-port) that the packet is being sent to.                                                          | [`Stream`](#structure-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`](#structure-type-and-flags) |
| Session ID       | Unknown.                                                                                                                       | `u8`                                          |
| Packet Signature | Unknown.                                                                                                                       | `u32`                                         |
| Sequence ID      | A strictly monotonically increasing integer used for [acknowledgements](#acknowledgements) and [ordering](#ordering).          | `u16`                                         |

A 4-byte checksum is appended to the end of **every** packet, see [Checksums](#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](#1-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](#connections).

See the [key exchange](#3-key-exchange) stage of connections.

### Client->Server Payload
| Name                 | Description                | Type                                                 |
|----------------------|----------------------------|------------------------------------------------------|
| Connection Signature | Unknown.                   | `u32`                                                |
| Public Key           | The client's _public key_. | [`Public Key`](#structure-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`](03_structure.md#structure-buffer)         |
| Public Key           | The server's _public key_.                                                                                                                                                                                                                                | [`Public Key`](03_structure.md#structure-public-key) |
| Tag                  | A HMAC-SHA256 signature on both the [client's key](#structure-public-key) and the [server's key](#structure-public-key) concatenated, signed using the Diffie-Hellman exchange result (NOT the shared key). | [`Buffer`](Common%20Structures#structure-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](#encryption), then [de-compressed](#compression) (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](#5-data) stage of connections.

### Client->Server, Server->Client Payload
| Name        | Description                                                                                                                                                     | Type  |
|-------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|-------|
| Fragment ID | Used to re-assemble [fragmented](#fragmentation) 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](#6-disconnect) stage of connections.

## Packet: `PING`
This packet contains no additional information, see the [Pinging](#2-pinging) stage of connections.

## Packet: `USER`
This packet contains no additional information, see the [User](#4-user) stage of connections.

# Concepts

## Connections
PRUDP adds **connections** to UDP, meaning that each client is recorded, keeping state, and kept alive using [pings](#packet-ping) and [acknowledgements](#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](#base-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](RMC) data.

Also, each stream can be identified by its [stream type](#enum-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](../Ubi%20Services)). The host's public key is signed using the certificate private key during the [key exchange](#3-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-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](#ordering) sent by the client.
- To identify that the client is a **PRUDP** user, and the [type of connection](#virtual-port) being made.

The host then sends back a [`SYN`](#packet-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-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-syn) packet from the host, the client will attempt to establish a basis for [encrypted communication](#encryption) to be used over [`DATA`](#packet-data) packets, sending its own ephemeral [public key](#structure-public-key) in a [`CONNECT`](#packet-connect) packet. The host performs a Diffie-Hellman key exchange using its secret key, and the **shared secret** used for [encryption](#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](#structure-public-key), prefixed with its **signature** signed using the [signing private key](#identification), 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](#structure-public-key) concatenated with the host's [public key](#structure-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
```py
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-user) packet, which the host acknowledges.

#### 5. Data
The client and host can now both begin sending [`DATA`](#packet-data) packets, [encrypted](#encryption) using the derived secret established during the [key exchange](#3-key-exchange) stage. The data may also be [fragmented](#fragmentation) and [compressed](#compression).

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](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-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](#structure-type-and-flags) flag are required to send back a packet of the **same type**, with the [Ack](#structure-type-and-flags) 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](#packet-syn), and therefore may not be possible once the client and server start sending disjointed [`DATA`](#packet-data) packets.

For example, when a connecting client sends a [`CONNECT`](#client-server-payload-1) packet, it is set with the [Need Ack](#structure-type-and-flags) 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`](#server-client-payload-1) packet with the [Ack](#structure-type-and-flags) flag set.

[Reliable](#structure-type-and-flags)-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](#structure-type-and-flags) flag also have the [Multi Ack](#structure-type-and-flags) flag. The purpose of this flag is not entirely clear, but the recipient must respond to it with both the [Ack](#structure-type-and-flags) flag **and** the [Multi Ack](#structure-type-and-flags) 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](https://en.wikipedia.org/wiki/Cryptographic_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`](#packet-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`](#packet-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
```py
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`](#packet-data) packets **may** be compressed using [zlib](https://www.zlib.net/). This **always** comes before [encryption](#encryption) and after [fragmentation](#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
```py
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`](#packet-data) packets **must** be encrypted using 128 bit [AES](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard)-[CBC](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_block_chaining_(CBC)). This **always** comes after [compression](#compression), meaning that the **fragmented, compressed** data is encrypted.

The 16 byte IV for each packet is generated randomly using a [CRPNG](https://en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator), 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](#3-key-exchange) stage.

### Pseudo-Code Example
```py
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 `0`s.

### Pseudo-Code Example
```py
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](#virtual-port) for this stream. | `u4`                               |
| Port | The type of stream that the connection is using.  | [`Stream Type`](#enum-stream-type) |

### Structure: `Type and Flags`
A packed `u8` containing
| Name      | Description                                                                                                                                                                                                          | Type                               |
|-----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------|
| Type      | The type of packet that is being sent.                                                                                                                                                                               | [`Packet Type`](#enum-packet-type) |
| Ack       | This packet is an [acknowledgement](#acknowledgements) 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`](#packet-syn) and [`CONNECT`](#packet-connect), after the connection signature, and for [`DATA`](#packet-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`](#packet-syn)               | `0`   |
| [`CONNECT`](#packet-connect)       | `1`   |
| [`DATA`](#packet-data)             | `2`   |
| [`DISCONNECT`](#packet-disconnect) | `3`   |
| [`PING`](#packet-ping)             | `4`   |
| [`USER`](#packet-user)             | `6`   |