rust-raknet 0.16.0

RakNet Protocol implementation by Rust.
Documentation
rust-raknet-0.16.0 has been yanked.

rust-raknet

A high-performance implementation of the RakNet protocol in Rust.

Build Crates.io Documentation MIT Discord

Get started · Bedrock proxy · Benchmarks · Contributing

rust-raknet provides reliable message delivery over UDP for Rust applications. Built on Tokio, it implements RakNet handshakes, acknowledgements, retransmission, ordering and fragmentation. Choose the delivery guarantees your application needs.

  • Client and listener APIs, with all five RakNet reliability modes.
  • Bounded send queues, backpressure and selective retransmission.
  • Configurable MTU, accept backlog and UDP receive buffers.
  • Optional Linux receive socket sharding for busy servers.
  • Pure Rust, MIT licensed. Linux, Windows, macOS and BSD.

Requirements: Rust 1.85+ and Tokio 1.38+.

Get started

[dependencies]
rust-raknet = "0.16.0"
tokio = { version = "1.38", features = ["full"] }

Echo server

use rust_raknet::{RaknetListener, Reliability, error::Result};

#[tokio::main]
async fn main() -> Result<()> {
    let address = "127.0.0.1:19132".parse().unwrap();
    let mut listener = RaknetListener::bind(&address).await?;
    listener.listen().await;

    loop {
        let socket = listener.accept().await?;
        tokio::spawn(async move {
            while let Ok(message) = socket.recv().await {
                if socket.send(&message, Reliability::ReliableOrdered).await.is_err() {
                    break;
                }
            }
        });
    }
}

Client

use rust_raknet::{RaknetSocket, Reliability, error::Result};

#[tokio::main]
async fn main() -> Result<()> {
    let address = "127.0.0.1:19132".parse().unwrap();
    let socket = RaknetSocket::connect(&address).await?;
    socket.send(&[0xfe, 1, 2, 3], Reliability::ReliableOrdered).await?;
    let reply = socket.recv().await?;
    println!("Echo: {reply:?}");
    socket.close().await
}

Application messages must be nonempty and start with 0xfe. Keep receiving while sending sustained bursts so the other end can make progress.

Reliability Delivery Ordering
Unreliable Best effort None
UnreliableSequenced Best effort Older messages are discarded
Reliable Retransmitted until acknowledged None
ReliableOrdered Retransmitted until acknowledged In order, per channel
ReliableSequenced Reliable, with sequencing Older messages are discarded

See the API docs for send_with_order_channel, flush, discovery and listener options.

Minecraft Bedrock

The crate transports Bedrock packets as bytes. It does not implement Xbox login or decode the game protocol. The proxy example forwards RakNet UDP game traffic and requires a backend server configured with transport=raknet.

Enabling RakNet in Bedrock 26.52

As of October 2, 2026, the latest stable Bedrock release is 26.52. The tested setup used a Windows 1.26.52 client and Bedrock Dedicated Server 1.26.52.3, with game protocol 2193 and RakNet protocol 11. Joining, movement, placing and breaking blocks, and the proxy's server-list MOTD all worked through rust-raknet 0.16.0 over RakNet UDP.

Dedicated servers default to NetherNet starting with 26.50. For the tested 26.52 setup, stop the server and edit its server.properties:

transport=raknet
server-port=19142
server-portv6=19143
enable-lan-visibility=false

Restart the dedicated server after saving. Setting transport=raknet selects the legacy UDP transport. Disabling LAN visibility avoids the extra default-port listeners when using a custom port. Keep the backend and proxy in the same isolated test network; the proxy command below forwards to backend port 19142.

Configure a matching Bedrock advertisement on the proxy listener before calling listen(). In example/proxy/src/main.rs, insert this before listener.listen().await:

listener.set_motd(
    "Rust RakNet Bedrock proxy",
    10,
    "2193",
    "1.26.52",
    "Survival",
    local_address.port(),
).await?;

Use the backend's protocol, game version and game mode when adapting this setup. The listener's default advertisement describes an older Bedrock version. An incorrect advertisement can prevent a recent client from joining, even when the UDP handshake works.

RakNet deprecation in Bedrock

Mojang is moving Bedrock networking to NetherNet. The 26.60.22/23 Preview changelog marks RakNet deprecated and changes the dedicated-server warning about using another transport into an error. Our 26.52 server also printed a NetherNet-only error, while the RakNet connection and gameplay tests still succeeded.

