Skip to content

Repository files navigation

slither

crates.io docs.rs CI

Slither — encrypted UDP transport. Sealed datagrams, ordered streams, and connections that change address mid-journey.

v0.2.0 · MSRV 1.96 (edition 2024) · MIT OR Apache-2.0 · #![forbid(unsafe_code)] · wire ratified and frozen (SPEC.md) · 1 100+ tests · Linux and macOS in CI · not independently audited — see SECURITY.md.

Two peers exchange encrypted, reliable, unordered messages over UDP — plus streams and unreliable datagrams — authenticated by raw public keys. No certificates, no TLS, no PKI.

Why slither

  • A peer is its public key. No CA, no trust store, no certificate plumbing.
  • A staged accept ladder — your application inspects a claimed identity and authorises it before the second Diffie-Hellman is spent; dropping the handle is the silent reject.
  • Connections roam — an authenticated packet from a new address moves the session there; nothing unauthenticated ever does.
  • Drivable without a kernel — two pure state machines behind one Wire trait, so your tests run the real protocol in memory on a paused clock.

When not to use it

  • Need NAT traversal or relay fallback? Use iroh.
  • Have certificates, want mainstream QUIC? Use quinn.
  • Want the Noise handshake alone, no transport? Use hiss — it is what slither is built on.

Install

[dependencies]
slither = "0.3"
# `slither::channel!` expands to `::hiss::…`, so your crate needs hiss too.
hiss = { version = "0.4", default-features = false }
# slither's driver runs on YOUR runtime; these are the features it uses.
tokio = { version = "1", features = ["rt", "net", "time", "sync", "macros"] }
rand_chacha = "0.10"   # only for `SoftwareIdentity` — it takes an RNG you own
getrandom = "0.4"      # …and something to seed it from

Nothing is on by default; test-util, sink, codec and tower are opt-in, and the table is on docs.rs. rand_core must be the 0.10 line hiss names (hiss::rand_core re-exports it): two majors in one graph give an unsatisfiable CryptoRng bound, not a version error.

Requirements

slither's driver is !Send. It runs with tokio::task::spawn_local on a current-thread runtime inside a LocalSet, and no handle crosses a thread. slither::prelude::block_on is the one line that pays that tax.

If your application uses #[tokio::main] — the multi-threaded runtime — Endpoint::builder()…build() panics at runtime; run slither on its own current-thread runtime and bridge with channels. This is deliberate: it lets a hardware-backed static key — an iOS Secure Enclave SecKey, not Send — drive the handshake.

Quickstart

use slither::prelude::*;                        // 0. the golden path, one line
slither::channel! { pub MySuite<P256, ChaChaPoly, Blake2b>; }   // 1. one suite
block_on(async {                                // 2. current-thread + LocalSet
    let me: SoftwareIdentity<MySuite> = SoftwareIdentity::generate(rng())?;
    let my_key = me.public_static().clone();    // 3. hand this to the peer
    let sock = tokio::net::UdpSocket::bind("0.0.0.0:0").await?; // it's a `Wire`
    let ep = Endpoint::builder().identity(me).wire(sock).build();
    let conn = ep.connect(peer_addr, peer_key)?.await?;          // 4. dial …
    conn.send_message(b"hello").await?;
    // … or answer: accept() -> read_identity() -> authenticate() -> accept()
});
the staged accept ladder

The full worked example is examples/echo.rs — two endpoints on UDP loopback, one message, clean close (cargo run --example echo). It is compiled by every cargo test run.

Limits

  • No NAT traversal, no relays. You supply reachable addresses. set_persistent_keepalive holds a NAT binding open; it does not punch one.
  • A reliable message is at most 262 144 B (256 KiB); an unreliable datagram payload at most 1 169 B. Every wire datagram is ≤ 1 200 B, never fragmented.
  • A connection carrying no traffic dies in 25 s, in silence. Connecting ahead of need does not keep a path warm.
  • One session per peer static — reconnecting is close() then dial; and messages and streams do not mix on one connection.
  • Reliability lives inside a connection; what a dead connection had not delivered is lost. No pacing, no ECN, no PMTUD; NewReno only.

Each is stated in full, at its call site, under Before you integrate on docs.rs.

How it works

slither's architecture

slither borrows WireGuard's homework — a keyed-BLAKE2b mac1 DoS gate, fresh-ephemeral handshake retransmission, an RFC 6479 replay window, roaming and the keepalive/liveness/rekey timers — over hiss's Noise IK (P-256 / ChaCha20-Poly1305 / BLAKE2b by reference; an AES-256-GCM sibling suite is offered where the hardware carries it — 1.63× measured on Apple Silicon; no RustCrypto crates). Inside the sealed packets rides a QUIC-shaped frame layer: streams, messages, datagrams, RFC 9002 loss recovery and NewReno.

Testability

The socket sits behind a small Wire trait, so the suite runs two endpoints over an in-memory FlakyWire (loss, reorder, duplication, delay, partition, send failure) on tokio's paused clock — the 5 s / 10 s / 25 s / 90 s timers resolve in virtual time. Enable test-util to do the same in your own tests.

Status

The wire is ratified and frozen (2026/08/14): every constant, header layout, frame type and timer lives in SPEC.md, and the code follows the spec, never the reverse — see CHANGELOG.md. An independent crate: zero bubble-* deps, everything from crates.io.

License

Licensed under either of Apache-2.0 or MIT, at your option. Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

About

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages