Skip to main content

urma_node/
peer_reach.rs

1//! NAT handling for the network loop (URMA_NODE_NAT.md §3.2–§3.6a):
2//! reachability from AutoNAT, the external-address policy, the relay
3//! reservation manager, relay server bookkeeping, hole punch outcomes, and the
4//! QUIC listener on the TCP listener's port.
5use crate::{
6    Error,
7    peer_external::{Externals, Verdict},
8    peer_gossip::{is_loopback, is_public},
9    peer_limits::{Class, Classes},
10    peer_nat::{Nat, PeerLink, Reachability},
11    peer_transport::{Behaviour, BehaviourEvent},
12};
13use libp2p::{
14    Multiaddr, PeerId, Swarm, autonat,
15    core::{ConnectedPoint, transport::ListenerId},
16    dcutr,
17    multiaddr::Protocol,
18    relay,
19    swarm::{ConnectionId, SwarmEvent},
20};
21use std::{
22    collections::{HashMap, HashSet},
23    io,
24    sync::{
25        Arc,
26        atomic::{AtomicBool, Ordering},
27    },
28    time::{Duration, Instant},
29};
30
31/// One live connection as the loop admitted it.
32pub(crate) struct Remote {
33    pub(crate) peer: PeerId,
34    pub(crate) address: Multiaddr,
35    pub(crate) outbound: bool,
36    pub(crate) class: Class,
37    since: Instant,
38    demoted: Option<Instant>,
39}
40
41/// Live connections by id. The class is kept here because admission forgets
42/// it before the loop sees the connection close.
43#[derive(Default)]
44pub(crate) struct Remotes(HashMap<ConnectionId, Remote>);
45
46impl Remotes {
47    /// Records an established connection with the class admission gave it.
48    pub(crate) fn admit(
49        &mut self,
50        (id, peer): (ConnectionId, PeerId),
51        endpoint: &ConnectedPoint,
52        classes: &Classes,
53    ) -> Class {
54        let class = match classes.of(id) {
55            Some(class) => class,
56            None => {
57                tracing::warn!(%peer, "connection without an admission class; kept off data");
58                Class::Auxiliary
59            }
60        };
61        let remote = Remote {
62            peer,
63            address: endpoint.get_remote_address().clone(),
64            outbound: endpoint.is_dialer(),
65            class,
66            since: Instant::now(),
67            demoted: None,
68        };
69        self.0.insert(id, remote);
70        class
71    }
72
73    pub(crate) fn remove(&mut self, id: ConnectionId) -> Option<Remote> {
74        self.0.remove(&id)
75    }
76
77    pub(crate) fn loopback(&self, id: ConnectionId) -> bool {
78        self.0
79            .get(&id)
80            .is_some_and(|remote| is_loopback(&remote.address))
81    }
82
83    /// Only peers we dialed directly may witness our address (§3.6a.4).
84    pub(crate) fn outbound_direct(&self, id: ConnectionId) -> bool {
85        self.0
86            .get(&id)
87            .is_some_and(|remote| remote.outbound && remote.class != Class::Relayed)
88    }
89
90    /// A relay circuit has no use once the peer holds a direct data link
91    /// (§3.4): the circuits to retire after `id` was admitted as `class`.
92    pub(crate) fn superseded(
93        &self,
94        id: ConnectionId,
95        peer: PeerId,
96        class: Class,
97    ) -> Vec<ConnectionId> {
98        let direct = self
99            .0
100            .values()
101            .any(|remote| remote.peer == peer && remote.class == Class::Data);
102        match class {
103            Class::Data => self.relayed_of(peer),
104            Class::Relayed if direct => vec![id],
105            Class::Relayed | Class::Auxiliary | Class::Holder | Class::Demoted => Vec::new(),
106        }
107    }
108
109    /// Holders admitted at least `grace` ago and links demoted at least
110    /// `grace` ago, oldest first, with whether each is a demoted link we
111    /// dialed to a public peer (an AutoNAT server or relay candidate).
112    fn retirable(&self, grace: Duration) -> Vec<(ConnectionId, PeerId, bool)> {
113        let mut retirable: Vec<(ConnectionId, PeerId, bool)> = self
114            .0
115            .iter()
116            .filter(|(_, remote)| match remote.class {
117                Class::Holder => remote.since.elapsed() >= grace,
118                Class::Demoted => remote.demoted.is_some_and(|at| at.elapsed() >= grace),
119                Class::Data | Class::Auxiliary | Class::Relayed => false,
120            })
121            .map(|(id, remote)| {
122                let seeker = remote.class == Class::Demoted && Self::candidate(remote);
123                (*id, remote.peer, seeker)
124            })
125            .collect();
126        retirable.sort_by_key(|(id, ..)| *id);
127        retirable
128    }
129
130    /// The peer's data connections, with whether we dialed each.
131    pub(crate) fn data_of(&self, peer: PeerId) -> Vec<(ConnectionId, bool)> {
132        self.0
133            .iter()
134            .filter(|(_, remote)| remote.peer == peer && remote.class == Class::Data)
135            .map(|(id, remote)| (*id, remote.outbound))
136            .collect()
137    }
138
139    pub(crate) fn reclass(&mut self, id: ConnectionId, class: Class) {
140        if let Some(remote) = self.0.get_mut(&id) {
141            remote.class = class;
142            remote.demoted = (class == Class::Demoted).then(Instant::now);
143        }
144    }
145
146    /// Peers whose data connection has been open `grace` without the peer
147    /// proving it speaks this protocol on our chain.
148    pub(crate) fn unproven(
149        &self,
150        grace: Duration,
151        proven: impl Fn(PeerId) -> bool,
152    ) -> HashSet<PeerId> {
153        self.0
154            .values()
155            .filter(|remote| remote.class == Class::Data && remote.since.elapsed() >= grace)
156            .map(|remote| remote.peer)
157            .filter(|peer| !proven(*peer))
158            .collect()
159    }
160
161    /// Connections demoted at least `grace` ago, with peer and direction.
162    pub(crate) fn demoted_after(&self, grace: Duration) -> Vec<(ConnectionId, PeerId, bool)> {
163        self.0
164            .iter()
165            .filter(|(_, remote)| remote.demoted.is_some_and(|at| at.elapsed() >= grace))
166            .map(|(id, remote)| (*id, remote.peer, remote.outbound))
167            .collect()
168    }
169
170    fn has_data(&self, peer: PeerId) -> bool {
171        self.0
172            .values()
173            .any(|remote| remote.peer == peer && remote.class == Class::Data)
174    }
175
176    fn relayed_of(&self, peer: PeerId) -> Vec<ConnectionId> {
177        self.0
178            .iter()
179            .filter(|(_, remote)| remote.peer == peer && remote.class == Class::Relayed)
180            .map(|(id, _)| *id)
181            .collect()
182    }
183
184    fn candidate(remote: &Remote) -> bool {
185        remote.outbound
186            && matches!(remote.class, Class::Data | Class::Demoted)
187            && is_public(&remote.address)
188    }
189
190    /// Public peers we hold an outbound data (or demoted) connection to,
191    /// oldest first, with the address we reached them on (without `/p2p`).
192    fn relay_candidates(&self) -> Vec<(PeerId, Multiaddr)> {
193        let mut candidates: Vec<(&ConnectionId, &Remote)> = self
194            .0
195            .iter()
196            .filter(|(_, remote)| Self::candidate(remote))
197            .collect();
198        candidates.sort_by_key(|(id, _)| **id);
199        candidates
200            .into_iter()
201            .map(|(_, remote)| (remote.peer, without_peer(&remote.address)))
202            .collect()
203    }
204}
205
206pub(crate) fn without_peer(address: &Multiaddr) -> Multiaddr {
207    address
208        .iter()
209        .filter(|part| !matches!(part, Protocol::P2p(_)))
210        .collect()
211}
212
213/// `/ip4|ip6/<public literal>/tcp/<port>` or `/udp/<port>/quic-v1`: the shapes
214/// this node advertises or offers as its own address.
215pub(crate) fn direct_shape(address: &Multiaddr) -> bool {
216    let mut parts = address.iter();
217    let ip = matches!(parts.next(), Some(Protocol::Ip4(_) | Protocol::Ip6(_)));
218    let transport = match (parts.next(), parts.next()) {
219        (Some(Protocol::Tcp(port)), None) => port != 0,
220        (Some(Protocol::Udp(port)), Some(Protocol::QuicV1)) => port != 0 && parts.next().is_none(),
221        _ => false,
222    };
223    ip && transport && is_public(address)
224}
225
226/// An operator announce address: a public IP literal with TCP or UDP/QUIC v1,
227/// ending at most in this node's own /p2p. Returned without /p2p, as identify
228/// advertises it.
229pub(crate) fn validate_announce(address: &Multiaddr, local: PeerId) -> Result<Multiaddr, Error> {
230    let mut bare = address.clone();
231    if let Some(Protocol::P2p(named)) = address.iter().last() {
232        if named != local {
233            return Err(Error::Invalid("announce address names another peer".into()));
234        }
235        bare.pop();
236    }
237    if !is_public(&bare) {
238        return Err(Error::Invalid("announce address must be public".into()));
239    }
240    if !direct_shape(&bare) {
241        return Err(Error::Invalid(
242            "announce address must be an IP literal with TCP or UDP/QUIC v1".into(),
243        ));
244    }
245    Ok(bare)
246}
247
248/// How the node decides its reachability: from AutoNAT probes, or fixed (a
249/// test stand-in for what AutoNAT would conclude).
250#[derive(Clone, Copy, Debug, PartialEq, Eq)]
251pub enum ReachMode {
252    AutoNat,
253    Forced(Reachability),
254}
255
256pub(crate) struct Reach {
257    forced: ReachMode,
258    externals: Externals,
259    probe_failed: bool,
260    relay_open: Arc<AtomicBool>,
261    local: PeerId,
262    listen: Option<Multiaddr>,
263    quic: bool,
264    reservations: HashMap<ListenerId, PeerId>,
265    backoff: HashMap<PeerId, Instant>,
266    circuits: HashMap<PeerId, u32>,
267}
268
269impl Reach {
270    const RESERVATIONS: usize = 2;
271    const BACKOFF: Duration = Duration::from_secs(10 * 60);
272    const HOLDER_GRACE: Duration = Duration::from_secs(10);
273
274    pub(crate) fn new(
275        forced: ReachMode,
276        announce: Vec<Multiaddr>,
277        relay_open: Arc<AtomicBool>,
278        (local, listen): (PeerId, Option<Multiaddr>),
279    ) -> Self {
280        Self {
281            forced,
282            externals: Externals::new(local, announce),
283            probe_failed: false,
284            relay_open,
285            local,
286            listen,
287            quic: false,
288            reservations: HashMap::new(),
289            backoff: HashMap::new(),
290            circuits: HashMap::new(),
291        }
292    }
293
294    /// Public with an announced or AutoNAT-confirmed direct address, private
295    /// once a probe failed without one, unknown before that; forced in tests.
296    pub(crate) fn update(&self, nat: &Nat) {
297        let observed = match (
298            self.externals.announced() || !nat.external().is_empty(),
299            self.probe_failed,
300        ) {
301            (true, _) => Reachability::Public,
302            (false, true) => Reachability::Private,
303            (false, false) => Reachability::Unknown,
304        };
305        let reachability = match self.forced {
306            ReachMode::Forced(reachability) => reachability,
307            ReachMode::AutoNat => observed,
308        };
309        nat.set_reachability(reachability);
310        self.relay_open
311            .store(reachability == Reachability::Public, Ordering::Release);
312    }
313
314    /// Takes the loop's NAT events and hands every other event back.
315    pub(crate) fn absorb(
316        &mut self,
317        swarm: &mut Swarm<Behaviour>,
318        remotes: &Remotes,
319        nat: &Nat,
320        event: SwarmEvent<BehaviourEvent>,
321    ) -> Option<SwarmEvent<BehaviourEvent>> {
322        match event {
323            SwarmEvent::Behaviour(BehaviourEvent::Autonat(event)) => {
324                self.autonat(swarm, &event, nat)
325            }
326            SwarmEvent::Behaviour(BehaviourEvent::AutonatServer(event)) => {
327                tracing::debug!(?event, "AutoNAT probe served")
328            }
329            SwarmEvent::Behaviour(BehaviourEvent::Relay(event)) => self.relay_server(&event, nat),
330            SwarmEvent::Behaviour(BehaviourEvent::RelayClient(event)) => {
331                Self::relay_client(&event, nat)
332            }
333            SwarmEvent::Behaviour(BehaviourEvent::Dcutr(event)) => {
334                Self::dcutr(swarm, remotes, event, nat)
335            }
336            SwarmEvent::ExternalAddrConfirmed { address } => self.external(address, true, nat),
337            SwarmEvent::ExternalAddrExpired { address } => self.external(address, false, nat),
338            SwarmEvent::NewListenAddr { ref address, .. }
339                if self.listen_addr(swarm, address, nat) => {}
340            SwarmEvent::ExpiredListenAddr { ref address, .. }
341                if self.listen_expired(address, nat) => {}
342            SwarmEvent::ListenerClosed {
343                listener_id,
344                ref addresses,
345                ref reason,
346            } => {
347                for address in addresses {
348                    self.listen_expired(address, nat);
349                }
350                self.listener_closed(listener_id, reason, nat);
351                return Some(event);
352            }
353            other => return Some(other),
354        }
355        None
356    }
357
358    /// A failure counts toward private whoever reports it; a success is
359    /// advertised only as `Externals` judges it.
360    fn autonat(
361        &mut self,
362        swarm: &mut Swarm<Behaviour>,
363        event: &autonat::v2::client::Event,
364        nat: &Nat,
365    ) {
366        if let Err(error) = &event.result {
367            tracing::debug!(%error, address = %event.tested_addr, "AutoNAT probe failed");
368            self.probe_failed = true;
369            self.update(nat);
370            return;
371        }
372        match self.externals.verdict(&event.tested_addr, event.server) {
373            Verdict::Confirm(address) => self.advertise(swarm, address, nat),
374            Verdict::Retry(address) => swarm.behaviour_mut().candidates.offer(address),
375            Verdict::Ignore => {
376                tracing::debug!(address = %event.tested_addr, server = %event.server,
377                    "AutoNAT confirmation not counted")
378            }
379        }
380    }
381
382    /// `witness`, a peer we dialed directly, sees us at `observed`: an AutoNAT
383    /// candidate, never an advertised address on its own, and evidence that a
384    /// confirmed address of the same transport no longer maps to us.
385    pub(crate) fn observed(
386        &mut self,
387        (swarm, remotes): (&mut Swarm<Behaviour>, &Remotes),
388        (observed, witness): (Multiaddr, PeerId),
389        nat: &Nat,
390    ) {
391        if !direct_shape(&observed) {
392            return;
393        }
394        let reported = (witness, Instant::now());
395        let data = |peer: &PeerId| remotes.has_data(*peer);
396        for stale in self
397            .externals
398            .witnessed(&observed, reported, &nat.external(), data)
399        {
400            tracing::info!(%stale, now = %observed, "external address mapping changed");
401            swarm.remove_external_address(&stale);
402            self.external(stale, false, nat);
403        }
404        if self.externals.flooded(witness) {
405            tracing::debug!(%witness, "witness reports too many addresses; ignored");
406            return;
407        }
408        swarm.behaviour_mut().candidates.offer(observed);
409    }
410
411    /// Advertises `address` as ours. The swarm reports no event for its own
412    /// additions, so the NAT state is updated here.
413    pub(crate) fn advertise(&self, swarm: &mut Swarm<Behaviour>, address: Multiaddr, nat: &Nat) {
414        swarm.add_external_address(address.clone());
415        self.external(address, true, nat);
416    }
417
418    /// Circuit addresses the relay client confirms stay out of the direct set,
419    /// and with announced addresses nothing else is advertised.
420    pub(crate) fn external(&self, address: Multiaddr, confirmed: bool, nat: &Nat) {
421        if PeerLink::of(&address) == PeerLink::Direct && self.externals.admits(&address) {
422            nat.set_external(address, confirmed);
423            self.update(nat);
424        }
425    }
426
427    fn relay_server(&mut self, event: &relay::Event, nat: &Nat) {
428        match event {
429            relay::Event::ReservationReqAccepted { src_peer_id, .. } => {
430                nat.set_reserved_by(*src_peer_id, true)
431            }
432            relay::Event::ReservationClosed { src_peer_id }
433            | relay::Event::ReservationTimedOut { src_peer_id } => {
434                nat.set_reserved_by(*src_peer_id, false)
435            }
436            relay::Event::CircuitReqAccepted {
437                src_peer_id,
438                dst_peer_id,
439            } => {
440                for peer in [src_peer_id, dst_peer_id] {
441                    *self.circuits.entry(*peer).or_default() += 1;
442                }
443            }
444            relay::Event::CircuitClosed {
445                src_peer_id,
446                dst_peer_id,
447                ..
448            } => {
449                for peer in [src_peer_id, dst_peer_id] {
450                    self.circuit_closed(*peer);
451                }
452            }
453            _ => (),
454        }
455    }
456
457    fn circuit_closed(&mut self, peer: PeerId) {
458        let Some(count) = self.circuits.get_mut(&peer) else {
459            return;
460        };
461        *count -= 1;
462        if *count == 0 {
463            self.circuits.remove(&peer);
464        }
465    }
466
467    /// A holder (admitted past the data budget, or demoted from data) must
468    /// carry a reservation either way or a circuit within the grace period.
469    /// While this node's reachability is unknown, or it is private with relays
470    /// missing, the oldest few demoted links it dialed to public peers stay
471    /// too: AutoNAT and reservations need them, and they are ours, not cheap
472    /// inbound links.
473    fn retire_holders(&self, swarm: &mut Swarm<Behaviour>, remotes: &Remotes, nat: &Nat) {
474        let relays = nat.relays();
475        let seeking = match nat.reachability() {
476            Reachability::Public => 0,
477            Reachability::Unknown => Self::RESERVATIONS,
478            Reachability::Private => Self::RESERVATIONS.saturating_sub(relays.len()),
479        };
480        let mut holding = nat.reserved_by_others();
481        holding.extend(relays);
482        let mut kept = 0;
483        for (id, peer, seeker) in remotes.retirable(Self::HOLDER_GRACE) {
484            if holding.contains(&peer) || self.circuits.contains_key(&peer) {
485                continue;
486            }
487            if seeker && kept < seeking {
488                kept += 1;
489                continue;
490            }
491            tracing::debug!(%peer, "holder carries no reservation or circuit; closing");
492            swarm.close_connection(id);
493        }
494    }
495
496    pub(crate) fn relay_client(event: &relay::client::Event, nat: &Nat) {
497        if let relay::client::Event::ReservationReqAccepted { relay_peer_id, .. } = event {
498            nat.set_relay(*relay_peer_id, true);
499        }
500    }
501
502    /// A punched direct link retires the circuit; a failure is recorded and
503    /// the next relayed contact tries again.
504    pub(crate) fn dcutr(
505        swarm: &mut Swarm<Behaviour>,
506        remotes: &Remotes,
507        event: dcutr::Event,
508        nat: &Nat,
509    ) {
510        match event.result {
511            Ok(_direct) => {
512                nat.punch_succeeded();
513                for id in remotes.relayed_of(event.remote_peer_id) {
514                    swarm.close_connection(id);
515                }
516            }
517            Err(error) => nat.punch_failed(format!("{}: {error}", event.remote_peer_id)),
518        }
519    }
520
521    /// Returns true when the address is a relay circuit, kept out of the
522    /// direct listeners; the first TCP listener address opens QUIC beside it.
523    pub(crate) fn listen_addr(
524        &mut self,
525        swarm: &mut Swarm<Behaviour>,
526        address: &Multiaddr,
527        nat: &Nat,
528    ) -> bool {
529        if PeerLink::of(address) == PeerLink::Relayed {
530            nat.set_circuit(self.with_local(address), true);
531            return true;
532        }
533        self.open_quic(swarm, address);
534        if let Some(candidate) = self.externals.bound(address) {
535            swarm.behaviour_mut().candidates.offer(candidate);
536        }
537        false
538    }
539
540    pub(crate) fn listen_expired(&self, address: &Multiaddr, nat: &Nat) -> bool {
541        let circuit = PeerLink::of(address) == PeerLink::Relayed;
542        if circuit {
543            nat.set_circuit(self.with_local(address), false);
544        }
545        circuit
546    }
547
548    pub(crate) fn listener_closed(
549        &mut self,
550        id: ListenerId,
551        reason: &Result<(), io::Error>,
552        nat: &Nat,
553    ) {
554        let Some(relay) = self.reservations.remove(&id) else {
555            return;
556        };
557        nat.set_relay(relay, false);
558        if reason.is_err() {
559            self.backoff.insert(relay, Instant::now() + Self::BACKOFF);
560        }
561    }
562
563    fn with_local(&self, address: &Multiaddr) -> Multiaddr {
564        match address.iter().last() {
565            Some(Protocol::P2p(_)) => address.clone(),
566            _ => address.clone().with(Protocol::P2p(self.local)),
567        }
568    }
569
570    fn open_quic(&mut self, swarm: &mut Swarm<Behaviour>, bound: &Multiaddr) {
571        let Some(listen) = &self.listen else {
572            return;
573        };
574        let port = bound.iter().find_map(|part| match part {
575            Protocol::Tcp(port) => Some(port),
576            _ => None,
577        });
578        let (Some(port), Some(ip), false) = (port, listen.iter().next(), self.quic) else {
579            return;
580        };
581        self.quic = true;
582        let quic = Multiaddr::empty()
583            .with(ip)
584            .with(Protocol::Udp(port))
585            .with(Protocol::QuicV1);
586        if let Err(error) = swarm.listen_on(quic.clone()) {
587            tracing::warn!(%error, %quic, "QUIC listener unavailable; TCP only");
588        }
589    }
590
591    /// While private, holds up to two reservations on public peers we dialed
592    /// (seeds first), skipping relays that recently failed; otherwise none.
593    pub(crate) fn maintain(&mut self, swarm: &mut Swarm<Behaviour>, remotes: &Remotes, nat: &Nat) {
594        self.retire_holders(swarm, remotes, nat);
595        if nat.reachability() != Reachability::Private {
596            for (id, _) in self.reservations.drain() {
597                swarm.remove_listener(id);
598            }
599            return;
600        }
601        let now = Instant::now();
602        self.backoff.retain(|_, until| *until > now);
603        for (relay, address) in remotes.relay_candidates() {
604            if self.reservations.len() >= Self::RESERVATIONS {
605                return;
606            }
607            let held = self.reservations.values().any(|known| *known == relay);
608            if held || relay == self.local || self.backoff.contains_key(&relay) {
609                continue;
610            }
611            let circuit = address
612                .with(Protocol::P2p(relay))
613                .with(Protocol::P2pCircuit);
614            match swarm.listen_on(circuit) {
615                Ok(id) => {
616                    self.reservations.insert(id, relay);
617                }
618                Err(error) => {
619                    tracing::warn!(%error, %relay, "relay reservation not attempted");
620                    self.backoff.insert(relay, now + Self::BACKOFF);
621                }
622            }
623        }
624    }
625}