Skip to main content

Crate uniudp

Crate uniudp 

Source
Expand description

§UniUDP

Unidirectional UDP transport for Rust — send messages reliably over UDP without requiring a back-channel. UniUDP handles chunking, reassembly, redundancy, forward error correction, packet authentication, and replay protection so you can focus on your application.

License: MIT Rust

§Why UniUDP?

Standard UDP gives you raw datagrams with no delivery guarantees. TCP gives you reliable streams but requires a bidirectional connection. UniUDP sits in between: it adds reliability mechanisms to UDP while remaining fully unidirectional — the receiver never sends packets back to the sender.

This makes UniUDP ideal for:

  • Broadcast/multicast data distribution — one sender, many receivers
  • Telemetry and sensor streaming — fire-and-forget with recovery
  • Log shipping and event forwarding — reliable delivery without back-pressure
  • Network-constrained environments — where return paths are unavailable or undesirable

§Features

FeatureDescription
Chunked transportAutomatically splits messages up to 16 MB into configurable chunks (default 1024 bytes)
Packet redundancyRetransmit each packet N times to survive packet loss
Forward error correctionReed-Solomon erasure coding for multi-chunk recovery per group
Packet authenticationOptional HMAC-SHA256 per-packet auth with key ID rotation
Replay protectionSession tracking, message freshness windows, and deduplication
Source policy controlsPin receivers to specific source addresses or IPs
Resource budgetsConfigurable limits on pending messages, bytes, and sessions
Async supportOptional Tokio integration for non-blocking I/O
Thread-safe senderArc<Sender> sharing across threads with &self send methods
DiagnosticsPer-receive counters for packets, rejections, duplicates, and recoveries

§Installation

Add to your Cargo.toml:

[dependencies]
uniudp = "1"

For async/Tokio support:

[dependencies]
uniudp = { version = "1", features = ["tokio"] }
tokio = { version = "1", features = ["macros", "rt", "net"] }

uniudp’s tokio feature enables async APIs in this crate. Add a direct tokio dependency when your application uses tokio::net::UdpSocket and #[tokio::main] (as in the async example below).

§Quick Start

§Send and receive a message

use std::net::UdpSocket;
use std::time::Duration;
use uniudp::message::SourcePolicy;
use uniudp::options::{ReceiveOptions, SendOptions};
use uniudp::receiver::Receiver;
use uniudp::sender::{SendRequest, Sender};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Bind sockets
    let send_socket = UdpSocket::bind("127.0.0.1:0")?;
    let mut recv_socket = UdpSocket::bind("127.0.0.1:5000")?;

    // Create sender and receiver
    let tx = Sender::new();
    let mut rx = Receiver::new();

    // Send a message with 2x redundancy and RS FEC
    let payload = b"hello uniudp".to_vec();
    let key = tx.send_with_socket(
        &send_socket,
        SendRequest::new(recv_socket.local_addr()?, &payload)
            .with_options(
                SendOptions::new()
                    .with_redundancy(2)
                    .with_chunk_size(1024)
                    .with_fec_mode(uniudp::fec::FecMode::ReedSolomon {
                        data_shards: 4,
                        parity_shards: 2,
                    }),
            ),
    )?;

    // Receive and reassemble
    let report = rx.receive_message(
        &mut recv_socket,
        ReceiveOptions::new()
            .with_key(key)
            .with_source_policy(SourcePolicy::AnyFirstSource)
            .with_overall_timeout(Duration::from_secs(2)),
    )?;

    let data = report.try_materialize_complete()?;
    assert_eq!(data, payload);
    Ok(())
}

§Async with Tokio

use tokio::net::UdpSocket;
use std::time::Duration;
use uniudp::message::SourcePolicy;
use uniudp::options::{ReceiveOptions, SendOptions};
use uniudp::receiver::Receiver;
use uniudp::sender::{SendRequest, Sender};

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let send_socket = UdpSocket::bind("127.0.0.1:0").await?;
    let recv_socket = UdpSocket::bind("127.0.0.1:0").await?;
    let destination = recv_socket.local_addr()?;

    let tx = Sender::new();
    let mut rx = Receiver::new();
    let payload = b"hello async uniudp".to_vec();

    let key = tx.send_with_tokio_socket(
        &send_socket,
        SendRequest::new(destination, &payload)
            .with_options(SendOptions::new().with_redundancy(2)),
    ).await?;

    let report = rx.receive_message_async(
        &recv_socket,
        ReceiveOptions::new()
            .with_key(key)
            .with_source_policy(SourcePolicy::AnyFirstSource)
            .with_overall_timeout(Duration::from_secs(2)),
    ).await?;

    assert_eq!(report.try_materialize_complete()?, payload);
    Ok(())
}

