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;
16
17use async_trait::async_trait;
18
19use crate::error::MethodError;
20use crate::method::{MethodOutcome, TraversalKind, TraversalMethod};
21use crate::peer::PeerTarget;
22
23/// Abstraction over the relay's RLY-007 hole-punch coordination: "given the target peer and my
24/// external address, tell me the peer's external address so we can simultaneously open."
25///
26/// Real impl talks the `HolePunchRequest`/`HolePunchCoordinate` wire to the relay; tests supply a
27/// mock returning a canned peer address (or an error) so the method logic is verified with no relay.
28#[async_trait]
29pub trait HolePunchCoordinator: Send + Sync {
30 /// Exchange external addresses via the relay for `target_peer` and return the peer's external
31 /// address to dial. `Err` = coordination failed (peer offline, relay down, timeout).
32 async fn coordinate(
33 &self,
34 target_peer: &str,
35 network_id: &str,
36 my_external_addr: SocketAddr,
37 ) -> Result<SocketAddr, String>;
38}
39
40/// The relay-coordinated hole-punch method. Needs this node's own reflexive (STUN-discovered)
41/// external address to advertise, and a [`HolePunchCoordinator`] to exchange it with the peer.
42pub struct HolePunchMethod<C: HolePunchCoordinator> {
43 coordinator: C,
44 /// This node's server-reflexive address (from [`crate::stun::query_reflexive_address`]).
45 pub my_external_addr: SocketAddr,
46}
47
48impl<C: HolePunchCoordinator> HolePunchMethod<C> {
49 /// Build a hole-punch method over `coordinator`, advertising `my_external_addr`.
50 pub fn new(coordinator: C, my_external_addr: SocketAddr) -> Self {
51 HolePunchMethod {
52 coordinator,
53 my_external_addr,
54 }
55 }
56}
57
58#[async_trait]
59impl<C: HolePunchCoordinator> TraversalMethod for HolePunchMethod<C> {
60 fn kind(&self) -> TraversalKind {
61 TraversalKind::HolePunch
62 }
63
64 async fn attempt(&self, peer: &PeerTarget) -> Result<MethodOutcome, MethodError> {
65 let peer_addr = self
66 .coordinator
67 .coordinate(
68 &peer.peer_id.to_hex(),
69 &peer.network_id,
70 self.my_external_addr,
71 )
72 .await
73 .map_err(|e| MethodError::failed(TraversalKind::HolePunch, e))?;
74 // The coordinator returns the single peer address to simultaneously-open against (its family
75 // is whatever the peers exchanged via STUN — IPv6 when both have it).
76 Ok(MethodOutcome::single(TraversalKind::HolePunch, peer_addr))
77 }
78}