Skip to main content

urma_node/
peer_nat.rs

1//! NAT state the network thread maintains and the owner reads: reachability
2//! from AutoNAT, the external addresses it confirmed, relay reservations held
3//! by this node and on it, the circuit addresses it listens on, and hole
4//! punching counters. A relayed connection only reaches a peer and hole
5//! punches; data never rides it.
6use crate::{peer_gossip::guard, recorded::RecordedError};
7use libp2p::{Multiaddr, PeerId, multiaddr::Protocol};
8use std::{collections::BTreeSet, fmt::Display, sync::Mutex};
9
10/// How a connection reaches its peer: over its own transport, or through a
11/// relay's circuit.
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum PeerLink {
14    Direct,
15    Relayed,
16}
17
18impl PeerLink {
19    /// A connection whose remote address passes through `/p2p-circuit`.
20    pub fn of(remote: &Multiaddr) -> Self {
21        match remote.iter().any(|part| part == Protocol::P2pCircuit) {
22            true => Self::Relayed,
23            false => Self::Direct,
24        }
25    }
26
27    pub fn as_str(self) -> &'static str {
28        match self {
29            Self::Direct => "direct",
30            Self::Relayed => "relayed",
31        }
32    }
33}
34
35/// Whether peers outside can dial this node, as AutoNAT last concluded.
36#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
37pub enum Reachability {
38    #[default]
39    Unknown,
40    Public,
41    Private,
42}
43
44impl Reachability {
45    pub fn as_str(self) -> &'static str {
46        match self {
47            Self::Unknown => "unknown",
48            Self::Public => "public",
49            Self::Private => "private",
50        }
51    }
52}
53
54/// DCUtR counters since start, with the latest failure.
55#[derive(Clone, Debug, Default)]
56pub(crate) struct HolePunch {
57    pub(crate) attempts: u64,
58    pub(crate) successes: u64,
59    pub(crate) last_error: Option<RecordedError>,
60}
61
62#[derive(Default)]
63pub(crate) struct Nat {
64    reachability: Mutex<Reachability>,
65    external: Mutex<Vec<Multiaddr>>,
66    relays: Mutex<BTreeSet<PeerId>>,
67    reserved_by_others: Mutex<BTreeSet<PeerId>>,
68    circuits: Mutex<Vec<Multiaddr>>,
69    punch: Mutex<HolePunch>,
70}
71
72fn mark(set: &Mutex<BTreeSet<PeerId>>, peer: PeerId, active: bool) {
73    let mut set = guard(set);
74    match active {
75        true => set.insert(peer),
76        false => set.remove(&peer),
77    };
78}
79
80fn place(list: &Mutex<Vec<Multiaddr>>, address: Multiaddr, present: bool) {
81    let mut list = guard(list);
82    list.retain(|known| *known != address);
83    if present {
84        list.push(address);
85    }
86}
87
88impl Nat {
89    pub(crate) fn reachability(&self) -> Reachability {
90        *guard(&self.reachability)
91    }
92
93    pub(crate) fn set_reachability(&self, reachability: Reachability) {
94        *guard(&self.reachability) = reachability;
95    }
96
97    /// Addresses AutoNAT confirmed or the operator announced, as advertised,
98    /// without `/p2p`.
99    pub(crate) fn external(&self) -> Vec<Multiaddr> {
100        guard(&self.external).clone()
101    }
102
103    pub(crate) fn set_external(&self, address: Multiaddr, confirmed: bool) {
104        place(&self.external, address, confirmed);
105    }
106
107    /// Relays holding an accepted reservation for this node.
108    pub(crate) fn relays(&self) -> Vec<PeerId> {
109        guard(&self.relays).iter().copied().collect()
110    }
111
112    pub(crate) fn set_relay(&self, relay: PeerId, active: bool) {
113        mark(&self.relays, relay, active);
114    }
115
116    /// Peers holding an active reservation on this node's relay server.
117    pub(crate) fn reserved_by_others(&self) -> Vec<PeerId> {
118        guard(&self.reserved_by_others).iter().copied().collect()
119    }
120
121    pub(crate) fn set_reserved_by(&self, holder: PeerId, active: bool) {
122        mark(&self.reserved_by_others, holder, active);
123    }
124
125    /// `/p2p-circuit` addresses this node listens on through its relays.
126    pub(crate) fn circuits(&self) -> Vec<Multiaddr> {
127        guard(&self.circuits).clone()
128    }
129
130    pub(crate) fn set_circuit(&self, address: Multiaddr, bound: bool) {
131        place(&self.circuits, address, bound);
132    }
133
134    pub(crate) fn hole_punch(&self) -> HolePunch {
135        guard(&self.punch).clone()
136    }
137
138    pub(crate) fn punch_succeeded(&self) {
139        let mut punch = guard(&self.punch);
140        punch.attempts += 1;
141        punch.successes += 1;
142    }
143
144    pub(crate) fn punch_failed(&self, error: impl Display) {
145        let mut punch = guard(&self.punch);
146        punch.attempts += 1;
147        punch.last_error = Some(RecordedError::now(error));
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use crate::{peer_gossip::Shared, peer_protocol::Reply, peerbook::PeerBook};
155    use bitcoin::{BlockHash, hashes::Hash};
156    use libp2p::identity::Keypair;
157
158    fn peer() -> PeerId {
159        Keypair::generate_ed25519().public().to_peer_id()
160    }
161
162    #[test]
163    fn a_relay_serves_its_reservation_holders_as_circuits_a_book_stores() {
164        let relay = peer();
165        let shared = Shared::new(relay);
166        let (holder, requester) = (peer(), peer());
167        shared.nat.set_reserved_by(holder, true);
168        shared.nat.set_reserved_by(requester, true);
169        assert!(
170            matches!(shared.serve(requester, false, 8), Reply::Peers(peers) if peers.is_empty())
171        );
172        let external: Multiaddr = "/ip4/11.0.0.1/tcp/28332".parse().unwrap();
173        shared.nat.set_external(external.clone(), true);
174        let Reply::Peers(peers) = shared.serve(requester, false, 8) else {
175            panic!("peers request refused");
176        };
177        let hop = external
178            .with(Protocol::P2p(relay))
179            .with(Protocol::P2pCircuit);
180        assert_eq!(peers.len(), 1, "the requester is never listed");
181        assert_eq!(
182            (peers[0].peer_id, peers[0].addrs.clone()),
183            (holder, vec![hop.clone()])
184        );
185        let mut book = PeerBook::empty(BlockHash::from_byte_array([42; 32]));
186        assert_eq!(book.merge(&peers, requester, &[]), 1);
187        let entry = &book.entries()[0];
188        assert_eq!(
189            (entry.peer_id, entry.addrs.clone()),
190            (Some(holder), vec![hop])
191        );
192        assert!(entry.circuit_only());
193    }
194
195    #[test]
196    fn reservations_circuits_and_punch_counters_track_their_latest_state() {
197        let nat = Nat::default();
198        let (relay, holder) = (peer(), peer());
199        nat.set_relay(relay, true);
200        nat.set_reserved_by(holder, true);
201        assert_eq!(
202            (nat.relays(), nat.reserved_by_others()),
203            (vec![relay], vec![holder])
204        );
205        nat.set_reserved_by(holder, false);
206        assert!(nat.reserved_by_others().is_empty());
207        let circuit: Multiaddr = format!("/ip4/1.2.3.4/tcp/1/p2p/{relay}/p2p-circuit")
208            .parse()
209            .unwrap();
210        nat.set_circuit(circuit.clone(), true);
211        nat.set_circuit(circuit.clone(), true);
212        assert_eq!(nat.circuits(), vec![circuit]);
213        nat.punch_succeeded();
214        nat.punch_failed("timeout");
215        let punch = nat.hole_punch();
216        assert_eq!((punch.attempts, punch.successes), (2, 1));
217        assert_eq!(
218            punch.last_error.map(|error| error.message).as_deref(),
219            Some("timeout")
220        );
221        assert_eq!(nat.reachability(), Reachability::Unknown);
222    }
223}