The compatibility result above covers the tested 26.52 builds. It does not establish support for 26.60 or later, and the cited preview note does not give a confirmed final removal date. Check the release notes and retest before upgrading a deployment that depends on RakNet. rust-raknet implements RakNet UDP; NetherNet uses WebRTC and requires a different transport implementation.

RakNet proxy

cargo run --release --manifest-path example/proxy/Cargo.toml -- \
  -l 127.0.0.1:19144 -r 127.0.0.1:19142

For a local test, add 127.0.0.1:19144 to the client's server list. On another machine, use the proxy's reachable address and frontend port. The client must join the proxy endpoint for its game traffic to pass through rust-raknet.

The proxy keeps the client's RakNet version when connecting upstream. Both forwarding directions run concurrently; upstream handshakes time out after 10 s. On Linux, add --socket-shards 4 to opt into four receive sockets. The default is one; several frontend shards can be slower when they feed a single upstream socket.

Server discovery

cargo run --manifest-path example/bedrock_ping/Cargo.toml -- play.example.com:19132

This sends an unconnected RakNet ping and prints the server name, game version, player counts and MOTD.

Configuration

The default nominal MTU is 1,400 B. Both peers can opt into another limit:

use rust_raknet::{RaknetListener, RaknetSocket, error::Result};

async fn configured_connection() -> Result<()> {
    let address = "127.0.0.1:19132".parse().unwrap();
    let mut listener = RaknetListener::bind_with_maximum_mtu(&address, 1428).await?;
    listener.listen().await;
    let client = RaknetSocket::connect_with_version_and_mtu(&address, 11, 1428).await?;
    assert_eq!(client.mtu(), 1428);
    client.close().await?;
    listener.close().await
}

Limits must be 61–1,492 B. Negotiation takes the smaller peer limit; mtu() returns it. The nominal MTU reserves 28 B for IPv4/UDP headers. Account for IPv6 or tunnel overhead when choosing a size. This is not path MTU discovery.

use std::num::NonZeroUsize;
use rust_raknet::{RaknetListener, error::Result};

async fn listen() -> Result<RaknetListener> {
    let address = "127.0.0.1:19132".parse().unwrap();
    let mut listener = RaknetListener::bind_with_socket_shards(
        &address,
        NonZeroUsize::new(4).unwrap(),
    ).await?;
    listener.listen().await;
    Ok(listener)
}

SO_REUSEPORT distributes peer flows across the sockets. They share one GUID, MOTD and accept backlog, and close as one listener. The shard count stays fixed until shutdown. The runtime needs enough worker threads to use them; benchmark your workload before changing the default of one socket.

  • Sends wait for the connected handshake and for space in a full send queue. Reliable delivery uses a 64-datagram flight window. A normal burst queues up to 256 KiB including frame overhead. A larger ReliableOrdered message can enter an empty queue, subject to a 64 MiB budget and 65,536 fragments. The budget counts payload plus 128 B per frame; it is not a process memory limit.
  • Receive reordering allows 65,536 reliable indexes and 64 MiB of ordered payload. Fragment reassembly allows 1,024 groups, 65,536 fragments per group and 64 MiB including frame overhead. A group stalled for 60 s closes the connection. Invalid or excessive receive state closes the connection instead of silently dropping acknowledged data.
  • bind() requests a 2 MiB UDP receive buffer; the OS may clamp it. bind_with_receive_buffer_size() requests a different size. from_std() keeps the supplied socket's buffer settings.
  • The default accept backlog is 128. Set with_accept_backlog(NonZeroUsize) before listen() to change it. A full backlog defers new offline handshakes until their next retry. It does not cap active sessions.

Benchmarks

These measurements compare this rust-raknet implementation with a TCP echo program, the official C KCP implementation, and quic-go. They are not measurements of the RakNet protocol in general.

Benchmark reference date: October 1, 2026.

Measured on an Intel Core i7-9700F (8 logical CPUs), Linux x86_64, Rust 1.98.1, Tokio 1.53.1, GCC 13.3.0 and Go 1.27.1, using optimized builds. Runs used an isolated loopback network with MTU 1,500 B, GSO/GRO limited to one packet, tc netem and nice 10. Host network settings were unchanged.

Throughput counts echoed application payload per direction, excluding headers and ACKs. Higher is better. Tables show the median of three runs; expand the ranges to see the variation. 1 MiB = 1,048,576 B.

