#![allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
reason = "M175: BEP 42 DHT Security Extension — 160-bit fixed-width IDs sliced into u32/u8 chunks for CRC32C arithmetic per spec"
)]
use std::net::IpAddr;
use irontide_core::Id20;
use irontide_core::crc32c;
const IPV4_MASK: [u8; 4] = [0x03, 0x0f, 0x3f, 0xff];
const IPV6_MASK: [u8; 8] = [0x01, 0x03, 0x07, 0x0f, 0x1f, 0x3f, 0x7f, 0xff];
#[must_use]
pub fn generate_node_id(ip: IpAddr, r: u8) -> Id20 {
debug_assert!(r < 8, "r must be in range [0, 8)");
let r = r & 0x07;
let crc = compute_ip_crc(ip, r);
let mut id = [0u8; 20];
fill_random(&mut id);
id[0] = (crc >> 24) as u8;
id[1] = (crc >> 16) as u8;
id[2] = ((crc >> 8) as u8 & 0xF8) | (id[2] & 0x07);
id[19] = r;
Id20(id)
}
fn compute_ip_crc(ip: IpAddr, r: u8) -> u32 {
match ip {
IpAddr::V4(v4) => {
let octets = v4.octets();
let masked: u32 = u32::from(octets[0] & IPV4_MASK[0]) << 24
| u32::from(octets[1] & IPV4_MASK[1]) << 16
| u32::from(octets[2] & IPV4_MASK[2]) << 8
| u32::from(octets[3] & IPV4_MASK[3]);
let value = masked | (u32::from(r) << 29);
crc32c(&value.to_be_bytes())
}
IpAddr::V6(v6) => {
let octets = v6.octets();
let mut masked: u64 = 0;
for i in 0..8 {
masked |= u64::from(octets[i] & IPV6_MASK[i]) << (56 - i * 8);
}
let value = masked | (u64::from(r) << 61);
crc32c(&value.to_be_bytes())
}
}
}
#[must_use]
pub fn is_valid_node_id(id: &Id20, ip: IpAddr) -> bool {
if is_bep42_exempt(ip) {
return true;
}
let r = id.0[19] & 0x07;
let crc = compute_ip_crc(ip, r);
let id_prefix =
(u32::from(id.0[0]) << 24) | (u32::from(id.0[1]) << 16) | (u32::from(id.0[2]) << 8);
let crc_prefix = crc & 0xFFFF_F800;
(id_prefix & 0xFFFF_F800) == crc_prefix
}
#[must_use]
pub fn is_bep42_exempt(ip: IpAddr) -> bool {
match ip {
IpAddr::V4(v4) => {
let o = v4.octets();
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 }
IpAddr::V6(v6) => {
let seg = v6.segments();
v6.is_loopback() || (seg[0] & 0xFFC0) == 0xFE80 || (seg[0] & 0xFE00) == 0xFC00 }
}
}
fn fill_random(buf: &mut [u8]) {
use std::cell::Cell;
use std::time::SystemTime;
thread_local! {
static STATE: Cell<u64> = Cell::new(
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as u64
);
}
for byte in buf.iter_mut() {
STATE.with(|s| {
let mut x = s.get();
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
s.set(x);
*byte = x as u8;
});
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum IpVoteSource {
Dht(u64),
Nat,
Tracker,
}
impl IpVoteSource {
#[must_use]
pub fn source_id(&self) -> u64 {
match self {
Self::Dht(addr_hash) => *addr_hash,
Self::Nat => 0xFFFF_0001,
Self::Tracker => 0xFFFF_0002,
}
}
}
#[derive(Debug, Clone)]
pub struct ExternalIpVoter {
votes: Vec<(u64, IpAddr)>,
consensus: Option<IpAddr>,
min_votes: usize,
}
impl ExternalIpVoter {
#[must_use]
pub fn new(min_votes: usize) -> Self {
Self {
votes: Vec::new(),
consensus: None,
min_votes: min_votes.max(1),
}
}
pub fn add_vote(&mut self, source_id: u64, ip: IpAddr) -> Option<IpAddr> {
if is_bep42_exempt(ip) {
return None;
}
if let Some(existing) = self.votes.iter_mut().find(|(id, _)| *id == source_id) {
existing.1 = ip;
} else {
self.votes.push((source_id, ip));
}
if self.votes.len() > 100 {
self.votes.drain(0..self.votes.len() - 100);
}
self.evaluate_consensus()
}
#[must_use]
pub fn consensus(&self) -> Option<IpAddr> {
self.consensus
}
#[must_use]
pub fn vote_count(&self) -> usize {
self.votes.len()
}
fn evaluate_consensus(&mut self) -> Option<IpAddr> {
if self.votes.len() < self.min_votes {
return None;
}
let mut counts: Vec<(IpAddr, usize)> = Vec::new();
for (_, ip) in &self.votes {
if let Some(entry) = counts.iter_mut().find(|(addr, _)| addr == ip) {
entry.1 += 1;
} else {
counts.push((*ip, 1));
}
}
let (best_ip, best_count) = counts.iter().max_by_key(|(_, c)| *c)?;
if *best_count * 2 > self.votes.len() {
let new_consensus = *best_ip;
if self.consensus != Some(new_consensus) {
self.consensus = Some(new_consensus);
return Some(new_consensus);
}
}
None
}
}
impl Default for ExternalIpVoter {
fn default() -> Self {
Self::new(10)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{Ipv4Addr, Ipv6Addr};
#[test]
fn bep42_test_vector_1() {
let ip = IpAddr::V4(Ipv4Addr::new(124, 31, 75, 21));
let r = 1u8;
let crc = compute_ip_crc(ip, r & 0x07);
assert_eq!((crc >> 24) as u8, 0x5f);
assert_eq!((crc >> 16) as u8, 0xbf);
assert_eq!((crc >> 8) as u8 & 0xF8, 0xbf & 0xF8);
}
#[test]
fn bep42_test_vector_2() {
let ip = IpAddr::V4(Ipv4Addr::new(21, 75, 31, 124));
let r = 86u8;
let crc = compute_ip_crc(ip, r & 0x07);
assert_eq!((crc >> 24) as u8, 0x5a);
assert_eq!((crc >> 16) as u8, 0x3c);
assert_eq!((crc >> 8) as u8 & 0xF8, 0xe9 & 0xF8);
}
#[test]
fn bep42_test_vector_3() {
let ip = IpAddr::V4(Ipv4Addr::new(65, 23, 51, 170));
let r = 22u8;
let crc = compute_ip_crc(ip, r & 0x07);
assert_eq!((crc >> 24) as u8, 0xa5);
assert_eq!((crc >> 16) as u8, 0xd4);
assert_eq!((crc >> 8) as u8 & 0xF8, 0x32 & 0xF8);
}
#[test]
fn bep42_test_vector_4() {
let ip = IpAddr::V4(Ipv4Addr::new(84, 124, 73, 14));
let r = 65u8;
let crc = compute_ip_crc(ip, r & 0x07);
assert_eq!((crc >> 24) as u8, 0x1b);
assert_eq!((crc >> 16) as u8, 0x03);
assert_eq!((crc >> 8) as u8 & 0xF8, 0x21 & 0xF8);
}
#[test]
fn bep42_test_vector_5() {
let ip = IpAddr::V4(Ipv4Addr::new(43, 213, 53, 83));
let r = 90u8;
let crc = compute_ip_crc(ip, r & 0x07);
assert_eq!((crc >> 24) as u8, 0xe5);
assert_eq!((crc >> 16) as u8, 0x6f);
assert_eq!((crc >> 8) as u8 & 0xF8, 0x6c & 0xF8);
}
#[test]
fn generated_id_verifies() {
let ip = IpAddr::V4(Ipv4Addr::new(124, 31, 75, 21));
for r in 0..8u8 {
let id = generate_node_id(ip, r);
assert!(
is_valid_node_id(&id, ip),
"generated ID should verify for r={r}"
);
}
}
#[test]
fn generated_id_fails_for_wrong_ip() {
let ip = IpAddr::V4(Ipv4Addr::new(124, 31, 75, 21));
let wrong_ip = IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8));
let id = generate_node_id(ip, 3);
assert!(
!is_valid_node_id(&id, wrong_ip),
"ID generated for one IP should not verify for a different IP"
);
}
#[test]
fn random_id_almost_certainly_fails_verification() {
let ip = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4));
let mut all_fail = true;
for _ in 0..100 {
let mut buf = [0u8; 20];
fill_random(&mut buf);
let id = Id20(buf);
if is_valid_node_id(&id, ip) {
all_fail = false;
}
}
assert!(
all_fail,
"random IDs should almost never pass BEP 42 verification"
);
}
#[test]
fn local_ips_always_valid() {
let random_id = Id20::from_hex("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d").unwrap();
assert!(is_valid_node_id(&random_id, "127.0.0.1".parse().unwrap()));
assert!(is_valid_node_id(&random_id, "10.0.0.1".parse().unwrap()));
assert!(is_valid_node_id(&random_id, "192.168.1.1".parse().unwrap()));
assert!(is_valid_node_id(&random_id, "172.16.5.1".parse().unwrap()));
assert!(is_valid_node_id(&random_id, "169.254.1.1".parse().unwrap()));
assert!(is_valid_node_id(&random_id, "::1".parse().unwrap()));
assert!(is_valid_node_id(&random_id, "fe80::1".parse().unwrap()));
assert!(is_valid_node_id(&random_id, "fc00::1".parse().unwrap()));
}
#[test]
fn public_ips_not_exempt() {
assert!(!is_bep42_exempt("8.8.8.8".parse().unwrap()));
assert!(!is_bep42_exempt("1.2.3.4".parse().unwrap()));
assert!(!is_bep42_exempt("2001:db8::1".parse().unwrap()));
}
#[test]
fn ipv6_generated_id_verifies() {
let ip = IpAddr::V6(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 1));
for r in 0..8u8 {
let id = generate_node_id(ip, r);
assert!(
is_valid_node_id(&id, ip),
"IPv6 generated ID should verify for r={r}"
);
}
}
#[test]
fn last_byte_is_r() {
let ip = IpAddr::V4(Ipv4Addr::new(203, 0, 113, 5));
for r in 0..8u8 {
let id = generate_node_id(ip, r);
assert_eq!(id.0[19] & 0x07, r, "last byte low 3 bits must be r");
}
}
#[test]
fn voter_no_consensus_below_threshold() {
let mut voter = ExternalIpVoter::new(3);
let ip: IpAddr = "203.0.113.5".parse().unwrap();
assert!(voter.add_vote(1, ip).is_none());
assert!(voter.add_vote(2, ip).is_none());
assert!(voter.consensus().is_none());
}
#[test]
fn voter_reaches_consensus() {
let mut voter = ExternalIpVoter::new(3);
let ip: IpAddr = "203.0.113.5".parse().unwrap();
voter.add_vote(1, ip);
voter.add_vote(2, ip);
let result = voter.add_vote(3, ip);
assert_eq!(result, Some(ip));
assert_eq!(voter.consensus(), Some(ip));
}
#[test]
fn voter_requires_majority() {
let mut voter = ExternalIpVoter::new(3);
let ip_a: IpAddr = "203.0.113.5".parse().unwrap();
let ip_b: IpAddr = "198.51.100.1".parse().unwrap();
voter.add_vote(1, ip_a);
voter.add_vote(2, ip_b);
voter.add_vote(3, ip_a);
assert_eq!(voter.consensus(), Some(ip_a));
}
#[test]
fn voter_ignores_private_ips() {
let mut voter = ExternalIpVoter::new(1);
assert!(voter.add_vote(1, "192.168.1.1".parse().unwrap()).is_none());
assert!(voter.add_vote(2, "10.0.0.1".parse().unwrap()).is_none());
assert_eq!(voter.vote_count(), 0);
}
#[test]
fn voter_deduplicates_same_source() {
let mut voter = ExternalIpVoter::new(2);
let ip: IpAddr = "203.0.113.5".parse().unwrap();
voter.add_vote(1, ip);
voter.add_vote(1, ip); assert_eq!(voter.vote_count(), 1);
assert!(voter.consensus().is_none());
}
#[test]
fn voter_consensus_changes_on_new_majority() {
let mut voter = ExternalIpVoter::new(2);
let ip_a: IpAddr = "203.0.113.5".parse().unwrap();
let ip_b: IpAddr = "198.51.100.1".parse().unwrap();
voter.add_vote(1, ip_a);
voter.add_vote(2, ip_a);
assert_eq!(voter.consensus(), Some(ip_a));
voter.add_vote(3, ip_b);
voter.add_vote(4, ip_b);
voter.add_vote(5, ip_b);
assert_eq!(voter.consensus(), Some(ip_b));
}
#[test]
fn vote_source_ids_are_distinct() {
let nat = IpVoteSource::Nat;
let tracker = IpVoteSource::Tracker;
let dht = IpVoteSource::Dht(12345);
assert_ne!(nat.source_id(), tracker.source_id());
assert_ne!(nat.source_id(), dht.source_id());
assert_ne!(tracker.source_id(), dht.source_id());
}
}