Skip to main content

confium_net_tcp/
transport.rs

1//! Connected TCP transport and the shared length-prefix framing layer.
2//!
3//! [`TcpTransport`] wraps a [`std::net::TcpStream`] and implements
4//! [`confium_net::Transport`]. Each `send` is written to the socket as
5//! a 4-byte big-endian length prefix followed by the payload bytes;
6//! each `recv` reads exactly one framed message (repeating partial-read
7//! calls until the full payload is in hand) so the
8//! "one `send` == one `recv`" contract holds over the byte stream.
9//!
10//! The framing helpers ([`write_frame`], [`read_frame`]) live here and
11//! are reused by [`crate::listener::TcpListener`] for accepted streams.
12
13use std::io::Read;
14use std::io::Write;
15use std::net::IpAddr;
16use std::net::Shutdown;
17use std::net::SocketAddr;
18use std::net::TcpStream;
19
20use url::Url;
21
22use confium_net::Listener;
23use confium_net::Result;
24use confium_net::Transport;
25use confium_net::error::ClosedSnafu;
26use confium_net::error::IoSnafu;
27use confium_net::error::MalformedUrlSnafu;
28use confium_net::registry::TransportKind;
29use snafu::IntoError;
30
31/// Maximum payload size for a single frame (8 MiB). Guards against a
32/// malicious or buggy peer sending a gigantic length prefix that would
33/// cause the receiver to allocate unbounded memory.
34pub(crate) const MAX_FRAME_LEN: u32 = 8 * 1024 * 1024;
35
36// ---- framing ---------------------------------------------------------
37
38/// Write one length-prefixed frame: 4-byte big-endian length, then the
39/// payload. Partial writes are repeated until the whole frame is on the
40/// wire.
41pub(crate) fn write_frame<W: Write>(w: &mut W, data: &[u8]) -> std::io::Result<()> {
42    let len = u32::try_from(data.len()).map_err(|_| {
43        std::io::Error::new(
44            std::io::ErrorKind::InvalidInput,
45            "frame exceeds 4 GiB length-prefix limit",
46        )
47    })?;
48    w.write_all(&len.to_be_bytes())?;
49    w.write_all(data)?;
50    w.flush()?;
51    Ok(())
52}
53
54/// Read exactly one length-prefixed frame into `buf`, returning:
55///
56/// - `Ok(Some(n))` — a frame of length `n` was read into `buf`. If the
57///   buffer was smaller than the frame, `n == buf.len()` and the
58///   remainder of the frame is drained and discarded, matching the
59///   built-in `inproc` transport's "fill what you can, drop the rest"
60///   semantics for undersized buffers.
61/// - `Ok(None)` — the peer closed the stream cleanly at a frame
62///   boundary (zero bytes of the next length prefix arrived before
63///   EOF). The caller should translate this to `Error::Closed`.
64///
65/// Any I/O failure, including EOF inside a length prefix or mid-frame,
66/// is returned as `Err`.
67pub(crate) fn read_frame<R: Read>(r: &mut R, buf: &mut [u8]) -> std::io::Result<Option<usize>> {
68    let mut prefix = [0u8; 4];
69    if !fill_exact(r, &mut prefix)? {
70        // Clean EOF at a frame boundary.
71        return Ok(None);
72    }
73    let len = u32::from_be_bytes(prefix);
74    if len > MAX_FRAME_LEN {
75        return Err(std::io::Error::new(
76            std::io::ErrorKind::InvalidData,
77            format!("frame length {len} exceeds maximum {MAX_FRAME_LEN}"),
78        ));
79    }
80    let len = len as usize;
81    let n = std::cmp::min(len, buf.len());
82    // Read the bytes the caller will receive.
83    read_exact(r, &mut buf[..n])?;
84    // Drain and discard any bytes beyond the caller's buffer so the
85    // socket is positioned at the start of the next frame.
86    let mut remaining = len - n;
87    let mut sink = [0u8; 4096];
88    while remaining > 0 {
89        let want = remaining.min(sink.len());
90        match r.read(&mut sink[..want])? {
91            0 => {
92                return Err(std::io::Error::new(
93                    std::io::ErrorKind::UnexpectedEof,
94                    "stream closed mid-frame",
95                ));
96            }
97            got => remaining -= got,
98        }
99    }
100    Ok(Some(n))
101}
102
103/// Read exactly `buf.len()` bytes. Returns `Ok(false)` if the stream
104/// reached EOF before **any** byte was read (a clean boundary); returns
105/// `Ok(true)` once `buf` is full. A short read partway through `buf`
106/// is `UnexpectedEof` — the peer closed inside a frame.
107fn fill_exact<R: Read>(r: &mut R, buf: &mut [u8]) -> std::io::Result<bool> {
108    let mut filled = 0;
109    while filled < buf.len() {
110        match r.read(&mut buf[filled..])? {
111            0 => {
112                if filled == 0 {
113                    return Ok(false);
114                }
115                return Err(std::io::Error::new(
116                    std::io::ErrorKind::UnexpectedEof,
117                    "stream closed inside length prefix",
118                ));
119            }
120            n => filled += n,
121        }
122    }
123    Ok(true)
124}
125
126/// Read exactly `buf.len()` bytes, erroring on any short read. Used
127/// after the length prefix is fully read, so any EOF here is mid-frame.
128fn read_exact<R: Read>(r: &mut R, buf: &mut [u8]) -> std::io::Result<()> {
129    let mut filled = 0;
130    while filled < buf.len() {
131        match r.read(&mut buf[filled..])? {
132            0 => {
133                return Err(std::io::Error::new(
134                    std::io::ErrorKind::UnexpectedEof,
135                    "stream closed mid-frame",
136                ));
137            }
138            n => filled += n,
139        }
140    }
141    Ok(())
142}
143
144// ---- connected transport --------------------------------------------
145
146/// Connected TCP transport. Owns the underlying [`TcpStream`]; `close`
147/// shuts both directions of the stream down so the peer observes a
148/// clean end-of-stream.
149pub struct TcpTransport {
150    stream: Option<TcpStream>,
151}
152
153impl TcpTransport {
154    /// Wrap an already-connected stream (used by
155    /// [`crate::listener::TcpListener`] for accepted peers).
156    pub(crate) fn from_stream(stream: TcpStream) -> Self {
157        Self {
158            stream: Some(stream),
159        }
160    }
161
162    /// Connect a new stream to `host:port`, honoring the
163    /// address-family hint `tcp4` / `tcp6` / `tcp` encoded in `scheme`.
164    pub(crate) fn connect(scheme: &str, host: &str, port: u16) -> std::io::Result<Self> {
165        let stream = match address_family(scheme) {
166            Some(false) => {
167                // tcp4: force IPv4.
168                let ip: IpAddr = host
169                    .parse()
170                    .map_err(|_| invalid_host(host, "not an IPv4 literal"))?;
171                if !ip.is_ipv4() {
172                    return Err(invalid_host(host, "not an IPv4 literal"));
173                }
174                TcpStream::connect(SocketAddr::new(ip, port))?
175            }
176            Some(true) => {
177                // tcp6: force IPv6.
178                let ip: IpAddr = host
179                    .parse()
180                    .map_err(|_| invalid_host(host, "not an IPv6 literal"))?;
181                if !ip.is_ipv6() {
182                    return Err(invalid_host(host, "not an IPv6 literal"));
183                }
184                TcpStream::connect(SocketAddr::new(ip, port))?
185            }
186            None => {
187                // tcp: any family. `host:port` is resolved by the
188                // standard library via `ToSocketAddrs` (covers literal
189                // IPs and DNS names).
190                TcpStream::connect((host, port))?
191            }
192        };
193        // Disable Nagle: threshold-cryptography traffic is bursty, small
194        // round messages where the latency cost of coalescing outweighs
195        // the bandwidth saving. (TODO.roadmap/05: "latency matters more
196        // than throughput" for typical TC schemes.)
197        stream.set_nodelay(true).ok();
198        Ok(Self::from_stream(stream))
199    }
200}
201
202impl Transport for TcpTransport {
203    fn send(&mut self, data: &[u8]) -> Result<()> {
204        let stream = match &mut self.stream {
205            Some(s) => s,
206            None => return ClosedSnafu.fail(),
207        };
208        write_frame(stream, data).map_err(io_to_closed)
209    }
210
211    fn recv(&mut self, buf: &mut [u8]) -> Result<usize> {
212        let stream = match &mut self.stream {
213            Some(s) => s,
214            None => return ClosedSnafu.fail(),
215        };
216        match read_frame(stream, buf) {
217            Ok(Some(n)) => Ok(n),
218            // Clean EOF at a frame boundary: peer closed cleanly.
219            Ok(None) => ClosedSnafu.fail(),
220            Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => ClosedSnafu.fail(),
221            Err(e) => Err(io_to_closed(e)),
222        }
223    }
224
225    fn close(&mut self) -> Result<()> {
226        if let Some(stream) = self.stream.take() {
227            // Best-effort: the peer may have already closed their end.
228            stream.shutdown(Shutdown::Both).ok();
229        }
230        Ok(())
231    }
232}
233
234impl Drop for TcpTransport {
235    fn drop(&mut self) {
236        if let Some(stream) = self.stream.take() {
237            stream.shutdown(Shutdown::Both).ok();
238        }
239    }
240}
241
242// ---- registry kind ---------------------------------------------------
243
244/// Registry kind for `tcp`, `tcp4`, `tcp6`.
245pub struct TcpTransportKind;
246
247impl TransportKind for TcpTransportKind {
248    fn schemes(&self) -> &'static [&'static str] {
249        &["tcp", "tcp4", "tcp6"]
250    }
251
252    fn connect(&self, url: &Url) -> Result<Box<dyn Transport>> {
253        let scheme = url.scheme();
254        let (host, port) = host_port(url, scheme)?;
255        match TcpTransport::connect(scheme, host, port) {
256            Ok(t) => Ok(Box::new(t)),
257            Err(e) => Err(IoSnafu.into_error(e)),
258        }
259    }
260
261    fn listen(&self, url: &Url) -> Result<Box<dyn Listener>> {
262        let scheme = url.scheme();
263        let (host, port) = host_port(url, scheme)?;
264        match crate::listener::TcpListener::bind(scheme, host, port) {
265            Ok(l) => Ok(Box::new(l)),
266            Err(e) => Err(IoSnafu.into_error(e)),
267        }
268    }
269}
270
271// ---- shared helpers --------------------------------------------------
272
273/// Extract the `(host, port)` pair from a `tcp*://` URL. The port is
274/// mandatory for TCP (unlike `inproc` which carries a channel name).
275pub(crate) fn host_port<'a>(url: &'a Url, scheme: &str) -> Result<(&'a str, u16)> {
276    let host = url.host_str().unwrap_or("");
277    if host.is_empty() {
278        return MalformedUrlSnafu {
279            scheme,
280            url: url.to_string(),
281            reason: "missing host (use tcp://<host>:<port>)",
282        }
283        .fail();
284    }
285    let port = match url.port() {
286        Some(p) => p,
287        None => {
288            return MalformedUrlSnafu {
289                scheme,
290                url: url.to_string(),
291                reason: "missing port (use tcp://<host>:<port>)",
292            }
293            .fail();
294        }
295    };
296    Ok((host, port))
297}
298
299/// Map a scheme to its address-family constraint: `Some(true)` for
300/// `tcp6`, `Some(false)` for `tcp4`, `None` for `tcp` (any family).
301pub(crate) fn address_family(scheme: &str) -> Option<bool> {
302    match scheme {
303        "tcp4" => Some(false),
304        "tcp6" => Some(true),
305        _ => None,
306    }
307}
308
309fn invalid_host(host: &str, reason: &str) -> std::io::Error {
310    std::io::Error::new(
311        std::io::ErrorKind::InvalidInput,
312        format!("invalid host '{host}': {reason}"),
313    )
314}
315
316/// Map an [`std::io::Error`] to a [`confium_net::Error`]. The
317/// confium-net error type does not today carry an arbitrary I/O source,
318/// so connection / read / write failures are reported as `Closed` —
319/// the transport is unusable after an I/O failure, matching how the
320/// inproc transport reports a broken channel.
321pub(crate) fn io_to_closed(_: std::io::Error) -> confium_net::Error {
322    ClosedSnafu.build()
323}