Skip to main content

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}