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 opens an RLY-002 forwarding channel to the target
19//! peer through the relay WebSocket.
20
21use std::net::SocketAddr;
22
23use async_trait::async_trait;
24
25use crate::error::MethodError;
26use crate::method::{MethodOutcome, TraversalKind, TraversalMethod};
27use crate::peer::PeerTarget;
28
29/// Abstraction over the relay **data plane**: open a stream to `target_peer` whose bytes are
30/// proxied THROUGH the relay (RLY-002). This is the tier-6 TURN-like fallback — distinct from the
31/// tier-5 [`HolePunchCoordinator`](super::hole_punch::HolePunchCoordinator), which only signals.
32///
33/// Returns the relay endpoint the data flows over (for observability — the mTLS session then runs
34/// over that tunnel). `Err` = the relay could not open the forwarding channel (peer offline / relay
35/// down / disabled).
36#[async_trait]
37pub trait RelayedTransport: Send + Sync {
38 /// Open a relay-proxied data channel to `target_peer` on `network_id`. Returns the relay's
39 /// endpoint address (the data path). `Err` = could not establish the tunnel.
40 async fn open_relayed(&self, target_peer: &str, network_id: &str)
41 -> Result<SocketAddr, String>;
42}
43
44/// The tier-6 relayed-transport (TURN-like) method — proxies ALL peer data through the relay. Only
45/// reached when every more-direct method (including the tier-5 hole punch) has failed.
46pub struct RelayedTransportMethod<T: RelayedTransport> {
47 transport: T,
48}
49
50impl<T: RelayedTransport> RelayedTransportMethod<T> {
51 /// Build a relayed-transport method over `transport` (the relay data-plane).
52 pub fn new(transport: T) -> Self {
53 RelayedTransportMethod { transport }
54 }
55}
56
57#[async_trait]
58impl<T: RelayedTransport> TraversalMethod for RelayedTransportMethod<T> {
59 fn kind(&self) -> TraversalKind {
60 TraversalKind::Relayed
61 }
62
63 async fn attempt(&self, peer: &PeerTarget) -> Result<MethodOutcome, MethodError> {
64 let relay_addr = self
65 .transport
66 .open_relayed(&peer.peer_id.to_hex(), &peer.network_id)
67 .await
68 .map_err(|e| MethodError::failed(TraversalKind::Relayed, e))?;
69 // The "dial address" for the relayed tier is the RELAY — all data flows through it (a single
70 // endpoint, not a candidate list).
71 Ok(MethodOutcome::single(TraversalKind::Relayed, relay_addr))
72 }
73}