dig_nat/tunnel.rs
1//! Byte-stream adapter over a relay tunnel — turns the message-oriented [`RelayTunnel`] (RLY-002
2//! send/recv of opaque payloads) into a tokio [`AsyncRead`] + [`AsyncWrite`] duplex stream, so the
3//! SAME mTLS handshake + [`yamux`](crate::mux) multiplexing that runs over a direct TCP connection
4//! runs UNCHANGED over the tier-6 relayed path.
5//!
6//! ## Why this exists (the security crux)
7//!
8//! The relayed tier is the last resort, but it MUST NOT be a weaker connection than a direct one. By
9//! carrying the identical [`dig_tls`] mTLS session over this adapter, a relayed [`PeerConnection`]
10//! presents the same CA-chained [`NodeCert`](dig_tls::NodeCert), the same `peer_id =
11//! SHA-256(SPKI DER)` pin, and the same #1204 BLS binding as a direct connection. The relay only ever
12//! forwards TLS records it cannot read — it is an untrusted byte forwarder, never a trusted party
13//! (§5.4 recipient-sealing still applies as a layer ABOVE this transport). The mTLS layer, not the
14//! relay, authenticates the peer.
15//!
16//! ## Framing
17//!
18//! A [`RelayTunnel`] forwards whole payloads A→relay→B in order. TLS is itself a length-delimited
19//! record protocol, so this adapter needs no framing of its own: each `poll_write` ships up to
20//! [`MAX_RELAY_PAYLOAD`](crate::relay::MAX_RELAY_PAYLOAD) bytes as one RLY-002 frame, and each
21//! `poll_read` drains one inbound payload at a time (buffering the remainder across reads). Payload
22//! order is preserved by the single relay socket; a dropped frame under backpressure surfaces as a
23//! stream error, which fails the dial (acceptable for a last-resort tier — the strategy has already
24//! exhausted every more-direct method).
25
26use std::io;
27use std::pin::Pin;
28use std::task::{Context, Poll};
29
30use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
31
32use crate::relay::{RelayTunnel, MAX_RELAY_PAYLOAD};
33
34/// A duplex byte stream over one [`RelayTunnel`]: writes become RLY-002 relayed frames to the peer,
35/// reads deliver the payloads the relay forwards back. Wrap it exactly like a [`tokio::net::TcpStream`]
36/// — the mTLS connector/acceptor and yamux run over it identically.
37pub struct RelayTunnelStream {
38 /// The underlying relay tunnel (RLY-002 send/recv over the persistent reservation socket).
39 tunnel: RelayTunnel,
40 /// Bytes from the most recently received payload not yet copied to a reader, and the read cursor
41 /// into them. A single inbound payload can satisfy several small `poll_read`s (e.g. TLS reads the
42 /// 5-byte record header then the body), so the remainder is held here rather than dropped.
43 read_carry: Vec<u8>,
44 read_pos: usize,
45}
46
47impl RelayTunnelStream {
48 /// Adapt a [`RelayTunnel`] into a byte-stream. The caller then runs the mTLS handshake over it.
49 pub fn new(tunnel: RelayTunnel) -> Self {
50 RelayTunnelStream {
51 tunnel,
52 read_carry: Vec::new(),
53 read_pos: 0,
54 }
55 }
56}
57
58impl AsyncRead for RelayTunnelStream {
59 fn poll_read(
60 self: Pin<&mut Self>,
61 cx: &mut Context<'_>,
62 buf: &mut ReadBuf<'_>,
63 ) -> Poll<io::Result<()>> {
64 let this = self.get_mut();
65
66 // Serve from the carry-over buffer first — one relay payload may fill many small reads.
67 if this.read_pos >= this.read_carry.len() {
68 match this.tunnel.poll_recv(cx) {
69 Poll::Ready(Some(payload)) => {
70 this.read_carry = payload;
71 this.read_pos = 0;
72 }
73 // The reservation dropped: signal clean EOF so the mTLS layer reports a closed
74 // connection rather than hanging.
75 Poll::Ready(None) => return Poll::Ready(Ok(())),
76 Poll::Pending => return Poll::Pending,
77 }
78 }
79
80 let remaining = &this.read_carry[this.read_pos..];
81 let n = remaining.len().min(buf.remaining());
82 buf.put_slice(&remaining[..n]);
83 this.read_pos += n;
84 Poll::Ready(Ok(()))
85 }
86}
87
88impl AsyncWrite for RelayTunnelStream {
89 fn poll_write(
90 self: Pin<&mut Self>,
91 _cx: &mut Context<'_>,
92 buf: &[u8],
93 ) -> Poll<io::Result<usize>> {
94 // Ship at most one relay frame per call; the caller loops for a larger buffer. TLS records are
95 // far smaller than the cap, so in practice one write == one record == one frame.
96 let n = buf.len().min(MAX_RELAY_PAYLOAD);
97 match self.tunnel.send(buf[..n].to_vec()) {
98 Ok(()) => Poll::Ready(Ok(n)),
99 Err(e) => Poll::Ready(Err(io::Error::new(io::ErrorKind::BrokenPipe, e))),
100 }
101 }
102
103 fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
104 // `send` hands the frame straight to the reservation's outbound sink — nothing is buffered here.
105 Poll::Ready(Ok(()))
106 }
107
108 fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
109 // Dropping the stream drops the tunnel, which deregisters its routing; there is no half-close
110 // to signal over RLY-002, so shutdown is a no-op success.
111 Poll::Ready(Ok(()))
112 }
113}