§Authenticated messages

use std::net::UdpSocket;
use uniudp::auth::{PacketAuth, PacketAuthKey};
use uniudp::config::ReceiverConfig;
use uniudp::options::SendIdentityOverrides;
use uniudp::receiver::Receiver;
use uniudp::sender::{SendRequest, Sender};

// Define a shared key (32 bytes) with a key ID for rotation
let auth = PacketAuth::new(1, PacketAuthKey::from([0xAB; 32]));

// Sender: attach auth to each message
let key = tx.send_with_socket(
    &send_socket,
    SendRequest::new(destination, &payload)
        .with_identity(SendIdentityOverrides::new().with_packet_auth(auth.clone())),
)?;

// Receiver: require authentication
let mut rx = Receiver::try_with_config(
    ReceiverConfig::new().with_auth_key(auth)
)?;

The receiver can hold multiple keys simultaneously for seamless key rotation.

§Core Concepts

§Messages and Chunks

UniUDP splits each message into fixed-size chunks (default 1024 bytes), wraps each in an 84-byte header, and sends them as individual UDP datagrams. The receiver reassembles chunks back into the original message.

§Reliability Without a Back-Channel

Since there is no return path, UniUDP provides two mechanisms to handle packet loss:

  • Redundancy (with_redundancy(n)) — sends each packet n times. Simple and effective for uniform loss.
  • Reed-Solomon FEC (with_fec_mode(FecMode::ReedSolomon { data_shards, parity_shards })) — generates parity shards per group, recovering up to parity_shards missing data chunks per group.

Both can be combined. The MessageReport tells you exactly which chunks were received, lost, or recovered via FEC.

§Choosing FEC Parameters

Reed-Solomon FEC is configured with data_shards (chunks per group) and parity_shards (recovery chunks per group). More parity shards improve loss tolerance at the cost of bandwidth.

ScenarioConfigOverheadRecovers
General purposedata_shards: 4, parity_shards: 250%Up to 2 lost chunks per group
Bandwidth-constraineddata_shards: 8, parity_shards: 112.5%1 per group
High loss (>15%)data_shards: 4, parity_shards: 4100%Up to 4 per group
Maximum resiliencedata_shards: 4, parity_shards: 2 + redundancy: 23x totalFEC + full duplication

For most workloads, data_shards: 4, parity_shards: 2 is a good starting point. See Performance for detailed tuning guidance.

§Identity and Sessions

Each Sender has a 128-bit SenderId and a random session_nonce. Together with a monotonically increasing message_id, these form a MessageKey that uniquely identifies every message. The receiver uses this identity triple for deduplication, replay protection, and freshness checks.

§Source Policies

Control which network sources the receiver accepts packets from:

PolicyBehavior
AnyFirstSourceAccept from any address; pin to the first source seen
Exact(addr)Only accept from a specific SocketAddr
SameIpAccept any port from the first source’s IP

§Timeouts and Completion

Receive operations use two timeouts:

  • Inactivity timeout (default 500ms) — how long to wait with no new packets before giving up
  • Overall timeout (default 5s) — maximum total wait time

The MessageReport includes a CompletionReason (Completed, InactivityTimeout, or OverallTimeout) so your application can distinguish full delivery from partial results.

§Send Options

use std::time::Duration;
use uniudp::fec::FecMode;
use uniudp::options::SendOptions;

let options = SendOptions::new()
    .with_chunk_size(1024)      // Bytes per chunk (default: 1024)
    .with_redundancy(2)         // Send each packet 2x (default: 1)
    .with_fec_mode(FecMode::ReedSolomon { data_shards: 4, parity_shards: 2 }) // RS FEC
    .with_delay(Duration::from_micros(100));  // Inter-packet pacing delay

§Receive Options

use std::time::Duration;
use uniudp::message::SourcePolicy;
use uniudp::options::ReceiveOptions;

