rings_node/extension/transport/mod.rs
1#![warn(missing_docs)]
2//! Transport relay — one abstraction for TCP, HTTP and (future) UDP.
3//!
4//! # Why these are *not* three protocols
5//!
6//! The rings overlay (DHT + swarm + backend envelopes) already provides a **reliable,
7//! ordered, bidirectional message channel between two DIDs** — call it the *virtual
8//! circuit*. TCP / HTTP / UDP "services" are all the **same thing**: a *relay* that
9//! maps a local I/O resource (a socket) onto that virtual circuit. They differ only in
10//! the shape of the local resource, along three axes:
11//!
12//! ```text
13//! axis TCP HTTP UDP
14//! ---------------------- ----------------- --------------------- ------------------
15//! session cardinality ω (endless stream) 1 (one req/resp, 0 (no session,
16//! affine: Req ⊸ Resp) datagrams)
17//! framing byte stream HTTP messages datagrams
18//! lifecycle open → data* → close open → 1×req → 1×resp none
19//! → close
20//! ordering / reliability ordered, reliable ordered, reliable unordered, lossy
21//! (semantics chosen
22//! when tunnelled)
23//! ```
24//!
25//! Categorically they are one structure at three points of a single "session
26//! cardinality" axis:
27//!
28//! - **TCP** = a bidirectional byte **stream** — the cofree stream / a long-lived
29//! process; cardinality **ω**.
30//! - **HTTP** = the **affine** degeneration of TCP: exactly one exchange
31//! `Request ⊸ Response` (a use-once session); cardinality **1**.
32//! - **UDP** = the **0-session** degeneration: `Datagram → [Datagram]`, a discrete
33//! transducer with no lifecycle; cardinality **0**.
34//!
35//! So adding UDP later is not a fourth subsystem — it is this axis taken to 0.
36//!
37//! # How it sits on the effect base (`backend::ext`)
38//!
39//! Pure/effect separation is preserved:
40//!
41//! - The **interpreter owns the live resources** (the `TcpStream` / `UdpSocket`), keyed
42//! by [`SessionId`], in a resource table. These are non-purifiable OS handles and so
43//! live only in the imperative shell — never in a protocol's state.
44//! - A protocol's **pure `step`** holds only session *metadata* (which `SessionId` maps
45//! to which peer/service, framing state, counters) — never a live socket.
46//! - Generic transport **effects** (run by the interpreter): stream ops
47//! `Connect` / `Write` / `Close`; datagram ops `Bind` / `SendTo`.
48//! - Local reads / accepts **re-inject** [`Frame`]s as events (the event trace of the
49//! effect monad): the read task feeds `Data` / `Close` / `Datagram` back through the
50//! router → `step` → an `Effect::Send` over the virtual circuit.
51//!
52//! TCP / HTTP / UDP are then thin instances over this one relay: TCP uses the stream
53//! ops with an ω session; HTTP adds "one request → one response → close" session logic
54//! (expressible purely in `step`); UDP uses only the datagram ops with no session.
55
56// The relay's imperative resource tables are private to the relay interpreter — not a public
57// API. Reachable in-crate by the relay extension only.
58#[cfg(feature = "node")]
59pub(crate) mod engine;
60#[cfg(feature = "browser")]
61pub(crate) mod wt;
62
63use bytes::Bytes;
64use rings_core::dht::Did;
65use serde::Deserialize;
66use serde::Serialize;
67
68/// Identifier of a relayed session/flow (a virtual circuit ↔ local socket pairing).
69///
70/// TCP uses it for a connection; UDP uses it for a *flow* (a NAT-like mapping that
71/// routes responses back to the right local client) — see [`TransportKind`].
72#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
73pub struct SessionId(pub u64);
74
75/// Which end **opened** a relay session, from the perspective of the node holding the key.
76///
77/// Necessary because two nodes that simultaneously open a tunnel to each other both mint
78/// `SessionId(0)`: without an initiator, "the session I opened to peer B" and "the session B
79/// opened to me" would collide on `(peer=B, namespace, session=0)`, and a wire `Data(0)`
80/// would be ambiguous. The initiator splits the id space into two halves per `(peer,
81/// namespace)`.
82#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
83pub enum Initiator {
84 /// This node opened the session (a client tunnel).
85 Local,
86 /// The peer opened the session (this node is the server).
87 Remote,
88}
89
90impl Initiator {
91 /// The other end's view of the same session.
92 pub fn flip(self) -> Self {
93 match self {
94 Initiator::Local => Initiator::Remote,
95 Initiator::Remote => Initiator::Local,
96 }
97 }
98}
99
100/// A relay session's full identity — the unit used to key live sessions and to address
101/// transport effects.
102///
103/// A bare [`SessionId`] is **not** a valid address: the id on the wire is assigned by the
104/// opener, so two ends can both pick `SessionId(0)`. The key scopes a session by `(peer,
105/// namespace, session, initiator)`, where `peer` is the **authenticated** other end
106/// (`event.from`, the verified signer) and `initiator` records which end opened it. Because a
107/// peer cannot forge `event.from`, it can only ever address sessions whose `peer` is itself
108/// (owner rejection); and `initiator` keeps a peer's session distinct from one of ours that
109/// happened to get the same id (bidirectional-open safety).
110#[derive(Clone, PartialEq, Eq, Hash, Debug)]
111pub struct SessionKey {
112 /// The authenticated remote end of the session (`event.from` for inbound frames).
113 pub peer: Did,
114 /// The transport namespace the session lives under (e.g. `tcp`, `udp`).
115 pub namespace: String,
116 /// The opener-assigned session id, unique only within `(peer, namespace, initiator)`.
117 pub session: SessionId,
118 /// Which end opened the session (disambiguates colliding ids on simultaneous open).
119 pub initiator: Initiator,
120}
121
122impl SessionKey {
123 /// Build a session key from its parts.
124 pub fn new(
125 peer: Did,
126 namespace: impl Into<String>,
127 session: SessionId,
128 initiator: Initiator,
129 ) -> Self {
130 Self {
131 peer,
132 namespace: namespace.into(),
133 session,
134 initiator,
135 }
136 }
137}
138
139/// Which local socket a relay session is backed by.
140///
141/// Both kinds share the same [`Frame`] vocabulary (`Open`/`Data`/`Close`); only the
142/// socket differs. UDP is *flow*-based rather than truly sessionless because a relayed
143/// datagram still needs a return path to the originating local client, so each flow
144/// carries a [`SessionId`] just like a TCP connection. `Data` preserves message
145/// boundaries (one datagram per frame) for UDP.
146#[derive(Clone, Copy, PartialEq, Eq, Debug)]
147pub enum TransportKind {
148 /// Connection-oriented byte stream.
149 Tcp,
150 /// Datagram flow (per-flow socket; message boundaries preserved per `Data`).
151 Udp,
152}
153
154/// The relay's overlay wire message — the payload carried under a transport namespace.
155///
156/// One vocabulary for both kinds (TCP connections and UDP flows):
157///
158/// ```text
159/// Open(session, service) → Data(session, bytes)* → Close(session)
160/// ```
161///
162/// `Open` is always sent by the session's opener. `Data`/`Shutdown`/`Close` flow in both
163/// directions over the *same* opener-assigned id, so they carry `from_opener` — whether the
164/// **sender** of this frame opened the session. The receiver flips it to recover its own
165/// [`Initiator`], so a peer's session never collides with one of ours sharing the same id.
166#[derive(Clone, Debug, Serialize, Deserialize)]
167pub enum Frame {
168 /// Open a session/flow to a named local service (always sent by the opener).
169 Open {
170 /// Session identifier (assigned by the opener).
171 session: SessionId,
172 /// Local service name to connect to.
173 service: String,
174 },
175 /// Bytes on an open session (one datagram per frame for UDP).
176 Data {
177 /// Session the bytes belong to.
178 session: SessionId,
179 /// Whether the sender of this frame opened the session.
180 from_opener: bool,
181 /// Payload bytes.
182 bytes: Bytes,
183 },
184 /// Half-close: the sender has no more `Data` this direction (a TCP FIN). The
185 /// receiver shuts down its local write side but keeps the reverse direction open.
186 /// Ignored by UDP (datagram flows have no half-close).
187 Shutdown {
188 /// Session being half-closed.
189 session: SessionId,
190 /// Whether the sender of this frame opened the session.
191 from_opener: bool,
192 },
193 /// Close a session/flow (full teardown, both directions).
194 Close {
195 /// Session to close.
196 session: SessionId,
197 /// Whether the sender of this frame opened the session.
198 from_opener: bool,
199 },
200}