Skip to main content

dig_dht/
record.rs

1//! [`ProviderRecord`] — the value the DHT stores: "peer P holds content C, reachable at these
2//! addresses, until this expiry" — plus the [`CandidateAddr`] address shape it carries.
3//!
4//! A provider record is what `announce_provider` PUTs and `find_providers` returns. It binds a
5//! **content key** (the [`ContentId`](crate::ContentId) hashed into the keyspace) to the
6//! **`peer_id`** of a node that holds it, together with candidate addresses so the finder can then
7//! open a dig-nat connection and fetch over the L7 peer RPC. Records are **TTL'd** (`expires_at`)
8//! and **republished** by the holder before expiry, so stale providers age out of the DHT
9//! automatically — a Kademlia provider record is soft state, not a permanent entry.
10//!
11//! The [`CandidateAddr`] `{ host, port, kind }` and the `kind` tokens are byte-compatible with the
12//! L7 peer-network `dig.getPeers` `addresses[]` shape (§7), so a record's addresses drop straight
13//! into a `PeerTarget` for [`dig_nat::connect`].
14
15use std::net::{IpAddr, SocketAddr};
16
17use dig_ip::Family;
18use serde::{Deserialize, Serialize};
19
20use dig_nat::PeerId;
21
22/// How a candidate address was learned — the L7 `dig.getPeers` `addresses[].kind` tokens (§7). The
23/// lowercase serde spelling is the frozen wire form; the ordering is most-direct-first (a dialer
24/// picks the lowest-rank dialable candidate).
25#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
26#[serde(rename_all = "lowercase")]
27pub enum AddressKind {
28    /// Advertised/observed directly reachable address (publicly routable or port-forwarded).
29    Direct,
30    /// A UPnP / NAT-PMP / PCP-mapped external address.
31    Mapped,
32    /// A STUN-discovered public reflexive address.
33    Reflexive,
34    /// Reachable through the relay (no direct candidate yet).
35    Relay,
36}
37
38impl AddressKind {
39    /// Most-direct-first rank (lower is more direct) — mirrors the dialer's candidate preference.
40    pub fn rank(self) -> u8 {
41        match self {
42            AddressKind::Direct => 0,
43            AddressKind::Mapped => 1,
44            AddressKind::Reflexive => 2,
45            AddressKind::Relay => 3,
46        }
47    }
48
49    /// Whether an address of this kind can be dialed directly (everything but a bare relay marker).
50    pub fn is_dialable(self) -> bool {
51        !matches!(self, AddressKind::Relay)
52    }
53}
54
55/// One candidate address for a provider: `{ host, port, kind }` (L7 `dig.getPeers` §7). The finder
56/// dials these (most-direct-first) via [`dig_nat::connect`] to reach the provider.
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58pub struct CandidateAddr {
59    /// IPv4/IPv6 literal or hostname.
60    pub host: String,
61    /// P2P port.
62    pub port: u16,
63    /// How this address was learned.
64    pub kind: AddressKind,
65}
66
67impl CandidateAddr {
68    /// A directly-dialable candidate (public / port-forwarded / discovered).
69    pub fn direct(host: impl Into<String>, port: u16) -> Self {
70        CandidateAddr {
71            host: host.into(),
72            port,
73            kind: AddressKind::Direct,
74        }
75    }
76
77    /// A relay-only marker (no direct address; reach via the relay / a brokered hole punch).
78    pub fn relay_marker() -> Self {
79        CandidateAddr {
80            host: String::new(),
81            port: 0,
82            kind: AddressKind::Relay,
83        }
84    }
85
86    /// The address-family half of the sort key, derived from [`dig_ip::Family`] — the ecosystem's
87    /// single source of truth for the IPv6-first / IPv4-fallback rule (CLAUDE.md §5.2):
88    ///
89    /// - `0` — a genuine IPv6 literal (tried first);
90    /// - `1` — an IPv4 literal, INCLUDING an IPv4-mapped IPv6 address, which [`Family::of`]
91    ///   correctly classifies as V4 because it is IPv4 reachability (the fallback);
92    /// - `2` — a host that is not an IP literal at all. A DHT candidate is an *observed* socket
93    ///   address, so a non-literal is a malformed or hostname-bearing record whose reachability this
94    ///   crate cannot classify and does not resolve; it must never outrank a usable IPv4 literal.
95    ///
96    /// Deriving the family here, rather than hand-rolling an `is_ipv6` check, keeps dig-dht from
97    /// drifting off the canonical contract.
98    fn family_rank(&self) -> u8 {
99        let family = self
100            .host
101            .parse::<IpAddr>()
102            .ok()
103            .map(|ip| Family::of(&SocketAddr::new(ip, self.port)));
104        match family {
105            Some(Family::V6) => 0,
106            Some(Family::V4) => 1,
107            None => 2,
108        }
109    }
110
111    /// The identity of the ENDPOINT this candidate names, for deduplication: the parsed address in
112    /// canonical form plus the port, falling back to the raw host text when it is not an IP literal.
113    ///
114    /// Deduplicating on the raw `host` string would treat one address spelled several ways as several
115    /// dial targets — `2001:db8::1`, `2001:0db8::1`, `2001:db8:0:0:0:0:0:1` and `2001:DB8::1` are the
116    /// same host — which lets a padded record consume every slot of the dial set with a single
117    /// address. Parsing first collapses those spellings, and an IPv4-mapped IPv6 literal is reduced to
118    /// its IPv4 form so `::ffff:a.b.c.d` and `a.b.c.d` are recognised as one endpoint (consistent with
119    /// [`Family::of`] classifying both as IPv4 reachability).
120    fn dial_identity(&self) -> (String, u16) {
121        let host = match self.host.parse::<IpAddr>() {
122            Ok(IpAddr::V6(v6)) => v6
123                .to_ipv4_mapped()
124                .map(IpAddr::V4)
125                .unwrap_or(IpAddr::V6(v6))
126                .to_string(),
127            Ok(ip) => ip.to_string(),
128            Err(_) => self.host.clone(),
129        };
130        (host, self.port)
131    }
132
133    /// Whether this candidate is a genuine IPv6 literal — the tier that is tried FIRST and, being
134    /// the preferred tier, the one that can crowd every other out of a capped dial set.
135    fn is_ipv6_literal(&self) -> bool {
136        self.family_rank() == 0
137    }
138
139    /// Sort key for IPv6-first, then most-direct-first ordering: `(family_rank, kind_rank)`. The
140    /// family half comes from [`dig_ip::Family`] (see [`family_rank`](Self::family_rank)); the
141    /// dht-specific directness tiebreak stays [`AddressKind::rank`], so within one family the most
142    /// direct candidate sorts first.
143    fn family_then_kind_rank(&self) -> (u8, u8) {
144        (self.family_rank(), self.kind.rank())
145    }
146}
147
148/// Sort `addresses` **IPv6-first, then by [`AddressKind::rank`]** — the ecosystem-wide IPv6-first,
149/// IPv4-fallback rule for peer communication. Used by both [`ProviderRecord::new`] and
150/// [`crate::routing::Contact::new`] so provider and routing-table address lists share one ordering
151/// policy. This only reorders the list; the wire shape of each [`CandidateAddr`] is unchanged.
152pub(crate) fn sort_addresses_ipv6_first(addresses: &mut [CandidateAddr]) {
153    addresses.sort_by_key(CandidateAddr::family_then_kind_rank);
154}
155
156/// Maximum [`CandidateAddr`] entries kept per [`ProviderRecord`] / [`crate::routing::Contact`].
157///
158/// A record/contact carries candidate addresses so a finder can dial the holder; nothing on the
159/// wire or decode path previously bounded how many a single record could carry (only the overall
160/// 256 KiB frame did — [`crate::wire::MAX_FRAMED_BODY`]), so one frame could smuggle thousands of
161/// addresses that the victim would store, fold into its routing table, AND re-serve (cloned) to
162/// every querying peer — memory inflation plus bandwidth amplification (SPEC §5.5, §14). Eight is
163/// generous headroom over the four [`AddressKind`] variants (a conforming producer emits at most
164/// one address per kind per family) while remaining a small, cheap-to-clone constant.
165pub const MAX_ADDRESSES_PER_RECORD: usize = 8;
166
167/// Sort `addresses` **IPv6-first-then-rank** (see [`sort_addresses_ipv6_first`]) and then truncate
168/// to [`MAX_ADDRESSES_PER_RECORD`], so the most-preferred candidates are the ones kept when a list
169/// exceeds the cap. This is the one admission point both the constructors ([`ProviderRecord::new`],
170/// [`crate::routing::Contact::new`]) and the wire-decode boundary (`handle_request_from`'s
171/// `AddProvider` arm, and contacts folded in from lookup responses) MUST call before accepting an
172/// address list from any source that did not already go through it — a `ProviderRecord` /
173/// `Contact` deserialized directly from the wire bypasses the constructors entirely (their fields
174/// are public), so capping only in `new` would not close the untrusted-input path.
175pub(crate) fn sort_and_cap_addresses(addresses: &mut Vec<CandidateAddr>) {
176    sort_addresses_ipv6_first(addresses);
177    addresses.truncate(MAX_ADDRESSES_PER_RECORD);
178}
179
180/// Upper bound on how many candidates [`dial_candidates`] hands a dialer for ONE peer, so a record
181/// padded with addresses cannot turn a single holder into a connect storm. Byte-for-byte the same
182/// bound dig-download applies on its own dial path, so a consumer that adopts this iterator sees no
183/// change in attempt count.
184pub const MAX_DIAL_CANDIDATES: usize = 4;
185
186/// The dialable candidates of `addresses`, in **dial order**: IPv6 first, then IPv4, then anything
187/// unresolvable — deduped by `host:port` and capped at [`MAX_DIAL_CANDIDATES`].
188///
189/// This is the §5.2-compliant order (IPv6-first, IPv4-**fallback**) and the ONE place the DHT
190/// expresses it, so every consumer inherits it instead of re-deriving a ranking of its own. A dialer
191/// walks the WHOLE list and only reports failure once every candidate has been tried: in #836 a
192/// reader instead took a single address, tried one IPv6 literal, and gave up while a working IPv4
193/// candidate sat unused — v4 is the fallback, so a failed v6 attempt MUST fall through to it.
194///
195/// Relay markers are excluded (they are not directly dialable — reach those peers via the relay /
196/// a brokered punch). Unresolvable candidates are KEPT, last, on purpose: a dialer that walks them
197/// can report a concrete per-candidate reason instead of pretending the provider had no address.
198pub fn dial_candidates(addresses: &[CandidateAddr]) -> Vec<&CandidateAddr> {
199    let mut candidates: Vec<&CandidateAddr> =
200        addresses.iter().filter(|a| a.kind.is_dialable()).collect();
201    // Sorted defensively rather than trusting the stored order: the same ranking is applied when a
202    // list is constructed or deserialized, but `addresses` is a public field any caller may rewrite.
203    candidates.sort_by_key(|a| a.family_then_kind_rank());
204    let mut seen = std::collections::HashSet::new();
205    candidates.retain(|a| seen.insert(a.dial_identity()));
206    reserve_fallback_slot_and_cap(&mut candidates);
207    candidates
208}
209
210/// Truncate `candidates` to [`MAX_DIAL_CANDIDATES`] while KEEPING the fallback tier represented.
211///
212/// Truncating the family-sorted list outright would let the preferred tier fill the cap on its own: a
213/// holder advertising four or more IPv6 candidates would yield a dial set containing no IPv4 at all,
214/// so a dialer that faithfully walked every candidate it was given would STILL never reach the working
215/// address — precisely the #836 read-leg failure this iterator exists to prevent, and a violation of
216/// the rule that a failed IPv6 attempt must never mask a working IPv4 one. It needs no attacker: a
217/// dual-stack holder legitimately emits direct + mapped + reflexive IPv6 candidates, and an IPv6
218/// address with no working route is ordinary.
219///
220/// So when the cap would exclude EVERY non-IPv6 candidate and one exists, the least-preferred kept
221/// slot is given to the best non-IPv6 candidate. IPv6 still leads the list — the reservation costs one
222/// surplus IPv6 attempt, never the ordering.
223fn reserve_fallback_slot_and_cap(candidates: &mut Vec<&CandidateAddr>) {
224    if candidates.len() <= MAX_DIAL_CANDIDATES {
225        return;
226    }
227    let kept_excludes_every_fallback = candidates[..MAX_DIAL_CANDIDATES]
228        .iter()
229        .all(|a| a.is_ipv6_literal());
230    let fallback = kept_excludes_every_fallback
231        .then(|| candidates.iter().find(|a| !a.is_ipv6_literal()).copied())
232        .flatten();
233    match fallback {
234        Some(fallback) => {
235            candidates.truncate(MAX_DIAL_CANDIDATES - 1);
236            candidates.push(fallback);
237        }
238        None => candidates.truncate(MAX_DIAL_CANDIDATES),
239    }
240}
241
242/// serde hook applied to every `addresses` field ([`ProviderRecord`], [`crate::routing::Contact`]),
243/// so [`MAX_ADDRESSES_PER_RECORD`] holds **by construction** for any value that is deserialized —
244/// from a peer's wire frame, a config file, or a cached snapshot — and not only at the ingest call
245/// sites that remember to call [`sort_and_cap_addresses`] (§14). Deserialization is the ONE
246/// unavoidable gate every untrusted address list passes through; enforcing the bound there means a
247/// future ingest path cannot silently reintroduce an unbounded list.
248///
249/// It **bounds rather than rejects**: a list longer than the cap is sorted and truncated, never
250/// turned into a decode error. Rejecting would make a nonconforming (or simply older, looser)
251/// producer's record unparseable, which the store-format compatibility rule forbids — and would
252/// hand a peer an easy way to poison a whole frame. Sorting before truncating keeps the
253/// most-preferred candidates, so a hostile peer cannot bury the one reachable address behind filler.
254pub(crate) fn deserialize_capped_addresses<'de, D>(
255    deserializer: D,
256) -> Result<Vec<CandidateAddr>, D::Error>
257where
258    D: serde::Deserializer<'de>,
259{
260    let mut addresses = Vec::<CandidateAddr>::deserialize(deserializer)?;
261    sort_and_cap_addresses(&mut addresses);
262    Ok(addresses)
263}
264
265/// The DHT's stored value: peer `provider_peer_id` holds the content whose key is `content_key`,
266/// reachable at `addresses`, until `expires_at`.
267///
268/// - `content_key` is the 64-hex [`Key`](crate::Key) the content id hashed to — the DHT stores by
269///   key, not by the (larger, granularity-tagged) content id, so a record is compact and the store
270///   is a pure key→providers map.
271/// - `provider_peer_id` is the 64-hex `peer_id` of the holder; a finder builds a `PeerTarget` from
272///   it plus `addresses` and connects via dig-nat.
273/// - `expires_at` is absolute Unix seconds; a record past its expiry is treated as absent and GC'd.
274///   The holder republishes (a fresh record with a new `expires_at`) before expiry to stay findable.
275#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
276pub struct ProviderRecord {
277    /// The content key (64-hex) this record provides for — the [`Key`](crate::Key) a content id
278    /// hashed to.
279    pub content_key: String,
280    /// The holder's `peer_id` (64-hex).
281    pub provider_peer_id: String,
282    /// Candidate addresses to reach the holder, ordered IPv6-first then most-direct-first by
283    /// [`AddressKind::rank`] and bounded to [`MAX_ADDRESSES_PER_RECORD`] — held by BOTH
284    /// [`ProviderRecord::new`] and deserialization ([`deserialize_capped_addresses`]), so a record
285    /// off the wire carries the same guarantee as a constructed one.
286    #[serde(deserialize_with = "deserialize_capped_addresses")]
287    pub addresses: Vec<CandidateAddr>,
288    /// Absolute expiry (Unix seconds). A record at/after this time is stale.
289    pub expires_at: u64,
290}
291
292impl ProviderRecord {
293    /// Build a record: peer `provider` holds `content_key`, reachable at `addresses`, until
294    /// `expires_at` (absolute Unix seconds).
295    pub fn new(
296        content_key: &crate::key::Key,
297        provider: &PeerId,
298        mut addresses: Vec<CandidateAddr>,
299        expires_at: u64,
300    ) -> Self {
301        sort_and_cap_addresses(&mut addresses);
302        ProviderRecord {
303            content_key: content_key.to_hex(),
304            provider_peer_id: provider.to_hex(),
305            addresses,
306            expires_at,
307        }
308    }
309
310    /// The provider's `peer_id` decoded from the 64-hex field, or `None` if malformed.
311    pub fn provider_peer_id(&self) -> Option<PeerId> {
312        PeerId::from_hex(&self.provider_peer_id)
313    }
314
315    /// Whether this record is expired at `now` (Unix seconds) — stale records are dropped on read.
316    pub fn is_expired(&self, now: u64) -> bool {
317        now >= self.expires_at
318    }
319
320    /// The FIRST candidate only — the IPv6-preferred, most-direct dialable address, if any.
321    ///
322    /// **Prefer [`dial_candidates`](Self::dial_candidates) for dialing.** This returns one address,
323    /// so a caller that dials it and stops has made a single attempt and cannot fall back: an
324    /// unusable IPv6 candidate then masks a working IPv4 one, violating the IPv4-**fallback** half
325    /// of §5.2 (exactly the #836 read-leg failure). Use this only where a single representative
326    /// address is genuinely what is wanted — a log line, a display string, a metric label.
327    pub fn best_address(&self) -> Option<&CandidateAddr> {
328        self.addresses.iter().find(|a| a.kind.is_dialable())
329    }
330
331    /// This provider's dialable candidates in §5.2 dial order — see [`dial_candidates`] for the
332    /// ordering contract. Dial these in order, falling through on failure, before concluding the
333    /// holder is unreachable.
334    pub fn dial_candidates(&self) -> Vec<&CandidateAddr> {
335        dial_candidates(&self.addresses)
336    }
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342    use crate::key::Key;
343
344    fn pid(b: u8) -> PeerId {
345        PeerId::from_bytes([b; 32])
346    }
347
348    #[test]
349    fn record_round_trips_through_json() {
350        let key = Key::from_bytes([0xAB; 32]);
351        let rec = ProviderRecord::new(
352            &key,
353            &pid(0x07),
354            vec![CandidateAddr::direct("203.0.113.7", 9444)],
355            1_000,
356        );
357        let json = serde_json::to_string(&rec).unwrap();
358        let back: ProviderRecord = serde_json::from_str(&json).unwrap();
359        assert_eq!(rec, back);
360        assert_eq!(back.provider_peer_id().unwrap(), pid(0x07));
361        assert_eq!(back.content_key, key.to_hex());
362    }
363
364    #[test]
365    fn ttl_expiry() {
366        let rec = ProviderRecord::new(&Key::from_bytes([0u8; 32]), &pid(1), vec![], 100);
367        assert!(!rec.is_expired(99));
368        assert!(rec.is_expired(100));
369        assert!(rec.is_expired(101));
370    }
371
372    #[test]
373    fn address_kind_wire_tokens_are_lowercase() {
374        assert_eq!(
375            serde_json::to_string(&AddressKind::Direct).unwrap(),
376            "\"direct\""
377        );
378        assert_eq!(
379            serde_json::to_string(&AddressKind::Reflexive).unwrap(),
380            "\"reflexive\""
381        );
382        assert_eq!(
383            serde_json::to_string(&AddressKind::Mapped).unwrap(),
384            "\"mapped\""
385        );
386        assert_eq!(
387            serde_json::to_string(&AddressKind::Relay).unwrap(),
388            "\"relay\""
389        );
390    }
391
392    #[test]
393    fn best_address_prefers_most_direct() {
394        let key = Key::from_bytes([0u8; 32]);
395        let rec = ProviderRecord::new(
396            &key,
397            &pid(1),
398            vec![
399                CandidateAddr {
400                    host: "r".into(),
401                    port: 1,
402                    kind: AddressKind::Reflexive,
403                },
404                CandidateAddr::direct("d", 2),
405                CandidateAddr::relay_marker(),
406            ],
407            10,
408        );
409        assert_eq!(rec.best_address().unwrap().kind, AddressKind::Direct);
410    }
411
412    #[test]
413    fn best_address_none_when_only_relay() {
414        let key = Key::from_bytes([0u8; 32]);
415        let rec = ProviderRecord::new(&key, &pid(1), vec![CandidateAddr::relay_marker()], 10);
416        assert!(rec.best_address().is_none());
417    }
418
419    #[test]
420    fn address_rank_ordering() {
421        assert!(AddressKind::Direct.rank() < AddressKind::Mapped.rank());
422        assert!(AddressKind::Mapped.rank() < AddressKind::Reflexive.rank());
423        assert!(AddressKind::Reflexive.rank() < AddressKind::Relay.rank());
424        assert!(!AddressKind::Relay.is_dialable());
425        assert!(AddressKind::Direct.is_dialable());
426    }
427
428    #[test]
429    fn provider_record_new_sorts_addresses_ipv6_first() {
430        // Fed in IPv4-first order; the stored list must come out IPv6-first, then by rank.
431        let key = Key::from_bytes([0u8; 32]);
432        let rec = ProviderRecord::new(
433            &key,
434            &pid(1),
435            vec![
436                CandidateAddr::direct("203.0.113.7", 9444), // IPv4 direct
437                CandidateAddr::direct("2001:db8::1", 9444), // IPv6 direct
438                CandidateAddr {
439                    host: "198.51.100.2".into(),
440                    port: 1,
441                    kind: AddressKind::Reflexive,
442                }, // IPv4 reflexive
443                CandidateAddr {
444                    host: "2001:db8::2".into(),
445                    port: 1,
446                    kind: AddressKind::Reflexive,
447                }, // IPv6 reflexive
448            ],
449            10,
450        );
451        let hosts: Vec<&str> = rec.addresses.iter().map(|a| a.host.as_str()).collect();
452        assert_eq!(
453            hosts,
454            vec!["2001:db8::1", "2001:db8::2", "203.0.113.7", "198.51.100.2"],
455            "addresses must be IPv6-first, then ranked by AddressKind"
456        );
457    }
458
459    #[test]
460    fn family_key_derives_from_dig_ip_family() {
461        // The FAMILY half of the sort key comes from `dig_ip::Family`, the single ecosystem source
462        // of truth — not a hand-rolled `is_ipv6` heuristic. The load-bearing proof is the
463        // IPv4-mapped IPv6 case: `dig_ip::Family::of` classifies `::ffff:a.b.c.d` as V4 (it is IPv4
464        // reachability), so it must sort with IPv4, AFTER a genuine IPv6 address of the same kind. A
465        // `host.parse::<IpAddr>()`-based family key would have (wrongly) treated it as IPv6.
466        let key = Key::from_bytes([0u8; 32]);
467        let rec = ProviderRecord::new(
468            &key,
469            &pid(1),
470            vec![
471                CandidateAddr::direct("::ffff:203.0.113.9", 9444), // IPv4-mapped → V4 per dig-ip
472                CandidateAddr::direct("2001:db8::1", 9444),        // genuine IPv6 → V6
473            ],
474            10,
475        );
476        let hosts: Vec<&str> = rec.addresses.iter().map(|a| a.host.as_str()).collect();
477        assert_eq!(
478            hosts,
479            vec!["2001:db8::1", "::ffff:203.0.113.9"],
480            "an IPv4-mapped IPv6 address must sort as V4 (dig_ip::Family), after a genuine IPv6"
481        );
482    }
483
484    #[test]
485    fn directness_kind_rank_preserved_as_tiebreak_within_a_family() {
486        // Within ONE address family the dht-specific most-direct-first `AddressKind::rank` tiebreak
487        // MUST survive the migration to dig-ip family keying: same family, different directness →
488        // Direct before Mapped before Reflexive.
489        let key = Key::from_bytes([0u8; 32]);
490        let rec = ProviderRecord::new(
491            &key,
492            &pid(1),
493            vec![
494                CandidateAddr {
495                    host: "2001:db8::3".into(),
496                    port: 1,
497                    kind: AddressKind::Reflexive,
498                },
499                CandidateAddr {
500                    host: "2001:db8::2".into(),
501                    port: 1,
502                    kind: AddressKind::Mapped,
503                },
504                CandidateAddr::direct("2001:db8::1", 9444),
505            ],
506            10,
507        );
508        let hosts: Vec<&str> = rec.addresses.iter().map(|a| a.host.as_str()).collect();
509        assert_eq!(
510            hosts,
511            vec!["2001:db8::1", "2001:db8::2", "2001:db8::3"],
512            "within one family, addresses must stay ordered by AddressKind::rank (most-direct first)"
513        );
514    }
515
516    #[test]
517    fn best_address_prefers_ipv6_over_ipv4_at_same_rank() {
518        let key = Key::from_bytes([0u8; 32]);
519        let rec = ProviderRecord::new(
520            &key,
521            &pid(1),
522            vec![
523                CandidateAddr::direct("203.0.113.7", 9444), // IPv4 direct, fed first
524                CandidateAddr::direct("2001:db8::1", 9444), // IPv6 direct, fed second
525            ],
526            10,
527        );
528        assert_eq!(rec.best_address().unwrap().host, "2001:db8::1");
529    }
530
531    // ---- Address-list cap (MEDIUM: no cap on addresses[], SECURITY_AUDIT_P2P.md #179) ----
532
533    #[test]
534    fn provider_record_new_caps_addresses_at_the_constant() {
535        // Feed far more than the cap — a hostile/misconfigured caller must never make a
536        // constructed record carry an unbounded address list.
537        let key = Key::from_bytes([0u8; 32]);
538        let many: Vec<CandidateAddr> = (0..1000)
539            .map(|i| CandidateAddr::direct(format!("203.0.113.{}", i % 255), 9444))
540            .collect();
541        let rec = ProviderRecord::new(&key, &pid(1), many, 10);
542        assert_eq!(rec.addresses.len(), MAX_ADDRESSES_PER_RECORD);
543    }
544
545    #[test]
546    fn provider_record_new_cap_keeps_most_preferred_after_sort() {
547        // The cap must apply AFTER the IPv6-first-then-rank sort, so truncation drops the LEAST
548        // preferred candidates, not an arbitrary prefix of the input order.
549        let key = Key::from_bytes([0u8; 32]);
550        let mut addrs: Vec<CandidateAddr> = Vec::new();
551        // One preferred IPv6 direct address that must survive the cap...
552        addrs.push(CandidateAddr::direct("2001:db8::1", 9444));
553        // ...buried behind far more than the cap worth of low-preference IPv4 relay markers.
554        for i in 0..1000u32 {
555            addrs.push(CandidateAddr {
556                host: format!("198.51.100.{}", i % 255),
557                port: 1,
558                kind: AddressKind::Relay,
559            });
560        }
561        let rec = ProviderRecord::new(&key, &pid(1), addrs, 10);
562        assert_eq!(rec.addresses.len(), MAX_ADDRESSES_PER_RECORD);
563        assert_eq!(
564            rec.addresses[0].host, "2001:db8::1",
565            "the single most-preferred (IPv6 direct) candidate must survive truncation"
566        );
567    }
568
569    // ---- Deserialization-time address bound (#1514) ----
570
571    /// Build the JSON of a record carrying `n` addresses — the shape a hostile peer frames on the
572    /// wire, bypassing `ProviderRecord::new` entirely (its fields are public).
573    fn record_json_with_addresses(n: usize) -> String {
574        let addrs: Vec<String> = (0..n)
575            .map(|i| {
576                format!(
577                    r#"{{"host":"198.51.100.{}","port":1,"kind":"relay"}}"#,
578                    i % 255
579                )
580            })
581            .collect();
582        format!(
583            r#"{{"content_key":"{}","provider_peer_id":"{}","addresses":[{}],"expires_at":1}}"#,
584            "aa".repeat(32),
585            "bb".repeat(32),
586            addrs.join(",")
587        )
588    }
589
590    #[test]
591    fn deserialization_bounds_the_address_count() {
592        // #1514: the cap must hold BY CONSTRUCTION at the decode boundary, not only at the ingest
593        // call sites that remember to call `sort_and_cap_addresses`. Stated over the CLASS: no
594        // deserialized record, from any source, ever carries more than the cap.
595        let rec: ProviderRecord = serde_json::from_str(&record_json_with_addresses(1000)).unwrap();
596        assert_eq!(rec.addresses.len(), MAX_ADDRESSES_PER_RECORD);
597    }
598
599    #[test]
600    fn deserialization_bound_is_one_off_exact() {
601        // The one-off variant: exactly the cap survives untouched; exactly one more is bounded.
602        let at_cap: ProviderRecord =
603            serde_json::from_str(&record_json_with_addresses(MAX_ADDRESSES_PER_RECORD)).unwrap();
604        assert_eq!(at_cap.addresses.len(), MAX_ADDRESSES_PER_RECORD);
605        let over_by_one: ProviderRecord =
606            serde_json::from_str(&record_json_with_addresses(MAX_ADDRESSES_PER_RECORD + 1))
607                .unwrap();
608        assert_eq!(over_by_one.addresses.len(), MAX_ADDRESSES_PER_RECORD);
609    }
610
611    #[test]
612    fn deserialization_keeps_the_most_preferred_addresses() {
613        // Bounding must drop the LEAST preferred candidates, so a hostile peer cannot bury the one
614        // genuinely reachable address behind a wall of filler and have it truncated away.
615        let mut addrs: Vec<String> =
616            vec![r#"{"host":"2001:db8::1","port":9444,"kind":"direct"}"#.to_string()];
617        for i in 0..1000 {
618            addrs.push(format!(
619                r#"{{"host":"198.51.100.{}","port":1,"kind":"relay"}}"#,
620                i % 255
621            ));
622        }
623        // The preferred candidate sits LAST in the wire order, so a naive prefix-truncation would
624        // discard exactly the address that matters.
625        addrs.rotate_left(1);
626        let json = format!(
627            r#"{{"content_key":"{}","provider_peer_id":"{}","addresses":[{}],"expires_at":1}}"#,
628            "aa".repeat(32),
629            "bb".repeat(32),
630            addrs.join(",")
631        );
632        let rec: ProviderRecord = serde_json::from_str(&json).unwrap();
633        assert_eq!(rec.addresses.len(), MAX_ADDRESSES_PER_RECORD);
634        assert_eq!(
635            rec.addresses[0].host, "2001:db8::1",
636            "the most-preferred candidate must survive the bound regardless of wire position"
637        );
638    }
639
640    // ---- Ordered dial candidates (#1594) ----
641
642    fn record_with(addresses: Vec<CandidateAddr>) -> ProviderRecord {
643        ProviderRecord::new(&Key::from_bytes([0u8; 32]), &pid(1), addresses, 10)
644    }
645
646    #[test]
647    fn dial_candidates_order_v6_then_v4_then_unresolvable() {
648        let rec = record_with(vec![
649            CandidateAddr::direct("not-a-literal", 9444),
650            CandidateAddr::direct("203.0.113.7", 9444),
651            CandidateAddr::direct("2001:db8::1", 9444),
652        ]);
653        let hosts: Vec<&str> = rec
654            .dial_candidates()
655            .iter()
656            .map(|a| a.host.as_str())
657            .collect();
658        assert_eq!(
659            hosts,
660            vec!["2001:db8::1", "203.0.113.7", "not-a-literal"],
661            "dial order is IPv6, then IPv4, then anything unresolvable (§5.2)"
662        );
663    }
664
665    #[test]
666    fn dial_candidates_keep_the_ipv4_fallback_behind_an_ipv6_candidate() {
667        // The #836 failure this exists to prevent: a probe took `best_address()` alone, tried ONE
668        // IPv6 literal, and gave up while a working IPv4 candidate sat unused. IPv4 is the FALLBACK
669        // (§5.2), so it MUST still be present, after the v6 candidate, for a dialer to walk to.
670        let rec = record_with(vec![
671            CandidateAddr::direct("2001:db8::1", 9444),
672            CandidateAddr::direct("172.31.79.22", 9444),
673        ]);
674        let candidates = rec.dial_candidates();
675        assert_eq!(candidates.len(), 2, "the fallback must not be dropped");
676        assert_eq!(candidates[0].host, "2001:db8::1");
677        assert_eq!(candidates[1].host, "172.31.79.22");
678    }
679
680    #[test]
681    fn dial_candidates_treat_v4_mapped_v6_as_ipv4() {
682        // Canonical IPv4-in-IPv6 rule: `::ffff:a.b.c.d` is IPv4 REACHABILITY, so it must order with
683        // IPv4 — after a genuine IPv6 candidate. This is the one case where a hand-rolled
684        // `is_ipv6`-style check silently disagrees with `dig_ip::Family`.
685        let rec = record_with(vec![
686            CandidateAddr::direct("::ffff:203.0.113.9", 9444),
687            CandidateAddr::direct("2001:db8::1", 9444),
688        ]);
689        let hosts: Vec<&str> = rec
690            .dial_candidates()
691            .iter()
692            .map(|a| a.host.as_str())
693            .collect();
694        assert_eq!(hosts, vec!["2001:db8::1", "::ffff:203.0.113.9"]);
695    }
696
697    #[test]
698    fn dial_candidates_exclude_relay_markers() {
699        let rec = record_with(vec![
700            CandidateAddr::relay_marker(),
701            CandidateAddr::direct("2001:db8::1", 9444),
702        ]);
703        let candidates = rec.dial_candidates();
704        assert_eq!(
705            candidates.len(),
706            1,
707            "a relay marker is not directly dialable"
708        );
709        assert_eq!(candidates[0].host, "2001:db8::1");
710    }
711
712    #[test]
713    fn dial_candidates_are_bounded_and_deduped() {
714        // A record may legitimately carry up to MAX_ADDRESSES_PER_RECORD candidates; a dialer must
715        // not turn one provider into an unbounded connect storm, and must not waste an attempt
716        // re-dialing the same host:port twice.
717        let mut addresses = vec![CandidateAddr::direct("2001:db8::1", 9444); 3];
718        addresses.extend((0..5).map(|i| CandidateAddr::direct(format!("10.0.0.{i}"), 9444)));
719        let rec = record_with(addresses);
720        let candidates = rec.dial_candidates();
721        assert_eq!(candidates.len(), MAX_DIAL_CANDIDATES);
722        assert_eq!(
723            candidates
724                .iter()
725                .filter(|a| a.host == "2001:db8::1")
726                .count(),
727            1,
728            "a repeated host:port contributes exactly one dial attempt"
729        );
730    }
731
732    #[test]
733    fn dial_candidates_of_a_relay_only_record_are_empty() {
734        let rec = record_with(vec![CandidateAddr::relay_marker()]);
735        assert!(rec.dial_candidates().is_empty());
736    }
737
738    #[test]
739    fn unresolvable_host_sorts_after_an_ipv4_literal_in_the_stored_order() {
740        // The stored order and the dial order share ONE ranking policy, so a hostname (which is not
741        // reachability the DHT can classify) must never outrank a usable IPv4 literal anywhere.
742        let rec = record_with(vec![
743            CandidateAddr::direct("not-a-literal", 1),
744            CandidateAddr::direct("203.0.113.7", 1),
745        ]);
746        let hosts: Vec<&str> = rec.addresses.iter().map(|a| a.host.as_str()).collect();
747        assert_eq!(hosts, vec!["203.0.113.7", "not-a-literal"]);
748    }
749
750    #[test]
751    fn dial_candidates_reserve_a_slot_for_the_ipv4_fallback() {
752        // #836 again, one layer down: truncating to MAX_DIAL_CANDIDATES *after* the family sort means
753        // a record carrying four or more IPv6 candidates yields a dial set with ZERO IPv4 — so a
754        // dialer walking every candidate it is given still never reaches the working address. That
755        // contradicts the SPEC 5.5 MUST that a failed IPv6 attempt never masks a working IPv4 one.
756        // A dual-stack holder legitimately emits direct + mapped + reflexive v6, so this is reachable
757        // without an attacker; an IPv6 address with no working route is the common AWS case.
758        let rec = record_with(vec![
759            CandidateAddr::direct("2001:db8::1", 9444),
760            CandidateAddr::direct("2001:db8::2", 9444),
761            CandidateAddr::direct("2001:db8::3", 9444),
762            CandidateAddr::direct("2001:db8::4", 9444),
763            CandidateAddr::direct("203.0.113.7", 9444),
764        ]);
765        let candidates = rec.dial_candidates();
766        assert_eq!(candidates.len(), MAX_DIAL_CANDIDATES);
767        assert!(
768            candidates.iter().any(|a| a.host == "203.0.113.7"),
769            "the IPv4 fallback tier must keep a slot inside the cap, got {:?}",
770            candidates.iter().map(|a| &a.host).collect::<Vec<_>>()
771        );
772        assert_eq!(
773            candidates[0].host, "2001:db8::1",
774            "IPv6 still leads — the reservation costs the LEAST preferred v6 slot, not the order"
775        );
776    }
777
778    #[test]
779    fn dial_candidates_reserve_the_fallback_only_when_it_would_be_lost() {
780        // The one-off variant either side of the cap: at exactly the cap nothing is dropped and no
781        // reservation is needed, so a v4 that already fits must not be promoted out of order.
782        let rec = record_with(vec![
783            CandidateAddr::direct("2001:db8::1", 9444),
784            CandidateAddr::direct("2001:db8::2", 9444),
785            CandidateAddr::direct("2001:db8::3", 9444),
786            CandidateAddr::direct("203.0.113.7", 9444),
787        ]);
788        let hosts: Vec<&str> = rec
789            .dial_candidates()
790            .iter()
791            .map(|a| a.host.as_str())
792            .collect();
793        assert_eq!(
794            hosts,
795            vec!["2001:db8::1", "2001:db8::2", "2001:db8::3", "203.0.113.7"]
796        );
797    }
798
799    #[test]
800    fn dial_candidates_dedupe_equivalent_spellings_of_one_address() {
801        // Dedup on the RAW host string lets one address spelled four ways consume every slot, which
802        // is the fallback-starvation above with no distinct addresses at all. Equivalence is a
803        // property of the parsed IpAddr, not of the text.
804        let rec = record_with(vec![
805            CandidateAddr::direct("2001:db8::1", 9444),
806            CandidateAddr::direct("2001:0db8::1", 9444),
807            CandidateAddr::direct("2001:db8:0:0:0:0:0:1", 9444),
808            CandidateAddr::direct("2001:DB8::1", 9444),
809            CandidateAddr::direct("203.0.113.7", 9444),
810        ]);
811        let candidates = rec.dial_candidates();
812        assert_eq!(
813            candidates.len(),
814            2,
815            "four spellings of one IPv6 address are ONE dial attempt, got {:?}",
816            candidates.iter().map(|a| &a.host).collect::<Vec<_>>()
817        );
818        assert!(candidates.iter().any(|a| a.host == "203.0.113.7"));
819    }
820
821    #[test]
822    fn dial_candidates_treat_a_v4_mapped_spelling_as_the_same_address_as_its_ipv4() {
823        // `::ffff:a.b.c.d` and `a.b.c.d` are the same endpoint and the same IPv4 reachability (which
824        // is why `dig_ip::Family` ranks both V4), so they are one dial attempt, not two.
825        let rec = record_with(vec![
826            CandidateAddr::direct("::ffff:203.0.113.7", 9444),
827            CandidateAddr::direct("203.0.113.7", 9444),
828        ]);
829        assert_eq!(rec.dial_candidates().len(), 1);
830    }
831
832    #[test]
833    fn dial_candidates_keep_distinct_ports_of_one_host_apart() {
834        // Dedup is per ENDPOINT: the same host on two ports is two genuine dial targets.
835        let rec = record_with(vec![
836            CandidateAddr::direct("2001:db8::1", 9444),
837            CandidateAddr::direct("2001:db8::1", 9445),
838        ]);
839        assert_eq!(rec.dial_candidates().len(), 2);
840    }
841}