Skip to main content

dig_nat/
peer.rs

1//! The peer the caller wants to reach ([`PeerTarget`]) and the connection they get back
2//! ([`PeerConnection`]).
3//!
4//! To the caller, [`crate::connect`] "just connects to a peer" — they describe the peer once and
5//! receive an mTLS-authenticated [`PeerConnection`] whose remote `peer_id` has been verified. Which
6//! traversal method got there is reported (observability) but never something the caller must
7//! choose or handle.
8
9use std::net::SocketAddr;
10
11use crate::identity::PeerId;
12use crate::method::TraversalKind;
13use crate::mux::{PeerSession, PeerStream};
14
15/// A description of the peer to connect to.
16///
17/// The caller supplies the peer's stable [`PeerId`] (for mTLS verification) and any candidate
18/// addresses it knows (from discovery / the address manager / the relay peer list). At least one
19/// direct candidate OR `peer_id`+relay reachability is needed; the strategy uses the candidate list
20/// for the direct/mapping methods and `peer_id` for the relay-coordinated + relayed methods.
21///
22/// ## Address-family policy — IPv6-first, IPv4-fallback
23///
24/// The candidate list is stored **IPv6-first**: every IPv6 candidate precedes every IPv4 candidate,
25/// with the relative order WITHIN each family preserved (a stable sort). This is the foundation of
26/// the ecosystem's IPv6-first / IPv4-fallback rule — the dialer walks the candidates in order
27/// (happy-eyeballs, [`crate::dialer`]) so a peer reachable over IPv6 is dialed over IPv6, and IPv4 is
28/// used only when the IPv6 candidate(s) fail. Ordering is decided by the address FAMILY
29/// ([`SocketAddr::is_ipv6`]), never by a string heuristic. Use [`PeerTarget::with_addrs`] to supply
30/// several candidates (they are sorted for you); [`PeerTarget::with_addr`] for a single one.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct PeerTarget {
33    /// The peer's network identity — SHA-256 of its TLS SPKI DER. Used to VERIFY the mTLS peer and
34    /// to address it over the relay. Required.
35    pub peer_id: PeerId,
36
37    /// The peer's directly-dialable candidate `ip:port`s, ordered **IPv6-first** (see the
38    /// type-level address-family policy). Empty when the peer is only reachable via the relay.
39    /// Prefer the [`PeerTarget::with_addrs`]/[`PeerTarget::with_addr`] constructors (which enforce
40    /// the ordering) or [`PeerTarget::set_direct_addrs`] over mutating this directly; read it via
41    /// [`PeerTarget::direct_addrs`] (all, ordered) or [`PeerTarget::direct_addr`] (the
42    /// IPv6-preferred first candidate).
43    direct_addrs: Vec<SocketAddr>,
44
45    /// The network id the peer registered under (relay `network_id`, e.g. `DIG_MAINNET`). Used to
46    /// scope relay peer lookups + hole-punch coordination.
47    pub network_id: String,
48}
49
50impl PeerTarget {
51    /// A peer known by a single direct address (public / port-forwarded / discovered).
52    pub fn with_addr(
53        peer_id: PeerId,
54        direct_addr: SocketAddr,
55        network_id: impl Into<String>,
56    ) -> Self {
57        PeerTarget::with_addrs(peer_id, vec![direct_addr], network_id)
58    }
59
60    /// A peer known by one OR MORE direct candidate addresses. The candidates are stored
61    /// **IPv6-first** (IPv6 candidates before IPv4, order-preserving within each family) so the
62    /// dialer prefers IPv6 and falls back to IPv4 — pass them in any order.
63    pub fn with_addrs(
64        peer_id: PeerId,
65        direct_addrs: Vec<SocketAddr>,
66        network_id: impl Into<String>,
67    ) -> Self {
68        let mut t = PeerTarget {
69            peer_id,
70            direct_addrs,
71            network_id: network_id.into(),
72        };
73        sort_ipv6_first(&mut t.direct_addrs);
74        t
75    }
76
77    /// A peer known only by identity — reachable via relay-coordinated methods.
78    pub fn relay_only(peer_id: PeerId, network_id: impl Into<String>) -> Self {
79        PeerTarget {
80            peer_id,
81            direct_addrs: Vec::new(),
82            network_id: network_id.into(),
83        }
84    }
85
86    /// The peer's candidate addresses, ordered IPv6-first (the dial order). Empty for a relay-only
87    /// target.
88    pub fn direct_addrs(&self) -> &[SocketAddr] {
89        &self.direct_addrs
90    }
91
92    /// The single best (IPv6-preferred) candidate address, or `None` for a relay-only target.
93    ///
94    /// This is the backwards-compatible accessor for callers that want ONE address: it returns the
95    /// first candidate, which — because the list is IPv6-first — is the IPv6 candidate when one is
96    /// known, otherwise the IPv4 fallback. Prefer [`PeerTarget::direct_addrs`] to honour the full
97    /// happy-eyeballs fallback.
98    pub fn direct_addr(&self) -> Option<SocketAddr> {
99        self.direct_addrs.first().copied()
100    }
101
102    /// Replace the candidate list, re-establishing the IPv6-first ordering.
103    pub fn set_direct_addrs(&mut self, addrs: Vec<SocketAddr>) {
104        self.direct_addrs = addrs;
105        sort_ipv6_first(&mut self.direct_addrs);
106    }
107}
108
109/// Sort a candidate list **IPv6-first**: every IPv6 address precedes every IPv4 address, preserving
110/// the relative order within each family (a stable sort). This is the single place the IPv6-first
111/// ordering rule is applied. Ordering is decided by the address FAMILY ([`SocketAddr::is_ipv6`]),
112/// never by inspecting the string form (a bracketed `[v6]:port` and an `v4:port` both contain ':').
113pub fn sort_ipv6_first(addrs: &mut [SocketAddr]) {
114    // Stable sort with IPv6 (key 0) before IPv4 (key 1); ties keep input order.
115    addrs.sort_by_key(|a| if a.is_ipv6() { 0u8 } else { 1u8 });
116}
117
118/// Whether `addrs` is ALREADY ordered IPv6-first (every IPv6 candidate at or before every IPv4
119/// candidate) — i.e. [`sort_ipv6_first`] would be a no-op.
120///
121/// #179 finding 5 (optimization): the dial hot path ([`crate::dialer::happy_eyeballs_connect`])
122/// previously cloned + re-sorted the candidate slice on EVERY connect attempt even though callers
123/// (e.g. [`PeerTarget::direct_addrs`], [`crate::method::MethodOutcome::candidates`]) already produce
124/// IPv6-first-ordered lists. This cheap `O(n)` check lets the hot path skip the clone+sort when it
125/// would be redundant, while still defensively re-sorting when the input genuinely is not ordered
126/// (the ordering guarantee is never dropped — see [`crate::dialer::happy_eyeballs_connect`]).
127pub fn is_ipv6_first(addrs: &[SocketAddr]) -> bool {
128    // Equivalent to `addrs.is_sorted_by_key(...)`, hand-rolled because `is_sorted_by_key` only
129    // stabilized in Rust 1.82 and this crate's MSRV is 1.75.0.
130    fn family_key(a: &SocketAddr) -> u8 {
131        if a.is_ipv6() {
132            0
133        } else {
134            1
135        }
136    }
137    addrs
138        .windows(2)
139        .all(|w| family_key(&w[0]) <= family_key(&w[1]))
140}
141
142/// An established, mutually-authenticated, **multiplexed** connection to a peer.
143///
144/// The connection is one mTLS byte stream whose remote presented a certificate whose `peer_id`
145/// equals [`Self::peer_id`] (verified during the handshake by [`crate::mtls::PeerIdPinningVerifier`]),
146/// wrapped in a [`PeerSession`] so the caller can open **many concurrent logical streams**
147/// ([`open_stream`](Self::open_stream)) or **byte-range streams**
148/// ([`open_range_stream`](Self::open_range_stream)) — streaming-first, no head-of-line blocking.
149/// [`Self::method`] reports which traversal technique established it — observability only; the caller
150/// opens streams identically regardless of the tier.
151pub struct PeerConnection {
152    /// The verified remote identity (== the [`PeerTarget::peer_id`] the caller asked for).
153    pub peer_id: PeerId,
154    /// The traversal technique that established this connection (Direct, Upnp, …, Relayed).
155    pub method: TraversalKind,
156    /// The remote address the mTLS session runs over (the peer's endpoint, or the relay for a
157    /// relayed transport).
158    pub remote_addr: SocketAddr,
159    /// The multiplexed session over the authenticated, encrypted byte stream to the peer.
160    pub session: PeerSession,
161}
162
163impl PeerConnection {
164    /// Open a new concurrent logical stream to the peer (cheap; open as many as you need for
165    /// simultaneous transfers without head-of-line blocking).
166    pub async fn open_stream(&mut self) -> std::io::Result<PeerStream> {
167        self.session.open_stream().await
168    }
169
170    /// Open a `dig.fetchRange` stream for `req` (writes the range-request preamble, then streams
171    /// [`crate::mux::RangeFrame`]s). The primitive for multi-source parallel range downloads.
172    pub async fn open_range_stream(
173        &mut self,
174        req: &crate::mux::RangeRequest,
175    ) -> std::io::Result<PeerStream> {
176        self.session.open_range_stream(req).await
177    }
178
179    /// Availability pre-check (`dig.getAvailability`) — ask whether this peer holds `items` BEFORE
180    /// opening range streams (see [`crate::mux::PeerSession::query_availability`]).
181    pub async fn query_availability(
182        &mut self,
183        items: Vec<crate::mux::AvailabilityItem>,
184    ) -> std::io::Result<crate::mux::AvailabilityResponse> {
185        self.session.query_availability(items).await
186    }
187}
188
189impl std::fmt::Debug for PeerConnection {
190    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191        f.debug_struct("PeerConnection")
192            .field("peer_id", &self.peer_id)
193            .field("method", &self.method)
194            .field("remote_addr", &self.remote_addr)
195            .field("session", &"<multiplexed mTLS session>")
196            .finish()
197    }
198}