slither 0.3.0

Encrypted peer-to-peer UDP transport: reliable messages, streams and datagrams, authenticated by raw public keys - no certificates, no TLS. WireGuard-shaped handshake, QUIC-shaped frames.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
//! §16.11's service face — `feature = "tower"`.
//!
//! Three shapes over [`tower_service::Service`]: the dialer on
//! [`Endpoint`], the stream-opener on `&`[`Connection`], and [`serve`] for
//! the accepting side.
//!
//! # One bi stream per call — **ruling 225**
//!
//! slither has **no request/response correlation on the wire**, so a
//! `Service` over §9.8's message verb would need a request id the transport
//! does not carry — slither would have to invent application framing above
//! its own frame layer to find one, which would be the first place the crate
//! defined wire semantics above §8. The correlation slither already has is a
//! **stream**: `call()` opens one bi stream, `finish()` ends the request and
//! EOF ends the response.
//!
//! There is **no `Rpc`** (ruling 230): it is cut from this slice, nothing
//! here carries a codec bound, and the `tower` feature stays the cheap
//! `tower-service` dependency with no `tokio-util` in it.
//!
//! # The `!Send` caveat, stated rather than discovered — **S33**
//!
//! §16.3's driver is `!Send` **by requirement**, not by accident, and S21 is
//! why: a hardware-backed static key (an iOS Secure Enclave `SecKey`) is not
//! `Send`, and a transport that demanded it would exclude the case the DH
//! provider seam exists for.
//!
//! This surface composes regardless. [`tower_service::Service`] itself,
//! `tokio_util::codec`, `futures`' `Stream`/`Sink` combinators and
//! `tokio::io::copy` all carry **no `Send` bound**.
//!
//! What does **not** compose is anything that spawns onto a work-stealing
//! executor:
//!
//! - `tower::buffer::Buffer` and `tower::util::spawn_ready`;
//! - `tower::util::BoxService` — use **`UnsyncBoxService`** instead;
//! - hyper;
//! - plain `tokio::spawn` on any slither handle. `spawn_local` is the
//!   substitute, and it is what [`serve`] uses.
//!
//! A `Send` façade over a driver on its own thread would close that gap and
//! is **deliberately out of v0.2 scope**: it re-crosses the core→shell seam
//! with channels. The handle shapes are already compatible with adding one
//! later without a breaking change.

use std::fmt;
use std::future::Future;
use std::net::SocketAddr;
use std::pin::Pin;
use std::task::{Context, Poll};

use tower_service::Service;

use crate::core::Dir;
use crate::error::{ConnectError, ConnectionLost};
use crate::identity::{Identity, PublicKeyOf};
use crate::packet::Handshake;
use crate::shell::{BiStream, Connecting, Connection, Endpoint, WakerSlot};

// ═══════════════════════════════════════════════════════════════════════
// The dialer
// ═══════════════════════════════════════════════════════════════════════

/// `Endpoint` as a dialer: request is *(address, peer static)*, response is
/// the established [`Connection`].
///
/// This can be implemented **on the owned type** — unlike the stream-opener
/// below — because [`Endpoint::connect`] is *not* `async` and the
/// [`Connecting`] it returns owns everything it needs: it borrows nothing
/// from `&self`, so the future can outlive the `&mut self` that `call` is
/// given.
///
/// # Only for suites whose pattern has no pre-shared key
///
/// The request type is *(address, peer static)*, and there is nowhere in it
/// to put a per-peer PSK. That is a fact about `Service`, not an oversight:
/// widening the request to a triple would change this impl's shape for every
/// `IK` consumer to carry a `()`, and a
/// [`channel_psk!`](crate::channel_psk) endpoint's dial is
/// [`Endpoint::connect_with`] — a verb with an argument tower has no slot
/// for. A psk suite composes with tower at the *stream* layer below, which
/// takes an established connection and never dials (ruling 280).
impl<I: Identity> Service<(SocketAddr, PublicKeyOf<I>)> for Endpoint<I>
where
    I::Suite: Handshake<Psk = ()>,
{
    type Response = Connection<I::Suite>;
    type Error = ConnectError;
    type Future = Connect<I>;

    /// Always ready. An endpoint has no admission control of its own — the
    /// one-connection-per-static test is §16.1's and it is answered inside
    /// `call`, synchronously, from the endpoint core's own static map.
    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        Poll::Ready(Ok(()))
    }

    fn call(&mut self, (remote, remote_static): (SocketAddr, PublicKeyOf<I>)) -> Self::Future {
        Connect {
            state: match self.connect(remote, remote_static) {
                Ok(connecting) => ConnectState::Dialling(connecting),
                Err(err) => ConnectState::Failed(Some(err)),
            },
        }
    }
}

