dig_nat/method/hole_punch.rs
1//! Relay-coordinated hole-punch method (RLY-007) — when both peers are behind NAT, use the relay as
2//! a rendezvous to exchange server-reflexive addresses, then attempt a simultaneous open so a DIRECT
3//! path forms across both NATs (no relayed traffic).
4//!
5//! Flow: this node learns its own reflexive address via STUN ([`crate::stun`]) and sends the relay a
6//! [`RelayMessage::HolePunchRequest`](crate::wire::RelayMessage::HolePunchRequest) naming the target
7//! peer + our external address. The relay forwards it and returns the peer's external address in a
8//! [`RelayMessage::HolePunchCoordinate`](crate::wire::RelayMessage::HolePunchCoordinate). Both sides
9//! then dial each other's external address at the same time, punching a hole through their NATs.
10//!
11//! This is the LAST method before the fully-relayed transport: it still yields a *direct* peer
12//! address to dial, so a success avoids relaying traffic. The relay coordination is abstracted
13//! behind [`HolePunchCoordinator`] so the method is unit-tested with a mock coordinator (no relay).
14
15use std::net::SocketAddr;
16use std::sync::Arc;
17
18use async_trait::async_trait;
19
20use crate::error::MethodError;
21use crate::method::{MethodOutcome, TraversalKind, TraversalMethod};
22use crate::peer::PeerTarget;
23
24/// Abstraction over the relay's RLY-007 hole-punch coordination: "given the target peer and my
25/// external address, tell me the peer's external address so we can simultaneously open."
26///
27/// Real impl talks the `HolePunchRequest`/`HolePunchCoordinate` wire to the relay; tests supply a
28/// mock returning a canned peer address (or an error) so the method logic is verified with no relay.
29#[async_trait]
30pub trait HolePunchCoordinator: Send + Sync {
31 /// Exchange external addresses via the relay for `target_peer` and return the peer's external
32 /// address to dial. `Err` = coordination failed (peer offline, relay down, timeout).
33 async fn coordinate(
34 &self,
35 target_peer: &str,
36 network_id: &str,
37 my_external_addr: SocketAddr,
38 ) -> Result<SocketAddr, String>;
39}
40
41/// Forward through a shared trait object so [`crate::connect`] can compose a hole-punch method from an
42/// `Arc<dyn HolePunchCoordinator>` held in the runtime carrier (a generic [`HolePunchMethod<C>`] over
43/// the trait object).
44#[async_trait]
45impl HolePunchCoordinator for Arc<dyn HolePunchCoordinator> {
46 async fn coordinate(
47 &self,
48 target_peer: &str,
49 network_id: &str,
50 my_external_addr: SocketAddr,
51 ) -> Result<SocketAddr, String> {
52 (**self)
53 .coordinate(target_peer, network_id, my_external_addr)
54 .await
55 }
56}
57
58/// The relay-coordinated hole-punch method. Needs this node's own reflexive (STUN-discovered)
59/// external address to advertise, and a [`HolePunchCoordinator`] to exchange it with the peer.
60pub struct HolePunchMethod<C: HolePunchCoordinator> {
61 coordinator: C,
62 /// This node's server-reflexive address (from [`crate::stun::query_reflexive_address`]).
63 pub my_external_addr: SocketAddr,
64}
65
66impl<C: HolePunchCoordinator> HolePunchMethod<C> {
67 /// Build a hole-punch method over `coordinator`, advertising `my_external_addr`.
68 pub fn new(coordinator: C, my_external_addr: SocketAddr) -> Self {
69 HolePunchMethod {
70 coordinator,
71 my_external_addr,
72 }
73 }
74}
75
76#[async_trait]
77impl<C: HolePunchCoordinator> TraversalMethod for HolePunchMethod<C> {
78 fn kind(&self) -> TraversalKind {
79 TraversalKind::HolePunch
80 }
81
82 async fn attempt(&self, peer: &PeerTarget) -> Result<MethodOutcome, MethodError> {
83 let peer_addr = self
84 .coordinator
85 .coordinate(
86 &peer.peer_id.to_hex(),
87 &peer.network_id,
88 self.my_external_addr,
89 )
90 .await
91 .map_err(|e| MethodError::failed(TraversalKind::HolePunch, e))?;
92 // The coordinator returns the single peer address to simultaneously-open against (its family
93 // is whatever the peers exchanged via STUN — IPv6 when both have it).
94 Ok(MethodOutcome::single(TraversalKind::HolePunch, peer_addr))
95 }
96}