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}