/// The future [`Endpoint`]'s [`Service`] returns.
///
/// It exists to fold `connect`'s **synchronous** error into a future.
/// [`ConnectError::AlreadyConnected`] arrives *before any await* (rulings 87
/// and 90: `mint_pending` costs 0 DH and runs in the verb itself), and
/// [`Service::call`] cannot return a `Result`.
///
/// On the success path it **owns the [`Connecting`]**, not a copy of its
/// parts. That is load-bearing: dropping a `Connecting` is ruling 50's
/// cancellation, which retires the pending in the endpoint core's static map
/// in the same instant, so a dropped `Service` future cancels exactly as
/// `drop(connecting)` does — including the *"succeeds on the very next
/// line"* retry that ruling 50 makes structural.
pub struct Connect<I: Identity> {
    state: ConnectState<I>,
}

enum ConnectState<I: Identity> {
    Dialling(Connecting<I>),
    /// `connect()` failed synchronously. `Option` so the error can be moved
    /// out on the one poll that consumes it.
    Failed(Option<ConnectError>),
}

impl<I: Identity> Future for Connect<I> {
    type Output = Result<Connection<I::Suite>, ConnectError>;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        match &mut self.get_mut().state {
            // `Connecting` is a plain struct of `Rc`s and a `ConnectionId`,
            // so it is `Unpin` and needs no projection.
            ConnectState::Dialling(connecting) => Pin::new(connecting).poll(cx),
            ConnectState::Failed(err) => Poll::Ready(Err(err
                .take()
                .expect("a Connect future was polled after it completed"))),
        }
    }
}

/// **[RATIFIED 2026/08/18 — ruling 259(v)]** C-DEBUG. Hand-written for the
/// reason `compat::stream`'s eight are: a derive would emit
/// `impl<I: Identity + Debug>`, and §16.2's surface must be `Debug` for
/// every identity.
///
/// The state is worth naming — *did `connect()` already fail
/// synchronously?* is the question this future exists to answer, and it
/// carries no key material either way.
impl<I: Identity> fmt::Debug for Connect<I> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let state = match &self.state {
            ConnectState::Dialling(_) => "dialling",
            ConnectState::Failed(_) => "failed",
        };
        f.debug_struct("Connect")
            .field("state", &state)
            .finish_non_exhaustive()
    }
}

// ═══════════════════════════════════════════════════════════════════════
// The stream-opener
// ═══════════════════════════════════════════════════════════════════════

/// `&Connection` as a stream-opener: `call(())` opens one bidirectional
/// stream, and **the stream is the correlation** (ruling 225).
///
/// # Why the receiver is `&'a Connection<S>` and not `Connection<S>`
///
/// `PLAN.md` §3.4 sketches `impl Service<()> for Connection`, and
/// `CONTRACT-8.md` §0.0 repeats that sketch when restating ruling 225. **The
/// sketched form cannot be written.** [`Connection::open_bi`] takes `&self`,
/// so the future it produces borrows the connection; but
/// [`Service::call`] is `call(&mut self, req)` and hands the body an
/// *anonymous* lifetime, while `type Future` is an associated type that has
/// no way to name it — `Service` has no GAT. Implementing on
/// `&'a Connection<S>` puts the lifetime in `Self`, where the associated
/// type can name it.
///
/// Ruling 225 decided *which verb* the service face is built over — one bi
/// stream per call rather than the message verb — and says nothing about the
/// receiver; this is the shape that carries that decision into code.
/// `&'a Connection<S>` is `Copy`, so `call(&mut self, ..)` moves it into the
/// future freely, and a consumer writes:
///
/// ```text
/// let mut svc = &conn;          // or: (&conn).oneshot(())
/// let stream = svc.call(()).await?;
/// ```
///
/// `S: 'static` is ruling 228's bound on the waker slot `OpenBi` holds, not
/// a new one: it restricts nothing that can exist, because a `Connection<S>`
/// is only reachable through an `Endpoint<I>` and `EndpointBuilder::build`
/// already requires `I: 'static`.
impl<'a, S: Handshake + 'static> Service<()> for &'a Connection<S> {
    type Response = BiStream<S>;
    type Error = ConnectionLost;
    type Future = OpenBi<'a, S>;

    /// Always ready. §10.4's cumulative stream limit is **not** admission
    /// control that belongs here: ruling 101 makes an exhausted stream space
    /// a *park* rather than a failure, so the waiting happens in the future.
    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        Poll::Ready(Ok(()))
    }

    fn call(&mut self, _req: ()) -> Self::Future {
        // `&'a Connection<S>` is `Copy`, so the reference comes out of
        // `&mut self` with its `'a` intact and moves into the future. That
        // is the whole reason the impl is on the reference type.
        let conn = *self;
        OpenBi {
            conn,
            slot: conn.opener_slot_boxed(Dir::Bi),
        }
    }
}

