Skip to main content

phantom_protocol/transport/
session_transport.rs

1//! The `SessionTransport` byte-pipe abstraction — the boundary between
2//! `PhantomSession`'s background data pump and a concrete physical transport
3//! (`TcpSessionTransport`, `WebSocketLeg`, `EmbeddedLeg`, ...).
4//!
5//! Defined with **native async fn in trait** (AFIT, stable since Rust 1.75)
6//! rather than the `#[async_trait]` macro: method calls do not allocate a
7//! boxed future, and `Send`-ness of the returned futures is checked at the
8//! *use* site (the concrete impl type), not at the trait-impl site. The
9//! latter is what allows `EmbeddedLeg<R, W, ...>` to compose with
10//! `embedded-io-async`, whose async-fn-in-trait futures are not `Send`-
11//! bounded — `#[async_trait]` here would have failed to prove `Send`-ness
12//! at the generic impl site.
13//!
14//! Dependency-light (only `bytes` + `CoreError`) so it compiles in the
15//! shipped `no_std + alloc` subset (Phase 3.6) alongside `legs::embedded`.
16//! Re-exported from [`crate::api::session`] so the historical import path
17//! stays stable.
18//!
19//! Phase 3.6 (no-std foundation): module compiles without `std` because the
20//! implementation uses only `core::future::Future` and pulls nothing from
21//! `std::*`. The crate-level `#![cfg_attr(not(feature = "std"), no_std)]` in
22//! `lib.rs` drives the no_std switch; this module needs no extra attribute.
23
24use crate::errors::CoreError;
25use bytes::Bytes;
26// `String` is in the std prelude on std builds; under `no_std` it comes from `alloc`
27// (the migration address crosses this boundary as a `String` to keep the trait
28// `SocketAddr`-free — `std::net::SocketAddr` does not exist in `core`/`alloc`).
29#[cfg(not(feature = "std"))]
30use alloc::string::String;
31
32/// Lifecycle phase of a [`SessionTransport`], used to bound the receive frame
33/// size differently before vs. after the handshake (WIRE-001). During the
34/// unauthenticated handshake a peer can open a connection and declare a large
35/// frame; capping the receive size tightly there bounds the memory a single
36/// unauthenticated peer can make the server buffer. After establishment the cap
37/// rises to the steady-state application limit.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum FramePhase {
40    /// Pre-establishment: only small handshake messages are expected.
41    Handshake,
42    /// Post-establishment: full-size application frames are allowed.
43    Established,
44}
45
46/// Async transport trait for `PhantomSession`.
47///
48/// The byte-pipe abstraction over every concrete physical transport — PhantomUDP
49/// (`UdpClientTransport` / `UdpServerTransport`), TCP (`TcpSessionTransport`),
50/// WebSocket, WASI, Embedded, and the off-by-default TLS-mimicry leg. Used by the
51/// background handshake + data-pump task for all message-oriented I/O.
52///
53/// `recv_bytes` returns `Bytes` (Phase 2.8) so the recv pipeline can
54/// fan out the same buffer to multiple consumers via cheap refcount
55/// clones — no `Vec → Bytes` conversion at the trait boundary.
56/// `send_bytes` keeps `&[u8]` because the caller routinely sends a
57/// borrowed slice of an already-allocated send buffer.
58///
59/// **Wrapper contract (audit EPS-04):** every method below the two I/O methods
60/// has a *silently-succeeding* default (`{}` / `false` / `Ok(())`). Those are
61/// correct for transports without migration, but they make a **partial wrapper**
62/// (a newtype that forwards only `send_bytes` / `recv_bytes`) compile with no
63/// warning while every control call no-ops on the default — the exact bug that
64/// made the pre-ε FFI `migrate()` vacuous through `ObservedTransport`. Any wrapper
65/// over a `SessionTransport` **MUST forward the full surface** (all the control
66/// methods, not just I/O); the `observed_transport_forwards_all_control_methods`
67/// tripwire test guards this for the in-crate wrappers.
68pub trait SessionTransport: Send + Sync + 'static {
69    /// Send raw bytes to the peer.
70    ///
71    /// Desugared form (`fn -> impl Future + Send`) rather than `async fn`
72    /// so the `+ Send` bound on the returned future is explicit. This is
73    /// what lets the data pump spawn its task generically over any
74    /// `T: SessionTransport` without an AFIT `return_type_notation` hack.
75    fn send_bytes(
76        &self,
77        data: &[u8],
78    ) -> impl core::future::Future<Output = Result<(), CoreError>> + Send;
79    /// Receive the next message from the peer. The returned `Bytes` is
80    /// a refcounted view over an opaque buffer; subsequent `clone()`s
81    /// are cheap.
82    fn recv_bytes(&self) -> impl core::future::Future<Output = Result<Bytes, CoreError>> + Send;
83
84    /// Move the transport to a new [`FramePhase`], adjusting the receive
85    /// frame-size cap (WIRE-001). Default no-op — transports that do not
86    /// length-prefix / buffer, or are inherently bounded, need not implement it.
87    /// Called once by the session machinery at the handshake → data-pump
88    /// boundary. Adding this defaulted method is source-compatible for every
89    /// existing impl.
90    fn set_frame_phase(&self, _phase: FramePhase) {}
91
92    /// Stamp this transport's outbound routing CID (ε / WIRE v5). Called once by
93    /// the session machinery at the handshake → data-pump boundary with the
94    /// session's `current_outbound_cid()` (the rotating `CID_0`), switching the
95    /// transport off the bootstrap ConnId onto the chain. Default no-op —
96    /// socket-routed transports (TCP / WebSocket / WASI / Embedded) carry no
97    /// on-wire CID and ignore it. Source-compatible for every existing impl.
98    fn set_outbound_cid(&self, _cid: [u8; 8]) {}
99
100    /// Whether the transport has observed an unvalidated **candidate** source for
101    /// this session — a connection-migration signal (Phase 4). Only an
102    /// address-aware transport (the UDP server) ever returns `true`; stream
103    /// transports (TCP / WebSocket / WASI / Embedded) have no migration and use
104    /// the default `false`. Deliberately **SocketAddr-free** so the generic data
105    /// pump that calls it stays no_std-clean (`std::net::SocketAddr` does not
106    /// exist in `core`/`alloc`); the candidate address is held inside the
107    /// concrete transport.
108    fn has_migration_candidate(&self) -> bool {
109        false
110    }
111
112    /// Send `data` — an already-encrypted `PATH_VALIDATION` challenge built by the
113    /// session — to the transport's internally-tracked candidate source, distinct
114    /// from the established peer, under the transport's own anti-amplification cap
115    /// (RFC 9000 §8.2). Returns `Ok(true)` if a candidate existed and the send was
116    /// within budget, `Ok(false)` otherwise (no candidate, or the 3× cap was hit).
117    /// Default (non-address transports): a no-op `Ok(false)`. The candidate
118    /// address never crosses this boundary, keeping the trait no_std-safe.
119    fn send_to_candidate(
120        &self,
121        _data: &[u8],
122    ) -> impl core::future::Future<Output = Result<bool, CoreError>> + Send {
123        async { Ok(false) }
124    }
125
126    /// Commit the most-recently-received frame's source as the migration candidate — call ONLY
127    /// from the post-decrypt path, i.e. once that frame has been AEAD-verified (M-1). On the
128    /// address-aware UDP server this is the migration-integrity fix: the candidate (and hence
129    /// the server's `PATH_CHALLENGE` target) is only ever an AEAD-authenticated source, so a
130    /// spoofed CID-matched datagram (which never decrypts) cannot clobber the candidate slot
131    /// and misdirect / stall a legitimate migration. SocketAddr-free — the address stays inside
132    /// the concrete transport. Default no-op for transports without migration.
133    fn confirm_authenticated_source(&self) {}
134
135    /// Promote the migration candidate to the established peer (Phase 4, the
136    /// switch): after the candidate's path validates, subsequent app data and
137    /// retransmits go to it instead of the old peer. Returns `true` if a candidate
138    /// was promoted. Default no-op (`false`) for transports without migration.
139    /// SocketAddr-free — the address is internal to the concrete transport.
140    fn promote_candidate(&self) -> bool {
141        false
142    }
143
144    /// Migrate this transport to a new local address (Phase 4 / P4.2c — embedder-
145    /// triggered connection migration). The address crosses as a `String` (parsed
146    /// inside the concrete native transport) so the
147    /// trait stays `SocketAddr`-free and no_std-clean — `std::net::SocketAddr` does
148    /// not exist in `core`/`alloc`. Best-effort: a parse / bind / connect failure
149    /// returns `Err` and the session is expected to keep running on its existing
150    /// socket — migration never tears it down. Default no-op `Ok(())` for transports
151    /// without migration (TCP / WebSocket / WASI / Embedded / the in-memory test
152    /// pipe); only the native UDP client implements it.
153    fn migrate(
154        &self,
155        _local_addr: String,
156    ) -> impl core::future::Future<Output = Result<(), CoreError>> + Send {
157        async { Ok(()) }
158    }
159
160    /// Migrate the **server side** of this transport to a new local send address — the
161    /// mirror of [`migrate`](Self::migrate) for the accepting peer. The server binds a
162    /// fresh local socket, sends subsequent server→client datagrams from it (so the client
163    /// sees a new s2c source and follows it), and receives on it too (so once the client
164    /// switches its send target the c2s frames are delivered transparently). The address
165    /// crosses as a `String` to keep the trait `SocketAddr`-free / no_std-clean. Best-effort:
166    /// a parse / bind failure returns `Err` and the session keeps running on the old socket.
167    /// Default no-op `Ok(())` — only the native UDP server implements it. Kept distinct from
168    /// [`migrate`](Self::migrate) so the FFI-exported client `migrate()` cannot trigger a
169    /// server migration.
170    fn migrate_server(
171        &self,
172        _local_addr: String,
173    ) -> impl core::future::Future<Output = Result<(), CoreError>> + Send {
174        async { Ok(()) }
175    }
176}