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/// Abstraction over the relay **data plane**: open a stream to `target_peer` whose bytes are
33/// proxied THROUGH the relay (RLY-002). This is the tier-6 TURN-like fallback — distinct from the
34/// tier-5 [`HolePunchCoordinator`](super::hole_punch::HolePunchCoordinator), which only signals.
35///
36/// Returns the relay endpoint the data flows over (for observability — the mTLS session then runs
37/// over that tunnel). `Err` = the relay could not open the forwarding channel (peer offline / relay
38/// down / disabled).
39#[async_trait]
40pub trait RelayedTransport: Send + Sync {
41    /// Open a relay-proxied data channel to `target_peer` on `network_id`. Returns the relay's
42    /// endpoint address (the data path). `Err` = could not establish the tunnel.
43    async fn open_relayed(&self, target_peer: &str, network_id: &str)
44        -> Result<SocketAddr, String>;
45}
46
47/// The tier-6 relayed-transport (TURN-like) method — proxies ALL peer data through the relay. Only
48/// reached when every more-direct method (including the tier-5 hole punch) has failed.
49pub struct RelayedTransportMethod<T: RelayedTransport> {
50    transport: T,
51}
52
53impl<T: RelayedTransport> RelayedTransportMethod<T> {
54    /// Build a relayed-transport method over `transport` (the relay data-plane).
55    pub fn new(transport: T) -> Self {
56        RelayedTransportMethod { transport }
57    }
58}
59
60#[async_trait]
61impl<T: RelayedTransport> TraversalMethod for RelayedTransportMethod<T> {
62    fn kind(&self) -> TraversalKind {
63        TraversalKind::Relayed
64    }
65
66    async fn attempt(&self, peer: &PeerTarget) -> Result<MethodOutcome, MethodError> {
67        let relay_addr = self
68            .transport
69            .open_relayed(&peer.peer_id.to_hex(), &peer.network_id)
70            .await
71            .map_err(|e| MethodError::failed(TraversalKind::Relayed, e))?;
72        // The "dial address" for the relayed tier is the RELAY — all data flows through it (a single
73        // endpoint, not a candidate list).
74        Ok(MethodOutcome::single(TraversalKind::Relayed, relay_addr))
75    }
76}
77
78/// The PRODUCTION [`RelayedTransport`]: opens the RLY-002 forwarding channel over the node's LIVE
79/// persistent relay reservation (never a second socket), reusing the same [`RelayStatus`] handle the
80/// reservation loop publishes its outbound sink through. This is the tier-6 TURN fallback made real.
81///
82/// `open_relayed` (the ladder seam) confirms the reservation is held and the target is reachable via
83/// the relay, returning the relay endpoint for observability. The actual byte stream is obtained with
84/// [`open_tunnel`](Self::open_tunnel), which yields a [`RelayTunnel`] that forwards A→relay→B; per
85/// NC-1 the caller seals every payload to the recipient, so the relay forwards ciphertext only.
86pub struct ReservationRelayedTransport {
87    /// Shared handle to the persistent reservation — its live outbound sink + tunnel registry.
88    status: Arc<RelayStatus>,
89    /// The relay endpoint the data is forwarded through (observability; the byte path is the WS).
90    relay_endpoint: SocketAddr,
91}
92
93impl ReservationRelayedTransport {
94    /// Build the production transport over a live relay reservation (`status`) that forwards through
95    /// `relay_endpoint`.
96    pub fn new(status: Arc<RelayStatus>, relay_endpoint: SocketAddr) -> Self {
97        ReservationRelayedTransport {
98            status,
99            relay_endpoint,
100        }
101    }
102
103    /// Open a live RLY-002 relayed tunnel to `target_peer` (hex `peer_id`) over the held reservation.
104    /// The returned [`RelayTunnel`] sends/receives payloads forwarded A→relay→B. `Err` if no
105    /// reservation is currently held.
106    pub fn open_tunnel(&self, target_peer: &str, network_id: &str) -> Result<RelayTunnel, String> {
107        self.status.open_tunnel(target_peer, network_id)
108    }
109}
110
111#[async_trait]
112impl RelayedTransport for ReservationRelayedTransport {
113    async fn open_relayed(
114        &self,
115        target_peer: &str,
116        network_id: &str,
117    ) -> Result<SocketAddr, String> {
118        // Prove the RLY-002 forwarding channel can be established over the held reservation by opening
119        // (then releasing) a tunnel to the target; the working tunnel is taken via `open_tunnel`. A
120        // relay session must be live — otherwise this tier genuinely cannot carry the connection.
121        let _probe = self.status.open_tunnel(target_peer, network_id)?;
122        Ok(self.relay_endpoint)
123    }
124}