Introduction
reliable.rs is a simple packet acknowledgement system for UDP-based protocols, written in Rust.
It's useful in situations where you need to know which UDP packets you sent were received by the other side.
It has the following features:
- Acknowledgement when packets are received
- Packet fragmentation and reassembly
- RTT, jitter and packet loss estimates
- Duplicate packets are detected and dropped
It is a faithful port of the C library reliable and is wire compatible with it: packets written by one implementation are read by the other, byte for byte.
Usage
reliable is designed to operate with your own network socket library.
First, create an endpoint on each side of the connection. In a client/server setup you would have one endpoint on each client, and n endpoints on the server, one for each client slot:
use ;
let config = Config ;
let mut endpoint = new;
Where the C library takes transmit and process packet callbacks in its config, the Rust port takes closures at the call site. Send packets through the endpoint, transmitting the framed packet (or its fragments) over your UDP socket:
endpoint.send_packet;
For each packet you receive from your UDP socket, call this on the endpoint that should receive it:
endpoint.receive_packet;
And get acks like this:
for &acked_sequence in endpoint.acks
Once you process all acks, clear them:
endpoint.clear_acks;
Or do both in one call with endpoint.drain_acks(), which yields the acked sequence numbers and leaves the array empty.
Before you send a packet, you can ask reliable what sequence number the sent packet will have:
let sequence = endpoint.next_packet_sequence;
This way you can map acked sequence numbers to the contents of packets you sent, for example, resending unacked messages until a packet that included that message was acked.
Make sure to update each endpoint once per-frame. This keeps track of network stats like latency, jitter, packet loss and bandwidth:
endpoint.update;
You can then grab stats from the endpoint:
println!;
Endpoints clean up after themselves when dropped.
Mapping from the C API
| C | Rust |
|---|---|
reliable_default_config |
Config::default() |
reliable_endpoint_create |
Endpoint::new(config, time) |
reliable_endpoint_destroy |
Drop |
reliable_endpoint_send_packet + transmit_packet_function |
endpoint.send_packet(data, transmit) |
reliable_endpoint_receive_packet + process_packet_function |
endpoint.receive_packet(data, process) |
reliable_endpoint_get_acks / clear_acks |
endpoint.acks() / endpoint.clear_acks() |
reliable_endpoint_next_packet_sequence |
endpoint.next_packet_sequence() |
reliable_endpoint_update |
endpoint.update(time) |
reliable_endpoint_rtt etc. |
endpoint.rtt() etc. |
reliable_endpoint_bandwidth |
endpoint.bandwidth() |
reliable_endpoint_counters |
endpoint.counters() |
reliable_endpoint_reset |
endpoint.reset() |
reliable_log_level / reliable_set_printf_function |
the log crate facade |
reliable_init / reliable_term / custom allocators / assert handler |
not needed in Rust |
Building and testing
cargo build
cargo test
The minimum supported Rust version is 1.88. The crate contains no unsafe code (#![forbid(unsafe_code)]) and its only dependency is the log facade.
The test suite includes the C library's tests ported one for one, plus bounded runs of its soak harness (randomly sized fragmented packets over a lossy link, contents validated) and its fuzz harness (loss, reordering, duplication, bit corruption and random packet injection over a simulated link).
The C repo's example programs are ported under examples:
cargo run --example example
cargo run --example stats
cargo run --example soak -- 8192 --quiet
cargo run --example fuzz -- 100000 12345
Wire compatibility
Wire compatibility with the C library is a locked-in invariant, not a one-time claim. The wire-compat test crate vendors the C reference implementation (pinned at 1.3.4) and links it directly into the test binary via FFI. On every push and pull request, on Linux, macOS and Windows, CI verifies that:
- A Rust endpoint and a C endpoint exchanging bidirectional traffic (regular and fragmented) deliver every payload intact and ack everything the other sent.
- A Rust endpoint pair and a C endpoint pair driven through the same deterministic exchange put byte-identical datagrams on the wire.
cargo test --manifest-path wire-compat/Cargo.toml
Fuzzing
The C repo's libFuzzer harness (fuzz_target.c) is ported as a cargo-fuzz target. The fuzz input is a script of send/inject operations against a live endpoint pair, so coverage-guided fuzzing reaches the header parser, fragment reassembly, ack processing and stale/duplicate rejection:
cargo install cargo-fuzz
cargo +nightly fuzz run fuzz_endpoint
CI runs a one minute fuzz smoke test on every push, and a scheduled workflow fuzzes for 30 minutes weekly.
Caveats
reliable is a packet acknowledgement system, not a full messaging layer. Keep the following in mind:
-
Acks accumulate until you call
clear_acks, so make sure you clear acks once you have processed them each frame. If the ack buffer fills up, additional acks are dropped and an error is logged. -
The transmit closure passed to
send_packetmust not send packets on the same endpoint: it is called synchronously while the endpoint's transmit scratch buffer is in use. Sending on a different endpoint (e.g. loopback tests) is fine. -
Endpoints are not thread safe. Use one endpoint per-thread, or protect each endpoint with your own lock.
Author
The author of this library is Glenn Fiedler.
Open source libraries by the same author include: netcode, serialize, and yojimbo
If you find this software useful, please consider sponsoring it. Thanks!