Skip to main content

irontide_dht/
node_id.rs

1#![allow(
2    clippy::cast_possible_truncation,
3    clippy::cast_possible_wrap,
4    clippy::cast_sign_loss,
5    reason = "M175: BEP 42 DHT Security Extension — 160-bit fixed-width IDs sliced into u32/u8 chunks for CRC32C arithmetic per spec"
6)]
7
8//! BEP 42: DHT Security Extension — Node ID generation and verification.
9//!
10//! Ties DHT node IDs to IP addresses using CRC32C, making Sybil attacks
11//! computationally expensive. The first 21 bits of a valid node ID must
12//! match `CRC32C(masked_ip | (r << 29))`, and the last byte must equal `r`.
13//!
14//! Reference: <https://www.bittorrent.org/beps/bep_0042.html>
15
16use std::net::IpAddr;
17
18use irontide_core::Id20;
19use irontide_core::crc32c;
20
21// ── BEP 42 masks ────────────────────────────────────────────────────────
22
23/// IPv4 mask: only certain bits of the IP affect the node ID.
24const IPV4_MASK: [u8; 4] = [0x03, 0x0f, 0x3f, 0xff];
25
26/// IPv6 mask: applied to the high 64 bits of the address.
27const IPV6_MASK: [u8; 8] = [0x01, 0x03, 0x07, 0x0f, 0x1f, 0x3f, 0x7f, 0xff];
28
29// ── Node ID generation ──────────────────────────────────────────────────
30
31/// Generate a BEP 42-compliant node ID for the given IP address.
32///
33/// The `r` value (0..8) controls which of the 8 valid prefixes is used.
34/// The remaining bytes (3..19) are filled randomly.
35#[must_use]
36pub fn generate_node_id(ip: IpAddr, r: u8) -> Id20 {
37    debug_assert!(r < 8, "r must be in range [0, 8)");
38    let r = r & 0x07; // clamp to 3 bits
39
40    let crc = compute_ip_crc(ip, r);
41
42    // Build the node ID:
43    // - Bytes 0..3: first 21 bits from CRC, rest random
44    // - Bytes 3..19: random
45    // - Byte 19: r
46    let mut id = [0u8; 20];
47
48    // Fill with random bytes first
49    fill_random(&mut id);
50
51    // Set first 21 bits from CRC32C result (big-endian)
52    // Bits 0..7 of CRC → id[0]
53    // Bits 8..15 of CRC → id[1]
54    // Bits 16..20 of CRC → top 5 bits of id[2]
55    id[0] = (crc >> 24) as u8;
56    id[1] = (crc >> 16) as u8;
57    // Top 5 bits from CRC, bottom 3 bits random (preserve existing random)
58    id[2] = ((crc >> 8) as u8 & 0xF8) | (id[2] & 0x07);
59
60    // Last byte must be r
61    id[19] = r;
62
63    Id20(id)
64}
65
66/// Compute the CRC32C hash for BEP 42 node ID derivation.
67///
68/// For IPv4: `CRC32C((ip & 0x030f3fff) | (r << 29))`
69/// For IPv6: `CRC32C((ip[0..8] & 0x0103070f1f3f7fff) | (r << 61))`
70fn compute_ip_crc(ip: IpAddr, r: u8) -> u32 {
71    match ip {
72        IpAddr::V4(v4) => {
73            let octets = v4.octets();
74            // Apply mask and pack into big-endian u32
75            let masked: u32 = u32::from(octets[0] & IPV4_MASK[0]) << 24
76                | u32::from(octets[1] & IPV4_MASK[1]) << 16
77                | u32::from(octets[2] & IPV4_MASK[2]) << 8
78                | u32::from(octets[3] & IPV4_MASK[3]);
79            // OR in r at bits 29-31
80            let value = masked | (u32::from(r) << 29);
81            crc32c(&value.to_be_bytes())
82        }
83        IpAddr::V6(v6) => {
84            let octets = v6.octets();
85            // Apply mask to first 8 bytes, pack into big-endian u64
86            let mut masked: u64 = 0;
87            for i in 0..8 {
88                masked |= u64::from(octets[i] & IPV6_MASK[i]) << (56 - i * 8);
89            }
90            // OR in r at bits 61-63
91            let value = masked | (u64::from(r) << 61);
92            crc32c(&value.to_be_bytes())
93        }
94    }
95}
96
97// ── Node ID verification ────────────────────────────────────────────────
98
99/// Check whether a node ID is valid for the given IP address (BEP 42).
100///
101/// Returns `true` if the first 21 bits of the node ID match the CRC32C
102/// prefix for any `r` value in `[0, 8)`, and the last byte equals that `r`.
103///
104/// Local/private IPs are always considered valid (exempt from enforcement).
105#[must_use]
106pub fn is_valid_node_id(id: &Id20, ip: IpAddr) -> bool {
107    if is_bep42_exempt(ip) {
108        return true;
109    }
110
111    let r = id.0[19] & 0x07;
112    let crc = compute_ip_crc(ip, r);
113
114    // Compare first 21 bits
115    let id_prefix =
116        (u32::from(id.0[0]) << 24) | (u32::from(id.0[1]) << 16) | (u32::from(id.0[2]) << 8);
117    let crc_prefix = crc & 0xFFFF_F800; // top 21 bits of CRC (in top 21 bits of u32)
118
119    (id_prefix & 0xFFFF_F800) == crc_prefix
120}
121
122/// Returns `true` if the IP is exempt from BEP 42 enforcement.
123///
124/// Exempt ranges (BEP 42 section on "local networks"):
125/// - 10.0.0.0/8
126/// - 172.16.0.0/12
127/// - 192.168.0.0/16
128/// - 169.254.0.0/16
129/// - 127.0.0.0/8
130/// - IPv6 loopback (`::1`), link-local (`fe80::/10`), unique local (`fc00::/7`)
131#[must_use]
132pub fn is_bep42_exempt(ip: IpAddr) -> bool {
133    match ip {
134        IpAddr::V4(v4) => {
135            let o = v4.octets();
136            o[0] == 10                                          // 10.0.0.0/8
137                || (o[0] == 172 && (o[1] & 0xF0) == 16)        // 172.16.0.0/12
138                || (o[0] == 192 && o[1] == 168)                 // 192.168.0.0/16
139                || (o[0] == 169 && o[1] == 254)                 // 169.254.0.0/16
140                || o[0] == 127 // 127.0.0.0/8
141        }
142        IpAddr::V6(v6) => {
143            let seg = v6.segments();
144            v6.is_loopback()                                    // ::1
145                || (seg[0] & 0xFFC0) == 0xFE80                 // fe80::/10
146                || (seg[0] & 0xFE00) == 0xFC00 // fc00::/7
147        }
148    }
149}
150
151// ── Random fill helper ──────────────────────────────────────────────────
152
153/// Fill a byte slice with pseudo-random bytes (xorshift64, no external dep).
154fn fill_random(buf: &mut [u8]) {
155    use std::cell::Cell;
156    use std::time::SystemTime;
157
158    thread_local! {
159        static STATE: Cell<u64> = Cell::new(
160            SystemTime::now()
161                .duration_since(SystemTime::UNIX_EPOCH)
162                .unwrap_or_default()
163                .as_nanos() as u64
164        );
165    }
166
167    for byte in buf.iter_mut() {
168        STATE.with(|s| {
169            let mut x = s.get();
170            x ^= x << 13;
171            x ^= x >> 7;
172            x ^= x << 17;
173            s.set(x);
174            *byte = x as u8;
175        });
176    }
177}
178
179// ── External IP Voter ───────────────────────────────────────────────────
180
181/// Source of an external IP observation.
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
183pub enum IpVoteSource {
184    /// KRPC response `ip` field from a DHT node.
185    Dht(u64),
186    /// NAT traversal (`UPnP`, NAT-PMP, PCP).
187    Nat,
188    /// Tracker announce response.
189    Tracker,
190}
191
192impl IpVoteSource {
193    /// Derive a unique `source_id` for deduplication in the voter.
194    ///
195    /// DHT sources use the hash of the remote node's address.
196    /// NAT and tracker sources use distinct well-known IDs so they
197    /// don't overwrite each other's votes.
198    #[must_use]
199    pub fn source_id(&self) -> u64 {
200        match self {
201            Self::Dht(addr_hash) => *addr_hash,
202            Self::Nat => 0xFFFF_0001,
203            Self::Tracker => 0xFFFF_0002,
204        }
205    }
206}
207
208/// Consensus-based external IP detection.
209///
210/// Aggregates IP observations from multiple independent sources and
211/// determines our external IP when a majority agrees.
212#[derive(Debug, Clone)]
213pub struct ExternalIpVoter {
214    /// (`source_identifier`, `voted_ip`) — `source_identifier` is an opaque hash
215    /// to deduplicate votes from the same source (e.g., same DHT node).
216    votes: Vec<(u64, IpAddr)>,
217    /// Current consensus IP, if any.
218    consensus: Option<IpAddr>,
219    /// Minimum number of votes required before establishing consensus.
220    min_votes: usize,
221}
222
223impl ExternalIpVoter {
224    /// Create a new voter. `min_votes` is the minimum number of agreeing
225    /// observations before consensus is established (default recommendation: 10).
226    #[must_use]
227    pub fn new(min_votes: usize) -> Self {
228        Self {
229            votes: Vec::new(),
230            consensus: None,
231            min_votes: min_votes.max(1),
232        }
233    }
234
235    /// Record a vote for an external IP address.
236    ///
237    /// `source_id` should be unique per source (e.g., hash of the DHT node's
238    /// socket address) to prevent a single source from stuffing votes.
239    ///
240    /// Returns `Some(ip)` if consensus changed (new IP or first consensus).
241    pub fn add_vote(&mut self, source_id: u64, ip: IpAddr) -> Option<IpAddr> {
242        // Ignore local/private IPs — they're never our real external address
243        if is_bep42_exempt(ip) {
244            return None;
245        }
246
247        // Deduplicate: replace existing vote from this source
248        if let Some(existing) = self.votes.iter_mut().find(|(id, _)| *id == source_id) {
249            existing.1 = ip;
250        } else {
251            self.votes.push((source_id, ip));
252        }
253
254        // Evict oldest votes if the list grows too large (cap at 100)
255        if self.votes.len() > 100 {
256            self.votes.drain(0..self.votes.len() - 100);
257        }
258
259        self.evaluate_consensus()
260    }
261
262    /// Current consensus IP, if established.
263    #[must_use]
264    pub fn consensus(&self) -> Option<IpAddr> {
265        self.consensus
266    }
267
268    /// Number of recorded votes.
269    #[must_use]
270    pub fn vote_count(&self) -> usize {
271        self.votes.len()
272    }
273
274    /// Evaluate votes and return `Some(ip)` if consensus changed.
275    fn evaluate_consensus(&mut self) -> Option<IpAddr> {
276        if self.votes.len() < self.min_votes {
277            return None;
278        }
279
280        // Count votes per IP
281        let mut counts: Vec<(IpAddr, usize)> = Vec::new();
282        for (_, ip) in &self.votes {
283            if let Some(entry) = counts.iter_mut().find(|(addr, _)| addr == ip) {
284                entry.1 += 1;
285            } else {
286                counts.push((*ip, 1));
287            }
288        }
289
290        // Find the IP with the most votes
291        let (best_ip, best_count) = counts.iter().max_by_key(|(_, c)| *c)?;
292
293        // Require strict majority (>50% of all votes)
294        if *best_count * 2 > self.votes.len() {
295            let new_consensus = *best_ip;
296            if self.consensus != Some(new_consensus) {
297                self.consensus = Some(new_consensus);
298                return Some(new_consensus);
299            }
300        }
301
302        None
303    }
304}
305
306impl Default for ExternalIpVoter {
307    fn default() -> Self {
308        Self::new(10)
309    }
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315    use std::net::{Ipv4Addr, Ipv6Addr};
316
317    // ── BEP 42 test vectors ─────────────────────────────────────────
318    //
319    // From BEP 42 specification:
320    // | IP             | rand | Node ID prefix (first 3 bytes)  | last byte |
321    // |----------------|------|---------------------------------|-----------|
322    // | 124.31.75.21   |    1 | 5fbfbf                          | 01        |
323    // | 21.75.31.124   |   86 | 5a3ce9                          | 56        |
324    // | 65.23.51.170   |   22 | a5d432                          | 16        |
325    // | 84.124.73.14   |   65 | 1b0321                          | 41        |
326    // | 43.213.53.83   |   90 | e56f6c                          | 5a        |
327
328    #[test]
329    fn bep42_test_vector_1() {
330        let ip = IpAddr::V4(Ipv4Addr::new(124, 31, 75, 21));
331        let r = 1u8;
332        let crc = compute_ip_crc(ip, r & 0x07);
333        // Expected first 3 bytes of node ID: 0x5f, 0xbf, 0xbf
334        assert_eq!((crc >> 24) as u8, 0x5f);
335        assert_eq!((crc >> 16) as u8, 0xbf);
336        assert_eq!((crc >> 8) as u8 & 0xF8, 0xbf & 0xF8);
337    }
338
339    #[test]
340    fn bep42_test_vector_2() {
341        let ip = IpAddr::V4(Ipv4Addr::new(21, 75, 31, 124));
342        let r = 86u8;
343        let crc = compute_ip_crc(ip, r & 0x07);
344        assert_eq!((crc >> 24) as u8, 0x5a);
345        assert_eq!((crc >> 16) as u8, 0x3c);
346        assert_eq!((crc >> 8) as u8 & 0xF8, 0xe9 & 0xF8);
347    }
348
349    #[test]
350    fn bep42_test_vector_3() {
351        let ip = IpAddr::V4(Ipv4Addr::new(65, 23, 51, 170));
352        let r = 22u8;
353        let crc = compute_ip_crc(ip, r & 0x07);
354        assert_eq!((crc >> 24) as u8, 0xa5);
355        assert_eq!((crc >> 16) as u8, 0xd4);
356        assert_eq!((crc >> 8) as u8 & 0xF8, 0x32 & 0xF8);
357    }
358
359    #[test]
360    fn bep42_test_vector_4() {
361        let ip = IpAddr::V4(Ipv4Addr::new(84, 124, 73, 14));
362        let r = 65u8;
363        let crc = compute_ip_crc(ip, r & 0x07);
364        assert_eq!((crc >> 24) as u8, 0x1b);
365        assert_eq!((crc >> 16) as u8, 0x03);
366        assert_eq!((crc >> 8) as u8 & 0xF8, 0x21 & 0xF8);
367    }
368
369    #[test]
370    fn bep42_test_vector_5() {
371        let ip = IpAddr::V4(Ipv4Addr::new(43, 213, 53, 83));
372        let r = 90u8;
373        let crc = compute_ip_crc(ip, r & 0x07);
374        assert_eq!((crc >> 24) as u8, 0xe5);
375        assert_eq!((crc >> 16) as u8, 0x6f);
376        assert_eq!((crc >> 8) as u8 & 0xF8, 0x6c & 0xF8);
377    }
378
379    // ── generate + verify round-trip ────────────────────────────────
380
381    #[test]
382    fn generated_id_verifies() {
383        let ip = IpAddr::V4(Ipv4Addr::new(124, 31, 75, 21));
384        for r in 0..8u8 {
385            let id = generate_node_id(ip, r);
386            assert!(
387                is_valid_node_id(&id, ip),
388                "generated ID should verify for r={r}"
389            );
390        }
391    }
392
393    #[test]
394    fn generated_id_fails_for_wrong_ip() {
395        let ip = IpAddr::V4(Ipv4Addr::new(124, 31, 75, 21));
396        let wrong_ip = IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8));
397        let id = generate_node_id(ip, 3);
398        assert!(
399            !is_valid_node_id(&id, wrong_ip),
400            "ID generated for one IP should not verify for a different IP"
401        );
402    }
403
404    #[test]
405    fn random_id_almost_certainly_fails_verification() {
406        let ip = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4));
407        // A truly random 20-byte ID has a 1/2^21 chance of passing.
408        // Generate several and expect all to fail.
409        let mut all_fail = true;
410        for _ in 0..100 {
411            let mut buf = [0u8; 20];
412            fill_random(&mut buf);
413            let id = Id20(buf);
414            if is_valid_node_id(&id, ip) {
415                all_fail = false;
416            }
417        }
418        // With 100 trials and 1/2M chance each, probability of any passing is ~0.005%
419        assert!(
420            all_fail,
421            "random IDs should almost never pass BEP 42 verification"
422        );
423    }
424
425    // ── Exempt IPs ──────────────────────────────────────────────────
426
427    #[test]
428    fn local_ips_always_valid() {
429        let random_id = Id20::from_hex("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d").unwrap();
430        assert!(is_valid_node_id(&random_id, "127.0.0.1".parse().unwrap()));
431        assert!(is_valid_node_id(&random_id, "10.0.0.1".parse().unwrap()));
432        assert!(is_valid_node_id(&random_id, "192.168.1.1".parse().unwrap()));
433        assert!(is_valid_node_id(&random_id, "172.16.5.1".parse().unwrap()));
434        assert!(is_valid_node_id(&random_id, "169.254.1.1".parse().unwrap()));
435        assert!(is_valid_node_id(&random_id, "::1".parse().unwrap()));
436        assert!(is_valid_node_id(&random_id, "fe80::1".parse().unwrap()));
437        assert!(is_valid_node_id(&random_id, "fc00::1".parse().unwrap()));
438    }
439
440    #[test]
441    fn public_ips_not_exempt() {
442        assert!(!is_bep42_exempt("8.8.8.8".parse().unwrap()));
443        assert!(!is_bep42_exempt("1.2.3.4".parse().unwrap()));
444        assert!(!is_bep42_exempt("2001:db8::1".parse().unwrap()));
445    }
446
447    // ── IPv6 generation ─────────────────────────────────────────────
448
449    #[test]
450    fn ipv6_generated_id_verifies() {
451        let ip = IpAddr::V6(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 1));
452        for r in 0..8u8 {
453            let id = generate_node_id(ip, r);
454            assert!(
455                is_valid_node_id(&id, ip),
456                "IPv6 generated ID should verify for r={r}"
457            );
458        }
459    }
460
461    // ── Last byte = r ───────────────────────────────────────────────
462
463    #[test]
464    fn last_byte_is_r() {
465        let ip = IpAddr::V4(Ipv4Addr::new(203, 0, 113, 5));
466        for r in 0..8u8 {
467            let id = generate_node_id(ip, r);
468            assert_eq!(id.0[19] & 0x07, r, "last byte low 3 bits must be r");
469        }
470    }
471
472    // ── ExternalIpVoter ─────────────────────────────────────────────
473
474    #[test]
475    fn voter_no_consensus_below_threshold() {
476        let mut voter = ExternalIpVoter::new(3);
477        let ip: IpAddr = "203.0.113.5".parse().unwrap();
478        assert!(voter.add_vote(1, ip).is_none());
479        assert!(voter.add_vote(2, ip).is_none());
480        assert!(voter.consensus().is_none());
481    }
482
483    #[test]
484    fn voter_reaches_consensus() {
485        let mut voter = ExternalIpVoter::new(3);
486        let ip: IpAddr = "203.0.113.5".parse().unwrap();
487        voter.add_vote(1, ip);
488        voter.add_vote(2, ip);
489        let result = voter.add_vote(3, ip);
490        assert_eq!(result, Some(ip));
491        assert_eq!(voter.consensus(), Some(ip));
492    }
493
494    #[test]
495    fn voter_requires_majority() {
496        let mut voter = ExternalIpVoter::new(3);
497        let ip_a: IpAddr = "203.0.113.5".parse().unwrap();
498        let ip_b: IpAddr = "198.51.100.1".parse().unwrap();
499        voter.add_vote(1, ip_a);
500        voter.add_vote(2, ip_b);
501        // 1 vs 1 — no majority
502        voter.add_vote(3, ip_a);
503        // Now 2 vs 1 — ip_a has majority (2/3 > 50%)
504        assert_eq!(voter.consensus(), Some(ip_a));
505    }
506
507    #[test]
508    fn voter_ignores_private_ips() {
509        let mut voter = ExternalIpVoter::new(1);
510        assert!(voter.add_vote(1, "192.168.1.1".parse().unwrap()).is_none());
511        assert!(voter.add_vote(2, "10.0.0.1".parse().unwrap()).is_none());
512        assert_eq!(voter.vote_count(), 0);
513    }
514
515    #[test]
516    fn voter_deduplicates_same_source() {
517        let mut voter = ExternalIpVoter::new(2);
518        let ip: IpAddr = "203.0.113.5".parse().unwrap();
519        voter.add_vote(1, ip);
520        voter.add_vote(1, ip); // same source_id
521        assert_eq!(voter.vote_count(), 1);
522        // Still below threshold (only 1 unique source)
523        assert!(voter.consensus().is_none());
524    }
525
526    #[test]
527    fn voter_consensus_changes_on_new_majority() {
528        let mut voter = ExternalIpVoter::new(2);
529        let ip_a: IpAddr = "203.0.113.5".parse().unwrap();
530        let ip_b: IpAddr = "198.51.100.1".parse().unwrap();
531        voter.add_vote(1, ip_a);
532        voter.add_vote(2, ip_a);
533        assert_eq!(voter.consensus(), Some(ip_a));
534
535        // Now 3 sources vote for ip_b
536        voter.add_vote(3, ip_b);
537        voter.add_vote(4, ip_b);
538        voter.add_vote(5, ip_b);
539        // 2 for a, 3 for b — b has majority (3/5 > 50%)
540        assert_eq!(voter.consensus(), Some(ip_b));
541    }
542
543    // ── IpVoteSource ────────────────────────────────────────────────
544
545    #[test]
546    fn vote_source_ids_are_distinct() {
547        let nat = IpVoteSource::Nat;
548        let tracker = IpVoteSource::Tracker;
549        let dht = IpVoteSource::Dht(12345);
550        assert_ne!(nat.source_id(), tracker.source_id());
551        assert_ne!(nat.source_id(), dht.source_id());
552        assert_ne!(tracker.source_id(), dht.source_id());
553    }
554}