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 (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 peer's verified BLS G1 identity pubkey (#1204), captured from the cert binding when the
123    /// handshake carried a valid one. `None` for a legacy peer with no binding (or when binding
124    /// verification was off). The sealing layer (S2) seals directed payloads to this key so a
125    /// misdelivery cannot be opened by the wrong node.
126    pub peer_bls_pub: Option<[u8; 48]>,
127    /// The multiplexed session over the authenticated, encrypted byte stream to the peer.
128    pub session: PeerSession,
129}
130
131impl PeerConnection {
132    /// Open a new concurrent logical stream to the peer (cheap; open as many as you need for
133    /// simultaneous transfers without head-of-line blocking).
134    pub async fn open_stream(&mut self) -> std::io::Result<PeerStream> {
135        self.session.open_stream().await
136    }
137
138    /// Open a `dig.fetchRange` stream for `req` (writes the range-request preamble, then streams
139    /// [`crate::mux::RangeFrame`]s). The primitive for multi-source parallel range downloads.
140    pub async fn open_range_stream(
141        &mut self,
142        req: &crate::mux::RangeRequest,
143    ) -> std::io::Result<PeerStream> {
144        self.session.open_range_stream(req).await
145    }
146
147    /// Availability pre-check (`dig.getAvailability`) — ask whether this peer holds `items` BEFORE
148    /// opening range streams (see [`crate::mux::PeerSession::query_availability`]).
149    pub async fn query_availability(
150        &mut self,
151        items: Vec<crate::mux::AvailabilityItem>,
152    ) -> std::io::Result<crate::mux::AvailabilityResponse> {
153        self.session.query_availability(items).await
154    }
155}
156
157impl std::fmt::Debug for PeerConnection {
158    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159        f.debug_struct("PeerConnection")
160            .field("peer_id", &self.peer_id)
161            .field("method", &self.method)
162            .field("remote_addr", &self.remote_addr)
163            .field(
164                "peer_bls_pub",
165                &self.peer_bls_pub.map(|_| "<48-byte G1 pubkey>"),
166            )
167            .field("session", &"<multiplexed mTLS session>")
168            .finish()
169    }
170}