Implementations and versions

Column GitHub source Tested version / revision
TCP TCP echo driver, Tokio Driver 0.1.0; Tokio 1.53.1; Linux TCP stack 7.0.11-76070011-generic
rust-raknet b23r0/rust-raknet 0.16.0, transport revision 7f80c57
C KCP skywind3000/kcp Pinned commit b1a7a21; no release tag used
quic-go quic-go/quic-go v0.63.0; Go 1.27.1

TCP is a protocol, so its column identifies the benchmark driver, runtime and kernel rather than assigning TCP a project version. The TCP/KCP measurements predate the 0.16.0 version bump; the measured RakNet transport code is unchanged in revision 7f80c57.

The quic-go column was measured in a separate batch using the workloads specified in each table: a 64-message window for single connections and a 16-message window per concurrent connection. Loss traces are independent across runs.

QUIC uses one bidirectional reliable, ordered stream per connection. Records have four-byte little-endian lengths and reuse their buffers. Encryption, stream flow control and congestion control are enabled. rust-raknet and C KCP do not provide those same features. Their costs are included in the QUIC numbers, so throughput alone does not capture the difference in guarantees.

Single connection

Rust and Go drivers use four workers; C KCP uses one event loop per process. All UDP implementations have equal IPv4 packet budgets here; details are below. These numbers do not compare CPU efficiency.

Network profile Payload / burst count TCP rust-raknet C KCP quic-go
No injected loss 800 B / 200,000 messages 150.97 MiB/s 188.42 MiB/s 153.20 MiB/s 114.47 MiB/s
1% loss 800 B / 200,000 messages 20.08 MiB/s 174.84 MiB/s 143.60 MiB/s 46.00 MiB/s
5% loss 800 B / 200,000 messages 1.07 MiB/s 168.94 MiB/s 120.06 MiB/s 9.51 MiB/s
No injected loss, small messages 64 B / 300,000 messages 13.75 MiB/s 16.60 MiB/s 12.38 MiB/s 30.81 MiB/s
No injected loss, fragmented messages 4,096 B / 50,000 messages 344.51 MiB/s 316.13 MiB/s 254.99 MiB/s 173.02 MiB/s
1% loss + 5 ms each way 800 B / 3,000 messages 1.12 MiB/s 2.94 MiB/s 2.95 MiB/s 1.57 MiB/s
Network profile Payload / burst count TCP throughput (median, range) rust-raknet throughput (median, range) C KCP throughput (median, range) quic-go throughput (median, range)
No injected loss 800 B / 200,000 messages 150.97 MiB/s (150.44–153.57 MiB/s) 188.42 MiB/s (184.14–189.43 MiB/s) 153.20 MiB/s (151.82–154.32 MiB/s) 114.47 MiB/s (108.74–114.96 MiB/s)
1% loss 800 B / 200,000 messages 20.08 MiB/s (18.88–43.51 MiB/s) 174.84 MiB/s (171.90–186.40 MiB/s) 143.60 MiB/s (143.42–147.77 MiB/s) 46.00 MiB/s (43.25–46.66 MiB/s)
5% loss 800 B / 200,000 messages 1.07 MiB/s (1.02–1.08 MiB/s) 168.94 MiB/s (150.75–170.40 MiB/s) 120.06 MiB/s (118.66–122.54 MiB/s) 9.51 MiB/s (8.56–10.09 MiB/s)
No injected loss, small messages 64 B / 300,000 messages 13.75 MiB/s (13.49–13.81 MiB/s) 16.60 MiB/s (15.94–16.79 MiB/s) 12.38 MiB/s (12.18–12.42 MiB/s) 30.81 MiB/s (28.72–31.12 MiB/s)
No injected loss, fragmented messages 4,096 B / 50,000 messages 344.51 MiB/s (343.29–350.93 MiB/s) 316.13 MiB/s (313.54–317.18 MiB/s) 254.99 MiB/s (250.86–259.83 MiB/s) 173.02 MiB/s (120.29–229.28 MiB/s)
1% loss + 5 ms each way 800 B / 3,000 messages 1.12 MiB/s (1.07–1.19 MiB/s) 2.94 MiB/s (2.71–3.10 MiB/s) 2.95 MiB/s (2.86–2.97 MiB/s) 1.57 MiB/s (1.54–2.05 MiB/s)

