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
8use std::net::IpAddr;
17
18use irontide_core::Id20;
19use irontide_core::crc32c;
20
21const IPV4_MASK: [u8; 4] = [0x03, 0x0f, 0x3f, 0xff];
25
26const IPV6_MASK: [u8; 8] = [0x01, 0x03, 0x07, 0x0f, 0x1f, 0x3f, 0x7f, 0xff];
28
29#[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; let crc = compute_ip_crc(ip, r);
41
42 let mut id = [0u8; 20];
47
48 fill_random(&mut id);
50
51 id[0] = (crc >> 24) as u8;
56 id[1] = (crc >> 16) as u8;
57 id[2] = ((crc >> 8) as u8 & 0xF8) | (id[2] & 0x07);
59
60 id[19] = r;
62
63 Id20(id)
64}
65
66fn compute_ip_crc(ip: IpAddr, r: u8) -> u32 {
71 match ip {
72 IpAddr::V4(v4) => {
73 let octets = v4.octets();
74 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 let value = masked | (u32::from(r) << 29);
81 crc32c(&value.to_be_bytes())
82 }
83 IpAddr::V6(v6) => {
84 let octets = v6.octets();
85 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 let value = masked | (u64::from(r) << 61);
92 crc32c(&value.to_be_bytes())
93 }
94 }
95}
96
97#[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 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; (id_prefix & 0xFFFF_F800) == crc_prefix
120}
121
122#[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 || (o[0] == 172 && (o[1] & 0xF0) == 16) || (o[0] == 192 && o[1] == 168) || (o[0] == 169 && o[1] == 254) || o[0] == 127 }
142 IpAddr::V6(v6) => {
143 let seg = v6.segments();
144 v6.is_loopback() || (seg[0] & 0xFFC0) == 0xFE80 || (seg[0] & 0xFE00) == 0xFC00 }
148 }
149}
150
151fn 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
183pub enum IpVoteSource {
184 Dht(u64),
186 Nat,
188 Tracker,
190}
191
192impl IpVoteSource {
193 #[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#[derive(Debug, Clone)]
213pub struct ExternalIpVoter {
214 votes: Vec<(u64, IpAddr)>,
217 consensus: Option<IpAddr>,
219 min_votes: usize,
221}
222
223impl ExternalIpVoter {
224 #[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 pub fn add_vote(&mut self, source_id: u64, ip: IpAddr) -> Option<IpAddr> {
242 if is_bep42_exempt(ip) {
244 return None;
245 }
246
247 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 if self.votes.len() > 100 {
256 self.votes.drain(0..self.votes.len() - 100);
257 }
258
259 self.evaluate_consensus()
260 }
261
262 #[must_use]
264 pub fn consensus(&self) -> Option<IpAddr> {
265 self.consensus
266 }
267
268 #[must_use]
270 pub fn vote_count(&self) -> usize {
271 self.votes.len()
272 }
273
274 fn evaluate_consensus(&mut self) -> Option<IpAddr> {
276 if self.votes.len() < self.min_votes {
277 return None;
278 }
279
280 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 let (best_ip, best_count) = counts.iter().max_by_key(|(_, c)| *c)?;
292
293 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 #[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 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 #[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 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 assert!(
420 all_fail,
421 "random IDs should almost never pass BEP 42 verification"
422 );
423 }
424
425 #[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 #[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 #[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 #[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 voter.add_vote(3, ip_a);
503 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); assert_eq!(voter.vote_count(), 1);
522 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 voter.add_vote(3, ip_b);
537 voter.add_vote(4, ip_b);
538 voter.add_vote(5, ip_b);
539 assert_eq!(voter.consensus(), Some(ip_b));
541 }
542
543 #[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}