Skip to main content

slither/
lib.rs

1//! A WireGuard-shaped Noise-over-UDP packet layer carrying a QUIC-shaped
2//! reliable frame layer.
3//!
4//! slither seals every datagram with an IK handshake over P-256 /
5//! ChaCha20-Poly1305 / BLAKE2b (driven entirely through
6//! [`hiss`](https://docs.rs/hiss)), gates initiations behind a keyed-BLAKE2b
7//! mac1, and carries streams, messages and unreliable datagrams inside the
8//! sealed plaintext with flow control, RFC 9002 loss recovery and
9//! congestion control. Connections roam across address changes, rekey by
10//! ratchet, and are accepted in *stages*, so an application can inspect a
11//! peer's claimed identity before spending a second DH on it — a ladder
12//! climbed **in a loop**, not once per connection: `accept()` is drained
13//! for the lifetime of the endpoint, by diallers and responders alike
14//! (§6.5, documentation obligation #6).
15//! slither is `#![forbid(unsafe_code)]`; every Noise and curve operation
16//! goes through `hiss`, and no RustCrypto crate appears in the graph.
17//!
18//! # Quickstart
19//!
20//! Two halves, taken one at a time. `examples/echo.rs` in the repository
21//! is the full program — both halves in one process, with an echo back —
22//! run it with `cargo run --example echo`.
23//!
24//! Both start from the same one-time declaration: a crypto suite, which
25//! every slither type is generic over. Declare it once, anywhere in your
26//! crate ([`channel!`]):
27//!
28//! ```
29//! use slither::prelude::*;
30//!
31//! slither::channel! { pub MySuite<P256, ChaChaPoly, Blake2b>; }
32//! ```
33//!
34//! That one line is the whole import surface for what follows —
35//! [`prelude`] carries the golden path, hiss's three suite types included,
36//! and everything else keeps one spelling at its module
37//! (`slither::error::ReadError`, `slither::constants::NO_ERROR`).
38//!
39//! ## 1. Listen
40//!
41//! Bind a socket, build an endpoint, and answer whoever arrives.
42//!
43//! ```no_run
44//! # use slither::prelude::*;
45//! # use rand_chacha::ChaCha20Rng;
46//! # use rand_chacha::rand_core::SeedableRng;
47//! # slither::channel! { pub MySuite<P256, ChaChaPoly, Blake2b>; }
48//! # fn rng() -> ChaCha20Rng {
49//! #     let mut seed = [0u8; 32];
50//! #     getrandom::fill(&mut seed).expect("OS entropy");
51//! #     ChaCha20Rng::from_seed(seed)
52//! # }
53//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
54//! # block_on(async {
55//! let me: SoftwareIdentity<MySuite> = SoftwareIdentity::generate(rng())?;
56//! println!("my key: {:?}", me.public_static()); // the dialler needs this, out of band
57//! let sock = tokio::net::UdpSocket::bind("0.0.0.0:51820").await?;
58//! let ep = Endpoint::builder().identity(me).wire(sock).build();
59//! // Answering is a ladder: inspect the claim, then let the peer prove it.
60//! // accept() is a loop for the endpoint's lifetime.
61//! while let Some(intro) = ep.accept().await {
62//!     let claimed = intro.read_identity().await?;   // 1 DH — still just a claim
63//!     let conn = claimed.authenticate().await?      // proven ...
64//!         .accept().await?;                          // ... and connected
65//!     println!("got: {:?}", conn.recv_message().await?);
66//! }
67//! # Ok(())
68//! # })
69//! # }
70//! ```
71//!
72//! ## 2. Dial
73//!
74//! The answerer's static key reached you out of band — slither never
75//! learns one from the wire.
76//!
77//! ```no_run
78//! # use slither::prelude::*;
79//! # use rand_chacha::ChaCha20Rng;
80//! # use rand_chacha::rand_core::SeedableRng;
81//! # slither::channel! { pub MySuite<P256, ChaChaPoly, Blake2b>; }
82//! # fn rng() -> ChaCha20Rng {
83//! #     let mut seed = [0u8; 32];
84//! #     getrandom::fill(&mut seed).expect("OS entropy");
85//! #     ChaCha20Rng::from_seed(seed)
86//! # }
87//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
88//! # block_on(async {
89//! # let peer_key = *SoftwareIdentity::<MySuite>::generate(rng())?.public_static();
90//! let me: SoftwareIdentity<MySuite> = SoftwareIdentity::generate(rng())?;
91//! let sock = tokio::net::UdpSocket::bind("0.0.0.0:0").await?;
92//! let ep = Endpoint::builder().identity(me).wire(sock).build();
93//! // connect() spends no DH; awaiting Connecting runs the handshake.
94//! let conn = ep.connect("203.0.113.7:51820".parse()?, peer_key)?.await?;
95//! conn.send_message(b"hello").await?;
96//! conn.acked().await?;                              // the peer has it
97//! conn.close(slither::constants::NO_ERROR, b"done").await;
98//! # Ok(())
99//! # })
100//! # }
101//! ```
102//!
103//! # Install
104//!
105//! ```toml
106//! [dependencies]
107//! slither = "0.3"
108//! # `slither::channel!` expands to absolute `::hiss::…` paths, so your
109//! # crate must depend on hiss directly, on the same minor line.
110//! hiss = { version = "0.4", default-features = false }
111//! # slither's driver runs on your runtime; these are the features it uses.
112//! tokio = { version = "1", features = ["rt", "net", "time", "sync", "macros"] }
113//! rand_chacha = "0.10"   # only for `SoftwareIdentity`: it takes your RNG
114//! getrandom = "0.4"      # …and something to seed it from
115//! ```
116//!
117//! `rand_core` must be the **0.10** line hiss names — [`hiss::rand_core`]
118//! re-exports it. Two `rand_core` majors in one graph produce an
119//! unsatisfiable `CryptoRng` bound, not a version error. MSRV **1.96**.
120//!
121//! # Features
122//!
123//! Nothing is on by default.
124//!
125//! | Feature | What it adds |
126//! |---|---|
127//! | `test-util` | the `testutil` module, for driving slither in a downstream crate's tests |
128//! | `sink` | `Stream` / `Sink` adapters |
129//! | `codec` | `tokio_util::codec` support; implies `sink` |
130//! | `tower` | `tower::Service` shapes — **one bi stream per call** |
131//!
132//! **[RATIFIED 2026/08/16 — ruling 225]** The `tower` row read *"a
133//! `tower::Service` shape over the message verb"*, and that shape cannot
134//! work: slither has **no request/response correlation on the wire**, so a
135//! `Service` over §11 messages would need a request id the transport does
136//! not carry — slither would have to invent application framing above its
137//! own frame layer to find one. The correlation slither already has is a
138//! **stream**: `call()` opens one bi stream, `finish()` ends the request,
139//! EOF ends the response. `PLAN.md` §3.4 is the reasoned statement and it
140//! wins; this row and `Cargo.toml`'s comment were manifest text carrying no
141//! argument.
142//!
143//! # Shape
144//!
145//! ```text
146//! core::Endpoint / core::Connection   pure state machines, no I/O, no clock
147//!         ↑ poll_output() to Timeout, after every mutating call    (§16.4)
148//! shell: one !Send driver task        owns the cores, owns the Wire (§16.3)
149//!         ↑ handles
150//! compat: AsyncRead/Write · Stream/Sink · Codec · tower            (§16.11)
151//! ```
152//!
153//! The cores never read a clock — `now: Instant` is an argument on every
154//! mutating call — and the shell is a **single `!Send` actor** run with
155//! `tokio::task::spawn_local` on a current-thread runtime inside a
156//! `LocalSet`. Nothing on that path requires `Send`, deliberately: a
157//! hardware-backed static key (an iOS Secure Enclave `SecKey`) is not
158//! `Send`, and a transport that demanded it would exclude the case the DH
159//! provider seam exists for.
160//!
161//! Because the cores are pure and [`shell::wire::Wire`] is the only I/O
162//! seam, **the whole protocol is drivable without a kernel**: two
163//! endpoints over the in-memory `testutil` fabric on tokio's paused clock,
164//! with every timer resolving in virtual time.
165//!
166//! # Before you integrate
167//!
168//! Six hazards have no code fix. A consumer meets each one by getting it
169//! wrong, so each is stated here as well as at its call site.
170//!
171//! 1. **Reconnecting is `close()` then dial, not `connect()` again.**
172//!    `connect()` to a static that already has a live connection returns
173//!    [`ConnectError::AlreadyConnected`](error::ConnectError::AlreadyConnected)
174//!    — §16.1 admits one session per peer static, and the *existing*
175//!    connection is what holds it. "Call
176//!    connect again" is the natural guess and it is wrong: it does not
177//!    replace the old connection, it does not repair a wedged one, and it
178//!    leaves the first connection completely untouched.
179//!
180//!    An application that wants *reconnect now* releases the static first
181//!    and only then dials:
182//!
183//!    ```no_run
184//!    # use slither::prelude::*;
185//!    # use std::net::SocketAddr;
186//!    # async fn reconnect<I: Identity>(
187//!    #     endpoint: &Endpoint<I>,
188//!    #     stale: Connection<I::Suite>,
189//!    #     peer: slither::identity::PublicKeyOf<I>,
190//!    #     addr: SocketAddr,
191//!    # ) -> Result<Connection<I::Suite>, slither::error::ConnectError>
192//!    # where
193//!    #     I::Suite: slither::packet::Handshake<Psk = ()>,
194//!    # {
195//!    // Wrong: the static is still LIVE, so this is `AlreadyConnected`
196//!    // and the wedged connection is still there afterwards.
197//!    //
198//!    //     endpoint.connect(addr, peer)?.await
199//!
200//!    // Right: end the old one, wait for it to be gone, then dial.
201//!    stale.close(slither::constants::NO_ERROR, b"reconnecting").await;
202//!    stale.closed().await;
203//!    drop(stale);
204//!    endpoint.connect(addr, peer)?.await
205//!    # }
206//!    ```
207//!
208//!    The `closed().await` is not decoration: `close()` returns once the
209//!    CLOSE is sealed, and the static is released when the connection's
210//!    state is actually dropped (§16.4's `Retired`). Dialling before then
211//!    races the release.
212//! 2. **Do not punish on evidence a third party can manufacture.**
213//!    Authorise on it; do not punish on it. The rule is one sentence and it
214//!    reaches three rungs of the handshake, because *"an attacker gets an
215//!    innocent peer banned"* is the same hazard at all three.
216//!
217//!    - **A *claimed* static is not an authenticated one.** The identity
218//!      `read_identity()` reveals during a staged accept is an
219//!      **unauthenticated assertion**, made before any DH proves
220//!      possession. Denylisting on it lets an attacker claim any public key
221//!      in order to get its owner banned.
222//!    - **The source address and `sender_index` are worse, not better.**
223//!      §6.1 forbids durable state keyed on **three** quantities — the
224//!      claimed static, the source address, and `sender_index` — and the
225//!      two beside the static are the *cheaper* keys to abuse: a spoofed
226//!      source costs an attacker no DH at all, needs no knowledge of
227//!      anyone's public key, and has no return-routability proof at stage
228//!      0. A source-address denylist under the flood §6.3 describes bans
229//!      spoofed victims. See [`Intro::source`](shell::Intro::source) and
230//!      [`Intro::sender_index`](shell::Intro::sender_index).
231//!    - **A *proven* static does not make the accusation true.**
232//!      [`AuthError::Replay`](error::AuthError::Replay) is delivered after
233//!      the `ss` has genuinely proven the static, which is exactly what
234//!      makes it look like trustworthy evidence about that peer. It is not: one captured
235//!      initiation lets a third party produce it at will, from any address,
236//!      against a peer that has done nothing. It reports *this initiation
237//!      is not fresh*, never *this peer misbehaved*.
238//! 3. **A connection with nothing to say dies — in 25 s by default, in
239//!    silence.**
240//!    A connection that has received **no authenticated packet since it was
241//!    installed** transmits *nothing at all* and is torn down at
242//!    the representable deadline at install + its effective dead timeout
243//!    (`DEAD_TIMEOUT`, 25 s, under the v1 profile) with
244//!    [`ConnectionLost::TimedOut`](error::ConnectionLost::TimedOut).
245//!    **Connecting ahead of need does not keep a path warm**, and this is
246//!    the single most surprising behaviour for a new consumer.
247//!
248//!    §7.5's keepalive dance is **automatic for any connection that has
249//!    carried traffic**: one application message, in **one** direction,
250//!    puts the receiver into the state that makes it answer every effective
251//!    passive interval (10 s under v1), which puts the sender into it, and
252//!    the pair then sustains itself indefinitely without a per-connection
253//!    opt-in. [`config::TimingProfile`] may change that endpoint-wide
254//!    cadence and its dead timeout together; peers do not negotiate it.
255//!    Every deadline calculation is checked: one beyond the platform clock
256//!    horizon remains logically enabled but is never replaced with an
257//!    invented earlier timeout.
258//!
259//!    The knob is for the case that leaves out — a link that is **mutually
260//!    idle** and must nonetheless stay open, through a NAT binding or a
261//!    firewall's idle reaper.
262//!    [`Connection::set_persistent_keepalive`](shell::Connection::set_persistent_keepalive)
263//!    takes an interval in `[1 s, effective dead timeout)` and rejects
264//!    anything outside it rather than clamping. Enabling it on **one** side
265//!    is enough: the beacon reaches the peer, and the peer's automatic half
266//!    answers.
267//!
268//!    A beacon does not defer death, and is not meant to. Both keepalives
269//!    are *marking* sends, so they **arm** the death clock; a connection
270//!    whose beacons are never answered still ends one effective dead timeout
271//!    after the last authenticated packet it received. Two consecutive lost
272//!    beacons at the v1 10 s recommendation is what the default costs.
273//! 4. **Teardown triggers on dropping every *handle*, not the endpoint.**
274//!    The connection lives as long as any handle to it does, and ends when
275//!    the last one is dropped — the opposite of the obvious guess, and
276//!    sensitive to the order your values fall out of scope.
277//! 5. **Messages and streams do not mix on one connection.** Using
278//!    `send_message` alongside `open_uni` on the same connection is a
279//!    programming error with a defined, loud failure. The safe and unsafe
280//!    shapes look alike at the call site, which is exactly why it is
281//!    written down.
282//! 6. **`accept()` is a loop for the lifetime of the endpoint, not one
283//!    call per connection.** §6.5 puts it as a SHOULD: *"Every application
284//!    SHOULD treat `accept()` as a loop for the lifetime of its endpoint —
285//!    diallers and responders alike."* Accepting once and moving on is the
286//!    natural shape, and it strands the two cases the protocol expects the
287//!    *next* `accept()` to repair.
288//!
289//!    - **A lost msg2 leaves a responder holding a connection the peer
290//!      knows nothing about.** msg2 is never retransmitted — every
291//!      retransmit is a *completely fresh initiation* (§5.5) — so one
292//!      dropped msg2 leaves this side with a live, never-confirmed
293//!      connection while the peer re-offers a fresh [`Intro`](shell::Intro)
294//!      every `RETRANSMIT_BASE` (~5 s) until it gives up at
295//!      `HANDSHAKE_GIVEUP` (90 s). **No error ever prompts the retry**: the first `accept()`
296//!      *succeeded*. Only the next one closes the gap.
297//!    - **A restarted peer is the same shape (§6.8).** Its reconnection
298//!      parks as an ordinary `Intro` against our still-live static, the
299//!      now-zombie connection keeps running untouched, and nothing tears
300//!      it down until the replacing `accept()`. *"Restart needs no
301//!      machinery of its own"* is true only because the application is
302//!      still listening.
303//!
304//!    Admitting that fresh `Intro` **is** the replacement, by §6.4's §16.1
305//!    guard. Where the connection it displaces is one we **accepted** —
306//!    replacement basis `Some(t)`, and the new initiation's timestamp
307//!    strictly greater — the install fires
308//!    [`ConnectionLost::Replaced`](error::ConnectionLost::Replaced) on the
309//!    old connection and the new chain completes. Where it is one we
310//!    **dialled**, the basis is `None`, no initiation can replace it,
311//!    [`AcceptError::Stale`](error::AcceptError::Stale) comes back and
312//!    §6.8's restart instead resolves at the next representable liveness
313//!    deadline, ordinarily at most the effective dead timeout later. Either
314//!    way the application
315//!    side of it is the one instruction: keep accepting.
316//!
317//! # The spec is the authority
318//!
319//! **`SPEC.md` is the authority.** Every constant, header layout, frame
320//! type and timer in this crate is ratified there, and where the code and
321//! the spec disagree the spec is right. The module layout is deliberately
322//! one-to-one with the spec's sections so a reviewer can find the code for
323//! a section without searching.
324//!
325//! # Modules
326//!
327//! - [`prelude`] — the golden path in one glob: `use slither::prelude::*;`.
328//!   The root itself carries only [`hiss`], [`SessionId`], [`Dir`],
329//!   [`StreamId`] and [`Timestamp`]; every other name has exactly one
330//!   spelling, at its module (ruling 278).
331//! - [`identity`] — the static-key seam. A consumer implements
332//!   [`Identity`](identity::Identity) to put its key behind hardware;
333//!   [`SoftwareIdentity`](identity::SoftwareIdentity) is the in-memory
334//!   default.
335//! - [`shell`] — the I/O shell: [`Endpoint`](shell::Endpoint),
336//!   [`Connection`](shell::Connection), the staged accept ladder, the
337//!   stream handles, and [`shell::wire::Wire`] — the datagram seam an
338//!   application supplies.
339//! - [`config`] — endpoint configuration and §16.5's injected wall clock.
340//! - [`error`] — the closed error taxonomy of §18.1, plus `ConfigError`.
341//!   **Module-only** (ruling 278): its ten types are named
342//!   `slither::error::ReadError` and are deliberately not in the prelude —
343//!   bare-minimal code never spells one, since `?` into
344//!   `Box<dyn std::error::Error>` covers the quickstart.
345//! - [`packet`] — §2–§5's wire: the suite declaration, §6.1's handshake
346//!   ladder as a trait, the three headers, mac1 and §3.1's gate.
347//! - [`constants`] — every named constant the spec fixes, one home, with
348//!   the derived ones re-derived as compile-time assertions.
349//! - [`compat`] — §16.11's composability surface: `AsyncRead`/`AsyncWrite`
350//!   on the stream handles and the two `io::Error` conversions (ungated),
351//!   plus `Stream`/`Sink`, `tokio_util::codec` and `tower::Service` faces
352//!   behind their features. It adds **no verb and no state**.
353//! - `testutil` — the in-memory `Network` / `FlakyWire` / `FlakyPolicy`
354//!   fabric and the counting DH provider, behind the `test-util` feature.
355//!   Attested surface (ruling 60), not a test convention: it is
356//!   deterministic under a caller-supplied seed, and renaming one of those
357//!   three types is a protocol revision.
358//! - `core` — §16.4's two sans-io state machines. Crate-internal:
359//!   nothing outside the crate drives them directly.
360
361#![forbid(unsafe_code)]
362#![warn(missing_docs)]
363
364pub mod compat;
365pub mod config;
366pub mod constants;
367pub mod error;
368pub mod identity;
369pub mod packet;
370pub mod prelude;
371pub mod shell;
372pub(crate) mod varint;
373
374// §16.4's two cores. `pub(crate)` deliberately, as an API position — not a
375// slice artefact (ruling 277 swept this comment's original "until the
376// driver lands" clause; the driver landed): §16.6 makes a build that
377// accepts a caller-chosen RNG seed "security-relevant", and that seam
378// stays unpublished while the crate is unaudited. Promotion later is
379// additive; demotion is breaking.
380//
381// NOTE for every file in this crate: a crate-level `mod core` makes a bare
382// `use core::…` ambiguous against the `core` crate in the extern prelude.
383// Write `::core::…` for the language core and `crate::core::…` for this
384// module — the convention the packet layer already follows.
385pub(crate) mod core;
386
387#[cfg(any(test, feature = "test-util"))]
388pub mod testutil;
389
390/// A completed session's channel binding — **hiss's type, re-exported**.
391///
392/// **[RATIFIED 2026/08/15 — ruling 89]** `SessionId` is
393/// [`hiss::noise::SessionId`] and not a slither wrapper: hiss derives it
394/// from the handshake hash, both peers of a session produce the same value,
395/// and it is a *public* channel-binding value meant for out-of-band
396/// comparison — logging it, or a short-authentication-string check between
397/// peers. A slither wrapper would be a type that must be kept equal to
398/// hiss's by hand, for no gain.
399///
400/// # Its `Eq` is not constant-time
401///
402/// By hiss's deliberate choice. It carries no secret material and **must
403/// not be used to compare one**: reaching for it as a session token or an
404/// authentication comparison is the mistake this paragraph exists to
405/// prevent.
406///
407/// Read it off a live connection with
408/// [`Connection::session_id`](shell::Connection::session_id).
409pub use hiss::noise::SessionId;
410
411// The three identifiers §16.4's surface names that a consumer must be able
412// to spell: [`Dir`], [`StreamId`] and [`Timestamp`]. They live inside the
413// `pub(crate)` core, so they are re-exported here to be publicly
414// *reachable* — `WallClock::now()` returns a [`Timestamp`], and a public
415// signature naming an unreachable type is a rustdoc break, not merely a
416// lint.
417//
418// **[RATIFIED 2026/08/18 — ruling 259(iii)]** `ConnectionId` and `IntroId`
419// were re-exported here too, which made the count wrong and the surface
420// dead: neither appears in any public signature and neither has a public
421// constructor or accessor, so nothing a consumer can write names them. The
422// re-export bought nothing and would have frozen two types under semver.
423// They stay `pub(crate)`; the comment now names the three it counts.
424pub use crate::core::{Dir, StreamId, Timestamp};
425
426/// The `hiss` slither was built against, re-exported so the version you
427/// must match is findable.
428///
429/// [`channel!`] expands to absolute `::hiss::…` paths — `hiss::noise!`
430/// emits them and a `macro_rules` wrapper cannot rewrite them — so a crate
431/// that invokes it **must depend on `hiss` itself**, on the same minor
432/// line slither does:
433///
434/// ```toml
435/// [dependencies]
436/// slither = "0.3"
437/// hiss = { version = "0.4", default-features = false }
438/// ```
439///
440/// **This re-export does not remove that requirement.** It exists so the
441/// version can be read off slither's own docs, and so a type that crosses
442/// the boundary (a `Curve::PublicKey`, a `DhProvider`) can be named
443/// through one path when you would rather not name two.
444pub use hiss;