Each run has 100 warmups, 300 sequential RTT samples and the measured burst. All drivers keep at most 64 messages awaiting verified echoes and refill on each echo. TCP / rust-raknet / C KCP run order alternates; quic-go was measured in a separate batch with the same workload. rust-raknet uses ReliableOrdered; TCP uses TCP_NODELAY, length-prefixed records, whole-record writes and a reused buffer. The official C KCP core is unchanged, pinned to b1a7a21.

The Rust processes use four Tokio workers and quic-go uses GOMAXPROCS=4; C KCP uses one event loop per process. All servers use one listening socket. Throughput runs have no CPU affinity. This is not an equal-CPU-cost comparison.

rust-raknet uses a nominal MTU of 1,428 B here, including 28 B of IPv4/UDP headers. C KCP uses a 1,400 B UDP MTU, excluding those headers. Their IPv4 packet budgets match, and both split a 4,096 B message into three fragments. The default rust-raknet MTU is still 1,400 B, which needs four fragments for that message. C KCP uses message mode, send/receive windows of 64/128 segments, nodelay(1, 10, 2, 1), immediate writes/ACKs, and no FEC or encryption. rust-raknet keeps its normal flight window and retry timings. quic-go uses a 1,400 B UDP packet size with path MTU discovery disabled, matching the 1,428 B IPv4 budget. QUIC retains its normal congestion and flow control settings.

All four implementations completed 18/18 throughput runs each. rust-raknet leads C KCP in five profiles; with delay and loss, they are effectively tied. TCP leads the 4 KiB profile, and quic-go leads the 64 B profile.

Single-connection latency

RTT is round-trip latency. Lower is better. Values are the medians of run-level p50 / p95 / p99 percentiles from three runs, each with 300 warmups and 3,000 sequential samples.

Network profile TCP RTT (p50 / p95 / p99) rust-raknet RTT (p50 / p95 / p99) C KCP RTT (p50 / p95 / p99) quic-go RTT (p50 / p95 / p99)
No injected loss, CPU affinity 16.1 µs / 75.2 µs / 87.1 µs 17.6 µs / 22.4 µs / 48.0 µs 13.8 µs / 57.5 µs / 62.0 µs 62.7 µs / 99.9 µs / 140.9 µs

Client and server are pinned to separate CPUs. Each Rust process has four workers on its assigned CPU; quic-go uses GOMAXPROCS=4 on its assigned CPU, and C KCP has one event loop. These samples precede the burst and do not measure delivery latency under sustained load.

Concurrent connections

800 B messages, a sliding window of 16 per connection, and 1,048,576 verified ordered echoes per run. All four implementations use four workers and four server sockets with SO_REUSEPORT, with separate sets of four client/server CPUs. These are transport sessions, not authenticated Minecraft players.

Connections Injected loss TCP rust-raknet C KCP quic-go
64 connections 0% 713.62 MiB/s 543.43 MiB/s 362.82 MiB/s 220.49 MiB/s
256 connections 0% 598.00 MiB/s 556.90 MiB/s 415.87 MiB/s 184.68 MiB/s
1,024 connections 0% 591.75 MiB/s 497.71 MiB/s 380.07 MiB/s 164.48 MiB/s
2,048 connections 0% 586.90 MiB/s 440.64 MiB/s 361.93 MiB/s 133.22 MiB/s
64 connections 1% 390.96 MiB/s 520.11 MiB/s 377.12 MiB/s 216.83 MiB/s
256 connections 1% 519.37 MiB/s 511.55 MiB/s 384.26 MiB/s 147.19 MiB/s
1,024 connections 1% 424.33 MiB/s 455.86 MiB/s 390.54 MiB/s 64.55 MiB/s
2,048 connections 1% 423.78 MiB/s 389.14 MiB/s 335.09 MiB/s 31.48 MiB/s

TCP leads all four clean groups. At 1% loss, rust-raknet leads TCP at 64 and 1,024 connections; TCP leads at 256 and 2,048. rust-raknet leads C KCP in all eight groups. Small differences with overlapping ranges need more runs before calling a winner.