/// The future `&`[`Connection`]'s [`Service`] returns: one
/// [`open_bi`](Connection::open_bi).
///
/// Cancel-safe on `open_bi`'s terms — a dropped future has opened no stream,
/// because the index and the handle are taken in one synchronous step.
pub struct OpenBi<'a, S: Handshake> {
    conn: &'a Connection<S>,
    /// Held for the future's whole life and released on drop, which is the
    /// whole of `poll_open_bi`'s cancel-safety (ruling 228).
    slot: WakerSlot<Box<dyn FnMut(u64)>>,
}

impl<S: Handshake> Future for OpenBi<'_, S> {
    type Output = Result<BiStream<S>, ConnectionLost>;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        let this = self.get_mut();
        this.conn.poll_open_bi(cx, this.slot.key())
    }
}

/// **[RATIFIED 2026/08/18 — ruling 259(v)]** C-DEBUG. Hand-written: the
/// `WakerSlot` holds a `Box<dyn FnMut(u64)>`, which no derive can print.
impl<S: Handshake> fmt::Debug for OpenBi<'_, S> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("OpenBi").finish_non_exhaustive()
    }
}

/// The dialling side on the **owned** handle — the form `UnsyncBoxService`
/// and every other `'static` combinator require.
///
/// # Why both this and the `&Connection` impl exist
///
/// **[RATIFIED — ruling 239; this reverses ruling 236.]** `CONTRACT-8.md`
/// §6.2 held that the owned form *"cannot be written"*, because
/// [`Connection::open_bi`] takes `&self` and [`Service::call`] hands the body
/// an anonymous lifetime that `type Future` cannot name. **The premise is
/// sound and the conclusion does not follow**: the future does not have to
/// borrow. It can *own* a second handle, and an in-crate `clone_handle`
/// mints one with the same accounting `Drop` reverses, so nothing about
/// §16.2's last-handle rule moves.
///
/// The reason it must exist is a bound neither §6.2 nor ruling 236 looked at:
/// `UnsyncBoxService::new<S>` requires `S: Service<..> + 'static`, and
/// `&'a Connection<S>` is not `'static`. **S33's acceptance clause — *"an
/// `UnsyncBoxService` composes"* — is unsatisfiable on the borrowed form**,
/// and a story is the acceptance criterion.
///
/// # The hazard, stated because it is silent
///
/// `ServiceExt::oneshot` takes `self` **by value**. On this impl
/// `conn.oneshot(())` therefore **moves the connection into the combinator**,
/// and dropping the resulting future runs §16.2's last-handle rule —
/// `close(NO_ERROR, "")`. That is a closed connection from a call that looks
/// like a request. Reach for `(&conn).oneshot(())`, which moves a `Copy`
/// reference and leaves the handle where it is, or call
/// `svc.call(())` directly.
impl<S: Handshake + 'static> Service<()> for Connection<S> {
    type Response = BiStream<S>;
    type Error = ConnectionLost;
    type Future = OpenBiOwned<S>;

    /// Always ready — see the `&Connection` impl for why §10.4's stream
    /// limit is not admission control.
    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        Poll::Ready(Ok(()))
    }

    fn call(&mut self, _req: ()) -> Self::Future {
        let conn = Box::new(self.clone_handle());
        let slot = conn.opener_slot_boxed(Dir::Bi);
        OpenBiOwned { conn, slot }
    }
}

