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 (via `dig-ip`)
23///
24/// The candidate list is stored in DISCOVERY order — the IPv6-first preference + the local∩peer
25/// family intersection are applied at DIAL time by the canonical `dig-ip` crate ([`crate::dialer`]),
26/// not by this type. So a peer reachable over IPv6 is dialed over IPv6 and IPv4 is used only as a
27/// fallback, but a caller supplies candidates in whatever order it discovered them. Use
28/// [`PeerTarget::with_addrs`] to supply several candidates; [`PeerTarget::with_addr`] for a single
29/// one.
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct PeerTarget {
32 /// The peer's network identity — SHA-256 of its TLS SPKI DER. Used to VERIFY the mTLS peer and
33 /// to address it over the relay. Required.
34 pub peer_id: PeerId,
35
36 /// The peer's directly-dialable candidate `ip:port`s, in discovery order (family selection +
37 /// IPv6-first preference are applied at dial time by `dig-ip`; see the type-level address-family
38 /// policy). Empty when the peer is only reachable via the relay. Prefer the
39 /// [`PeerTarget::with_addrs`]/[`PeerTarget::with_addr`] constructors or
40 /// [`PeerTarget::set_direct_addrs`] over mutating this directly; read it via
41 /// [`PeerTarget::direct_addrs`] (all) or [`PeerTarget::direct_addr`] (the first candidate).
42 direct_addrs: Vec<SocketAddr>,
43
44 /// The network id the peer registered under (relay `network_id`, e.g. `DIG_MAINNET`). Used to
45 /// scope relay peer lookups + hole-punch coordination.
46 pub network_id: String,
47}
48
49impl PeerTarget {
50 /// A peer known by a single direct address (public / port-forwarded / discovered).
51 pub fn with_addr(
52 peer_id: PeerId,
53 direct_addr: SocketAddr,
54 network_id: impl Into<String>,
55 ) -> Self {
56 PeerTarget::with_addrs(peer_id, vec![direct_addr], network_id)
57 }
58
59 /// A peer known by one OR MORE direct candidate addresses, kept in the order supplied. The
60 /// dialer (`dig-ip`) applies the IPv6-first preference + local∩peer family intersection at dial
61 /// time, so the caller need not pre-order the candidates.
62 pub fn with_addrs(
63 peer_id: PeerId,
64 direct_addrs: Vec<SocketAddr>,
65 network_id: impl Into<String>,
66 ) -> Self {
67 PeerTarget {
68 peer_id,
69 direct_addrs,
70 network_id: network_id.into(),
71 }
72 }
73
74 /// A peer known only by identity — reachable via relay-coordinated methods.
75 pub fn relay_only(peer_id: PeerId, network_id: impl Into<String>) -> Self {
76 PeerTarget {
77 peer_id,
78 direct_addrs: Vec::new(),
79 network_id: network_id.into(),
80 }
81 }
82
83 /// The peer's candidate addresses, in discovery order. Empty for a relay-only target. The dial
84 /// order (IPv6-first, intersected with the local host's families) is computed by `dig-ip` at
85 /// dial time.
86 pub fn direct_addrs(&self) -> &[SocketAddr] {
87 &self.direct_addrs
88 }
89
90 /// The first candidate address, or `None` for a relay-only target.
91 ///
92 /// A convenience accessor for callers that want ONE address; it returns the first candidate in
93 /// discovery order. Prefer [`PeerTarget::direct_addrs`] so the dialer honours the full
94 /// family-aware happy-eyeballs fallback (`dig-ip`).
95 pub fn direct_addr(&self) -> Option<SocketAddr> {
96 self.direct_addrs.first().copied()
97 }
98
99 /// Replace the candidate list (kept in the order supplied).
100 pub fn set_direct_addrs(&mut self, addrs: Vec<SocketAddr>) {
101 self.direct_addrs = addrs;
102 }
103}
104
105/// An established, mutually-authenticated, **multiplexed** connection to a peer.
106///
107/// The connection is one mTLS byte stream whose remote presented a certificate whose `peer_id`
108/// equals [`Self::peer_id`] (verified during the handshake by [`crate::mtls::PeerIdPinningVerifier`]),
109/// wrapped in a [`PeerSession`] so the caller can open **many concurrent logical streams**
110/// ([`open_stream`](Self::open_stream)) or **byte-range streams**
111/// ([`open_range_stream`](Self::open_range_stream)) — streaming-first, no head-of-line blocking.
112/// [`Self::method`] reports which traversal technique established it — observability only; the caller
113/// opens streams identically regardless of the tier.
114pub struct PeerConnection {
115 /// The verified remote identity (== the [`PeerTarget::peer_id`] the caller asked for).
116 pub peer_id: PeerId,
117 /// The traversal technique that established this connection (Direct, Upnp, …, Relayed).
118 pub method: TraversalKind,
119 /// The remote address the mTLS session runs over (the peer's endpoint, or the relay for a
120 /// relayed transport).
121 pub remote_addr: SocketAddr,
122 /// The multiplexed session over the authenticated, encrypted byte stream to the peer.
123 pub session: PeerSession,
124}
125
126impl PeerConnection {
127 /// Open a new concurrent logical stream to the peer (cheap; open as many as you need for
128 /// simultaneous transfers without head-of-line blocking).
129 pub async fn open_stream(&mut self) -> std::io::Result<PeerStream> {
130 self.session.open_stream().await
131 }
132
133 /// Open a `dig.fetchRange` stream for `req` (writes the range-request preamble, then streams
134 /// [`crate::mux::RangeFrame`]s). The primitive for multi-source parallel range downloads.
135 pub async fn open_range_stream(
136 &mut self,
137 req: &crate::mux::RangeRequest,
138 ) -> std::io::Result<PeerStream> {
139 self.session.open_range_stream(req).await
140 }
141
142 /// Availability pre-check (`dig.getAvailability`) — ask whether this peer holds `items` BEFORE
143 /// opening range streams (see [`crate::mux::PeerSession::query_availability`]).
144 pub async fn query_availability(
145 &mut self,
146 items: Vec<crate::mux::AvailabilityItem>,
147 ) -> std::io::Result<crate::mux::AvailabilityResponse> {
148 self.session.query_availability(items).await
149 }
150}
151
152impl std::fmt::Debug for PeerConnection {
153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 f.debug_struct("PeerConnection")
155 .field("peer_id", &self.peer_id)
156 .field("method", &self.method)
157 .field("remote_addr", &self.remote_addr)
158 .field("session", &"<multiplexed mTLS session>")
159 .finish()
160 }
161}