Connections Injected loss TCP throughput (median, range) rust-raknet throughput (median, range) C KCP throughput (median, range) quic-go throughput (median, range)
64 connections 0% 713.62 MiB/s (700.90–714.45 MiB/s) 543.43 MiB/s (536.83–572.64 MiB/s) 362.82 MiB/s (361.95–377.37 MiB/s) 220.49 MiB/s (217.91–220.77 MiB/s)
256 connections 0% 598.00 MiB/s (581.63–620.48 MiB/s) 556.90 MiB/s (526.97–562.89 MiB/s) 415.87 MiB/s (329.26–424.58 MiB/s) 184.68 MiB/s (178.88–186.38 MiB/s)
1,024 connections 0% 591.75 MiB/s (562.39–594.38 MiB/s) 497.71 MiB/s (466.01–523.55 MiB/s) 380.07 MiB/s (378.09–415.04 MiB/s) 164.48 MiB/s (158.35–166.63 MiB/s)
2,048 connections 0% 586.90 MiB/s (579.19–593.85 MiB/s) 440.64 MiB/s (429.02–452.66 MiB/s) 361.93 MiB/s (310.89–376.98 MiB/s) 133.22 MiB/s (129.02–135.82 MiB/s)
64 connections 1% 390.96 MiB/s (388.32–424.96 MiB/s) 520.11 MiB/s (491.58–583.39 MiB/s) 377.12 MiB/s (358.86–406.28 MiB/s) 216.83 MiB/s (212.02–220.87 MiB/s)
256 connections 1% 519.37 MiB/s (440.41–549.12 MiB/s) 511.55 MiB/s (462.67–513.39 MiB/s) 384.26 MiB/s (372.07–406.03 MiB/s) 147.19 MiB/s (138.19–170.24 MiB/s)
1,024 connections 1% 424.33 MiB/s (416.75–478.65 MiB/s) 455.86 MiB/s (424.03–468.14 MiB/s) 390.54 MiB/s (372.44–415.87 MiB/s) 64.55 MiB/s (45.63–81.04 MiB/s)
2,048 connections 1% 423.78 MiB/s (114.57–465.28 MiB/s) 389.14 MiB/s (360.29–395.85 MiB/s) 335.09 MiB/s (323.29–356.21 MiB/s) 31.48 MiB/s (29.59–35.89 MiB/s)

Order rotates across TCP / rust-raknet / C KCP. Each connection completes 20 sequential RTT samples before a shared start barrier. Setup and those samples are outside the throughput timer; connections stay open until every burst finishes. All three implementations completed 24/24 runs each (72/72 total). The separate quic-go batch completed 24/24 concurrent runs with the same message counts, 16-message window, CPU affinity and start barriers.

TCP uses TCP_NODELAY, complete-record writes and reused buffers. Its four listeners distribute accepts; accepted streams each have their own socket. UDP sockets distribute datagrams. rust-raknet uses the default nominal MTU of 1,400 B; C KCP uses a 1,400 B UDP MTU. Their IPv4 budgets differ by 28 B, but an 800 B message fits one datagram in both. TCP can combine several records in a segment, so packet loss percentages do not imply equal lost-message counts. quic-go uses a 1,372 B UDP packet size and disables path MTU discovery, matching the default 1,400 B rust-raknet IPv4 budget. It uses four SO_REUSEPORT receive sockets and GOMAXPROCS=4 per process. Connections keep one stream open each.

The 2,048-connection TCP loss case spans 114.57–465.28 MiB/s. Random loss affects data and ACKs in both directions, and each run has a different trace. UDP buffers can also overflow under saturation, even at 0% injected loss. Check UDP error counters alongside netem counters when reproducing these results. The quic-go batch also recorded receive-buffer drops at 2,048 connections without injected loss; its 64- and 256-connection clean runs had none. CPU affinity does not reserve the CPUs exclusively.

Build instructions for the repository TCP/RakNet drivers and workload details are in the benchmark README and the C KCP adapters.

Contributing

Bug reports should include the crate version, a small reproducer and what you expected to happen. For larger changes, open an issue before writing the patch.

Work in a disposable container, VM or task copy with private caches. Keep network tests in a private network namespace. Before opening a pull request:

cargo fmt --all -- --check
cargo build --all-targets
cargo test --all-targets

Include the checks you ran. For transport or performance changes, include the workload and enough detail to reproduce it.

To bump the crate version, run python3 scripts/set-version.py NEW_VERSION in the task copy, using the next release version (for example, 0.16.0). This updates Cargo.toml, the Get Started dependency and the crate documentation together. CI checks they match.

Contributors

Thanks to everyone who has contributed commits:


Licensed under MIT. See the RakNet protocol reference. This project is not affiliated with Jenkins Software LLC or RakNet.