let options = ReceiveOptions::new()
    .with_key(key)                          // Wait for a specific message
    .with_source_policy(SourcePolicy::AnyFirstSource)
    .with_inactivity_timeout(Duration::from_millis(500))
    .with_overall_timeout(Duration::from_secs(5))
    .with_strict_rejections(false);         // true = fail-fast on bad packets

§Receiver Configuration

For advanced deployments, tune receiver resource budgets and security policies:

use std::time::Duration;
use uniudp::auth::{PacketAuth, PacketAuthKey};
use uniudp::config::{AuthMode, ReceiverConfig};
use uniudp::receiver::Receiver;

let key_v1 = PacketAuth::new(1, PacketAuthKey::from([0xAB; 32]));
let key_v2 = PacketAuth::new(2, PacketAuthKey::from([0xCD; 32]));

let config = ReceiverConfig::new()
    .with_max_pending_messages(100)            // Max concurrent incomplete messages
    .with_max_pending_bytes(128 * 1024 * 1024) // Max heap for pending state
    .with_max_receive_message_len(16 * 1024 * 1024) // Max message size (16 MB)
    .with_max_receive_chunks(4096)             // Max chunks per message
    .with_dedup_window(Duration::from_secs(10))
    .with_message_freshness_window(16_384)     // Message ID freshness distance
    .with_session_freshness_retention(Duration::from_secs(3600)) // Session state TTL
    .with_strict_message_ordering(true)        // Require message_id > max_seen
    .with_auth_mode(AuthMode::Require)         // Require packet authentication
    .with_auth_keys(vec![key_v1, key_v2]);     // Accept multiple key IDs

let mut rx = Receiver::try_with_config(config)?;

§Working with Results

use std::time::Duration;
use uniudp::message::SourcePolicy;
use uniudp::options::ReceiveOptions;
use uniudp::receiver::Receiver;

let report = rx.receive_message(&mut socket, options)?;

if report.is_complete() {
    // All chunks received — safe to materialize
    let data = report.try_materialize_complete()?;
} else {
    // Partial delivery — inspect what happened
    println!("received {}/{} chunks", report.chunks_received, report.chunks_expected);
    println!("lost chunks: {:?}", report.lost_chunks);
    println!("FEC recovered: {:?}", report.fec_recovered_chunks);
    println!("reason: {:?}", report.completion_reason);

    // Zero-fill missing chunks (lossy)
    let partial = report.materialize_payload_lossy();
}

§Diagnostics

After each receive, inspect packet-level counters:

use uniudp::receiver::Receiver;

let diag = rx.last_receive_diagnostics();
// diag.packets_received, diag.packets_accepted,
// diag.auth_rejections, diag.replay_rejections,
// diag.source_rejections, diag.duplicate_packets, ...

§API Modules

ModuleContents
uniudp::senderSender, SenderBuilder, SendRequest, SendFailure, MessageIdStart
uniudp::receiverReceiver, ReceiveLoopControl
uniudp::optionsSendOptions, ReceiveOptions, SendIdentityOverrides, ReceiveDiagnostics
uniudp::messageMessageKey, SenderId, SourcePolicy, MessageReport, CompletionReason
uniudp::configReceiverConfig, AuthMode, protocol constants
uniudp::authPacketAuth, PacketAuthKey
uniudp::packetPacketHeader, encoding/decoding, ParsedPacket
uniudp::fecFecMode, Reed-Solomon field encoding/decoding
uniudp::preludeCommon imports for quick setup

§Examples

Run the included examples:

cargo run --example basic_send_receive
cargo run --example fec_recovery
cargo run --example auth_key_rotation
cargo run --example multi_sender
cargo run --example tokio_send_receive --features tokio

§Detailed Documentation

  • Protocol — packet layout, header fields, encoding invariants
  • Security — threat model, authentication, replay protection
  • Performance — tuning chunking, redundancy, FEC, and receiver budgets

§Benchmarks

cargo bench --bench performance

Benchmarks cover packet encode/decode, FEC recovery, and end-to-end loopback throughput.

§License

MIT

Modules§

auth
config
fec
message
options
packet
prelude
receiver
sender

Structs§

ErrorDetail

Enums§

DecodeContext
EncodeContext
ReceiveRejectReason
UniUdpError
Crate error type.
ValidationContext

Type Aliases§

Result