/// The future [`Connection`]'s owned [`Service`] returns: one
/// [`open_bi`](Connection::open_bi), over a handle the future owns.
///
/// Cancel-safe on `open_bi`'s terms, and handle-safe on §16.2's: the handle
/// inside is accounted, so dropping this future decrements the count it
/// incremented and never triggers the last-handle close while the caller
/// still holds one.
pub struct OpenBiOwned<S: Handshake> {
    /// **Boxed deliberately.** `Pin::get_mut` in `poll` needs `Self: Unpin`,
    /// and `Connection<S>` is `Unpin` only if `S`'s public-key type is —
    /// which is an associated type with no such bound. `Box<T>` is `Unpin`
    /// unconditionally, and this module is `#![forbid(unsafe_code)]`, so the
    /// projection alternatives are closed. One allocation per `call()`,
    /// against opening a stream.
    conn: Box<Connection<S>>,
    /// Held for the future's whole life and released on drop (ruling 228).
    slot: WakerSlot<Box<dyn FnMut(u64)>>,
}

impl<S: Handshake> Future for OpenBiOwned<S> {
    type Output = Result<BiStream<S>, ConnectionLost>;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        let this = self.get_mut();
        this.conn.poll_open_bi(cx, this.slot.key())
    }
}

/// **[RATIFIED 2026/08/18 — ruling 259(v)]** C-DEBUG, [`OpenBi`]'s reason.
/// The owned handle is printed through `Connection`'s own `Debug`, which
/// already withholds the peer static and the session id.
impl<S: Handshake> fmt::Debug for OpenBiOwned<S> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("OpenBiOwned")
            .field("conn", &self.conn)
            .finish_non_exhaustive()
    }
}

// ═══════════════════════════════════════════════════════════════════════
// The accepting side
// ═══════════════════════════════════════════════════════════════════════

/// Serve `svc` over every bidirectional stream the peer opens, until the
/// connection dies.
///
/// One task per accepted stream, spawned with
/// [`spawn_local`](tokio::task::spawn_local) — **never `tokio::spawn`**,
/// which is S21 and the whole of the `!Send` actor path. That is what the
/// `'static` bounds are for: `spawn_local` needs the spawned future to
/// outlive any borrow. **No `Send` bound appears anywhere on this path.**
///
/// # It returns `ConnectionLost`, not `Result<(), ConnectionLost>`
///
/// `PLAN.md` §3.4 writes the `Result`, and `CONTRACT-8.md` §6.3 flags this
/// as a deliberate deviation: the loop **has no success exit**. It runs
/// until `accept_bi()` fails, and the only way `accept_bi()` can fail is the
/// connection dying. A `Result` whose `Ok` is unreachable is a shape rather
/// than information, so the death reason is returned directly.
///
/// # The service's own errors are the service's
///
/// `Svc::Response` and `Svc::Error` are unconstrained and both are
/// **discarded**. `serve` does not trace them: §18.2's trace targets are a
/// **closed list of exactly five**, and adding one is a protocol revision.
/// A service that wants its failures recorded records them itself.
///
/// # Readiness
///
/// Each accepted stream gets its own clone, and that clone is driven to
/// [`poll_ready`](Service::poll_ready) inside its task before
/// [`call`](Service::call) — `tower`'s protocol requires it, and a service
/// that is never ready simply never runs its call. This is
/// `ServiceExt::oneshot`'s shape, written out because slither does not
/// depend on `tower`'s `util` feature.
///
/// # Example shape
///
/// ```text
/// let lost = slither::compat::serve(&conn, my_service).await;
/// tracing::info!(%lost, "connection ended");
/// ```
pub async fn serve<H, Svc>(conn: &Connection<H>, svc: Svc) -> ConnectionLost
where
    // `'static` is `spawn_local`'s requirement on the spawned future, which
    // captures the accepted `BiStream<H>`. It is **not** a `Send`
    // requirement — the same pair of bounds `EndpointBuilder::build` carries
    // for the driver task.
    H: Handshake + 'static,
    Svc: Service<BiStream<H>> + Clone + 'static,
    Svc::Future: 'static,
{
    loop {
        let stream = match conn.accept_bi().await {
            Ok(stream) => stream,
            Err(lost) => return lost,
        };
        let mut svc = svc.clone();
        tokio::task::spawn_local(async move {
            if std::future::poll_fn(|cx| svc.poll_ready(cx)).await.is_err() {
                return;
            }
            let _ = svc.call(stream).await;
        });
    }
}