Skip to main content

dig_nat/method/
relayed.rs

1//! Relayed-transport method (TURN-like) — the **last resort**, tier 6.
2//!
3//! This tier is **sharply distinct** from the tier-5 hole-punch ([`super::hole_punch`]):
4//!
5//! | Tier | Method | Relay's role | Relay bandwidth |
6//! |------|--------|--------------|-----------------|
7//! | 5 | [`HolePunchMethod`](super::hole_punch::HolePunchMethod) | **signaling only** — brokers a candidate exchange, then the DATA path is peer-to-peer direct | minimal (a few coordination messages) |
8//! | 6 | [`RelayedTransportMethod`] (this) | **carries ALL data** — every byte of the peer connection is proxied through the relay (RLY-002 `relay_message`) | highest — the relay proxies the whole stream |
9//!
10//! Because tier 6 costs the relay the most bandwidth, it is tried **only after** the tier-5 hole
11//! punch fails: prefer brokering an introduction (hole punch) over proxying the stream (TURN). The
12//! [`crate::strategy`] enforces this via [`super::TraversalKind::rank`] (HolePunch=4 < Relayed=5).
13//!
14//! After the relay opens the tunnel, the resulting byte stream is still wrapped in the same mTLS
15//! (peer_id = SHA-256(SPKI)) as every other tier — the relay proxies ciphertext it cannot read.
16//!
17//! The relay data-plane is abstracted behind [`RelayedTransport`] so the method is unit-tested with
18//! a mock (no real relay). The production impl — [`ReservationRelayedTransport`] — opens an RLY-002
19//! forwarding channel to the target peer THROUGH the node's persistent relay reservation socket
20//! (never a second connection), and hands the caller a [`RelayTunnel`] for the byte stream.
21
22use std::net::SocketAddr;
23use std::sync::Arc;
24
25use async_trait::async_trait;
26
27use crate::error::MethodError;
28use crate::method::{MethodOutcome, TraversalKind, TraversalMethod};
29use crate::peer::PeerTarget;
30use crate::relay::{RelayStatus, RelayTunnel};
31
32/// A relayed transport that opens the DUPLEX BYTE TUNNEL an mTLS session runs over — the transport
33/// half of the tier-6 relayed dial. Distinct from [`RelayedTransport`] (which only reports a probe
34/// endpoint): this yields the actual byte channel, so a relayed connection carries the SAME dig-tls
35/// mTLS + `peer_id` pin + BLS binding as a direct one (the relay forwards ciphertext it cannot read).
36///
37/// [`crate::connect`] composes the relayed tier only when a `RelayedDialer` is supplied via the
38/// runtime carrier; the [`crate::dialer::MtlsDialer`] uses it to open the tunnel for a
39/// [`TraversalKind::Relayed`] outcome, then runs the identical handshake over it.
40#[async_trait]
41pub trait RelayedDialer: Send + Sync {
42    /// The relay endpoint the tunnel forwards through (observability + the relayed dial address).
43    fn relay_endpoint(&self) -> SocketAddr;
44
45    /// Whether a relayed tunnel can currently be opened (a reservation is held). The relayed method's
46    /// [`attempt`](TraversalMethod::attempt) gates on this so an unavailable relay falls through
47    /// cleanly rather than producing a doomed dial.
48    fn is_ready(&self) -> bool;
49
50    /// Open a live duplex tunnel to `target_peer` (hex `peer_id`) on `network_id`. `Err` if the
51    /// reservation is not held. The returned [`RelayTunnel`] is wrapped in a byte-stream adapter and
52    /// the mTLS handshake runs over it.
53    async fn open_dial_tunnel(
54        &self,
55        target_peer: &str,
56        network_id: &str,
57    ) -> Result<RelayTunnel, String>;
58}
59
60#[async_trait]
61impl RelayedDialer for ReservationRelayedTransport {
62    fn relay_endpoint(&self) -> SocketAddr {
63        self.relay_endpoint
64    }
65
66    fn is_ready(&self) -> bool {
67        self.status.relay_transport_ready()
68    }
69
70    async fn open_dial_tunnel(
71        &self,
72        target_peer: &str,
73        network_id: &str,
74    ) -> Result<RelayTunnel, String> {
75        self.status.open_tunnel(target_peer, network_id)
76    }
77}
78
79/// The relayed (tier-6, TURN-last) traversal method built over a [`RelayedDialer`]. Its
80/// [`attempt`](TraversalMethod::attempt) confirms the relay reservation is ready and yields the relay
81/// endpoint as the dial address; the actual byte tunnel + mTLS is opened by the dialer. This is the
82/// method [`crate::connect`] auto-composes for the relayed tier (given a runtime `RelayedDialer`).
83pub struct RelayedDialMethod {
84    dialer: Arc<dyn RelayedDialer>,
85}
86
87impl RelayedDialMethod {
88    /// Build the relayed method over a live [`RelayedDialer`] (the relay data-plane).
89    pub fn new(dialer: Arc<dyn RelayedDialer>) -> Self {
90        RelayedDialMethod { dialer }
91    }
92}
93
94#[async_trait]
95impl TraversalMethod for RelayedDialMethod {
96    fn kind(&self) -> TraversalKind {
97        TraversalKind::Relayed
98    }
99
100    async fn attempt(&self, _peer: &PeerTarget) -> Result<MethodOutcome, MethodError> {
101        if !self.dialer.is_ready() {
102            return Err(MethodError::failed(
103                TraversalKind::Relayed,
104                "relay reservation not connected — relayed transport unavailable",
105            ));
106        }
107        // The dial address is the relay endpoint (observability); the dialer opens the real tunnel to
108        // the peer over the held reservation.
109        Ok(MethodOutcome::single(
110            TraversalKind::Relayed,
111            self.dialer.relay_endpoint(),
112        ))
113    }
114}
115
116/// Abstraction over the relay **data plane**: open a stream to `target_peer` whose bytes are
117/// proxied THROUGH the relay (RLY-002). This is the tier-6 TURN-like fallback — distinct from the
118/// tier-5 [`HolePunchCoordinator`](super::hole_punch::HolePunchCoordinator), which only signals.
119///
120/// Returns the relay endpoint the data flows over (for observability — the mTLS session then runs
121/// over that tunnel). `Err` = the relay could not open the forwarding channel (peer offline / relay
122/// down / disabled).
123#[async_trait]
124pub trait RelayedTransport: Send + Sync {
125    /// Open a relay-proxied data channel to `target_peer` on `network_id`. Returns the relay's
126    /// endpoint address (the data path). `Err` = could not establish the tunnel.
127    async fn open_relayed(&self, target_peer: &str, network_id: &str)
128        -> Result<SocketAddr, String>;
129}
130
131/// The tier-6 relayed-transport (TURN-like) method — proxies ALL peer data through the relay. Only
132/// reached when every more-direct method (including the tier-5 hole punch) has failed.
133pub struct RelayedTransportMethod<T: RelayedTransport> {
134    transport: T,
135}
136
137impl<T: RelayedTransport> RelayedTransportMethod<T> {
138    /// Build a relayed-transport method over `transport` (the relay data-plane).
139    pub fn new(transport: T) -> Self {
140        RelayedTransportMethod { transport }
141    }
142}
143
144#[async_trait]
145impl<T: RelayedTransport> TraversalMethod for RelayedTransportMethod<T> {
146    fn kind(&self) -> TraversalKind {
147        TraversalKind::Relayed
148    }
149
150    async fn attempt(&self, peer: &PeerTarget) -> Result<MethodOutcome, MethodError> {
151        let relay_addr = self
152            .transport
153            .open_relayed(&peer.peer_id.to_hex(), &peer.network_id)
154            .await
155            .map_err(|e| MethodError::failed(TraversalKind::Relayed, e))?;
156        // The "dial address" for the relayed tier is the RELAY — all data flows through it (a single
157        // endpoint, not a candidate list).
158        Ok(MethodOutcome::single(TraversalKind::Relayed, relay_addr))
159    }
160}
161
162/// The PRODUCTION [`RelayedTransport`]: opens the RLY-002 forwarding channel over the node's LIVE
163/// persistent relay reservation (never a second socket), reusing the same [`RelayStatus`] handle the
164/// reservation loop publishes its outbound sink through. This is the tier-6 TURN fallback made real.
165///
166/// `open_relayed` (the ladder seam) confirms the reservation is held and the target is reachable via
167/// the relay, returning the relay endpoint for observability. The actual byte stream is obtained with
168/// [`open_tunnel`](Self::open_tunnel), which yields a [`RelayTunnel`] that forwards A→relay→B; per
169/// NC-1 the caller seals every payload to the recipient, so the relay forwards ciphertext only.
170pub struct ReservationRelayedTransport {
171    /// Shared handle to the persistent reservation — its live outbound sink + tunnel registry.
172    status: Arc<RelayStatus>,
173    /// The relay endpoint the data is forwarded through (observability; the byte path is the WS).
174    relay_endpoint: SocketAddr,
175}
176
177impl ReservationRelayedTransport {
178    /// Build the production transport over a live relay reservation (`status`) that forwards through
179    /// `relay_endpoint`.
180    pub fn new(status: Arc<RelayStatus>, relay_endpoint: SocketAddr) -> Self {
181        ReservationRelayedTransport {
182            status,
183            relay_endpoint,
184        }
185    }
186
187    /// Open a live RLY-002 relayed tunnel to `target_peer` (hex `peer_id`) over the held reservation.
188    /// The returned [`RelayTunnel`] sends/receives payloads forwarded A→relay→B. `Err` if no
189    /// reservation is currently held.
190    pub fn open_tunnel(&self, target_peer: &str, network_id: &str) -> Result<RelayTunnel, String> {
191        self.status.open_tunnel(target_peer, network_id)
192    }
193}
194
195#[async_trait]
196impl RelayedTransport for ReservationRelayedTransport {
197    async fn open_relayed(
198        &self,
199        target_peer: &str,
200        network_id: &str,
201    ) -> Result<SocketAddr, String> {
202        // Prove the RLY-002 forwarding channel can be established over the held reservation by opening
203        // (then releasing) a tunnel to the target; the working tunnel is taken via `open_tunnel`. A
204        // relay session must be live — otherwise this tier genuinely cannot carry the connection.
205        let _probe = self.status.open_tunnel(target_peer, network_id)?;
206        Ok(self.relay_endpoint)
207    }
208}