Skip to main content

saorsa_core/
security.rs

1// Copyright 2024 Saorsa Labs Limited
2//
3// This software is dual-licensed under:
4// - GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later)
5// - Commercial License
6//
7// For AGPL-3.0 license, see LICENSE-AGPL-3.0
8// For commercial licensing, contact: david@saorsalabs.com
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under these licenses is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
14//! Security module
15//!
16//! This module provides cryptographic functionality and Sybil protection for the P2P network.
17//! It implements IPv6-based node ID generation and IP diversity enforcement to prevent
18//! large-scale Sybil attacks while maintaining network openness.
19
20use crate::quantum_crypto::saorsa_transport_integration::{
21    MlDsaPublicKey, MlDsaSecretKey, MlDsaSignature, ml_dsa_sign, ml_dsa_verify,
22};
23use anyhow::{Result, anyhow};
24use lru::LruCache;
25use serde::{Deserialize, Serialize};
26use std::collections::HashMap;
27use std::fmt::Debug;
28use std::net::{Ipv4Addr, Ipv6Addr};
29use std::num::NonZeroUsize;
30use std::time::{Duration, SystemTime, UNIX_EPOCH};
31
32use std::sync::Arc;
33
34/// Maximum subnet tracking entries before evicting oldest (prevents memory DoS)
35const MAX_SUBNET_TRACKING: usize = 50_000;
36
37// ============================================================================
38// Generic IP Address Trait
39// ============================================================================
40
41/// Trait for IP addresses that can be used in node ID generation
42#[allow(dead_code)]
43pub trait NodeIpAddress: Debug + Clone + Send + Sync + 'static {
44    /// Get the octets of this IP address for hashing
45    fn octets_vec(&self) -> Vec<u8>;
46}
47
48impl NodeIpAddress for Ipv6Addr {
49    fn octets_vec(&self) -> Vec<u8> {
50        self.octets().to_vec()
51    }
52}
53
54impl NodeIpAddress for Ipv4Addr {
55    fn octets_vec(&self) -> Vec<u8> {
56        self.octets().to_vec()
57    }
58}
59
60// ============================================================================
61// Generic IP-Based Node Identity
62// ============================================================================
63
64/// Generic IP-based node identity that binds node ID to network location
65///
66/// This struct provides a unified implementation for both IPv4 and IPv6
67/// node identities, reducing code duplication while maintaining type safety.
68#[derive(Debug, Clone, Serialize, Deserialize)]
69#[allow(dead_code)]
70pub struct GenericIpNodeID<A: NodeIpAddress> {
71    /// Derived node ID (BLAKE3 of ip_addr + public_key + salt + timestamp)
72    pub node_id: Vec<u8>,
73    /// IP address this node ID is bound to
74    pub ip_addr: A,
75    /// ML-DSA public key for signatures
76    pub public_key: Vec<u8>,
77    /// Signature proving ownership of the IP address and keys
78    pub signature: Vec<u8>,
79    /// Timestamp when this ID was generated (seconds since epoch)
80    pub timestamp_secs: u64,
81    /// Salt used in node ID generation (for freshness)
82    pub salt: Vec<u8>,
83}
84
85#[allow(dead_code)]
86impl<A: NodeIpAddress> GenericIpNodeID<A> {
87    /// ML-DSA-65 signature length
88    const SIGNATURE_LENGTH: usize = 3309;
89
90    /// Generate a new IP-based node ID
91    pub fn generate(ip_addr: A, secret: &MlDsaSecretKey, public: &MlDsaPublicKey) -> Result<Self> {
92        let mut rng = rand::thread_rng();
93        let mut salt = vec![0u8; 16];
94        rand::RngCore::fill_bytes(&mut rng, &mut salt);
95
96        let timestamp = SystemTime::now();
97        let timestamp_secs = timestamp.duration_since(UNIX_EPOCH)?.as_secs();
98        let public_key = public.as_bytes().to_vec();
99        let ip_octets = ip_addr.octets_vec();
100
101        // Generate node ID: BLAKE3(ip_address || public_key || salt || timestamp)
102        let node_id = Self::compute_node_id(&ip_octets, &public_key, &salt, timestamp_secs);
103
104        // Create signature proving ownership
105        let message_to_sign = Self::build_message(&ip_octets, &public_key, &salt, timestamp_secs);
106        let sig = ml_dsa_sign(secret, &message_to_sign)
107            .map_err(|e| anyhow!("ML-DSA sign failed: {:?}", e))?;
108        let signature = sig.0.to_vec();
109
110        Ok(Self {
111            node_id,
112            ip_addr,
113            public_key,
114            signature,
115            timestamp_secs,
116            salt,
117        })
118    }
119
120    /// Verify that this node ID is valid and properly signed
121    pub fn verify(&self) -> Result<bool> {
122        let ip_octets = self.ip_addr.octets_vec();
123
124        // Reconstruct and verify node ID
125        let expected_node_id = Self::compute_node_id(
126            &ip_octets,
127            &self.public_key,
128            &self.salt,
129            self.timestamp_secs,
130        );
131
132        if expected_node_id != self.node_id {
133            return Ok(false);
134        }
135
136        // Verify signature
137        let public_key = MlDsaPublicKey::from_bytes(&self.public_key)
138            .map_err(|e| anyhow!("Invalid ML-DSA public key: {:?}", e))?;
139
140        if self.signature.len() != Self::SIGNATURE_LENGTH {
141            return Ok(false);
142        }
143
144        let mut sig_bytes = [0u8; 3309];
145        sig_bytes.copy_from_slice(&self.signature);
146        let signature = MlDsaSignature(Box::new(sig_bytes));
147
148        let message_to_verify = Self::build_message(
149            &ip_octets,
150            &self.public_key,
151            &self.salt,
152            self.timestamp_secs,
153        );
154
155        let ok = ml_dsa_verify(&public_key, &message_to_verify, &signature)
156            .map_err(|e| anyhow!("ML-DSA verify error: {:?}", e))?;
157        Ok(ok)
158    }
159
160    /// Get the age of this node ID in seconds
161    pub fn age_secs(&self) -> u64 {
162        let now = SystemTime::now()
163            .duration_since(UNIX_EPOCH)
164            .map(|d| d.as_secs())
165            .unwrap_or(0);
166        now.saturating_sub(self.timestamp_secs)
167    }
168
169    /// Check if the node ID has expired (older than max_age)
170    pub fn is_expired(&self, max_age: Duration) -> bool {
171        self.age_secs() > max_age.as_secs()
172    }
173
174    // Internal helpers
175
176    #[inline]
177    fn compute_node_id(
178        ip_octets: &[u8],
179        public_key: &[u8],
180        salt: &[u8],
181        timestamp_secs: u64,
182    ) -> Vec<u8> {
183        let mut hasher = blake3::Hasher::new();
184        hasher.update(ip_octets);
185        hasher.update(public_key);
186        hasher.update(salt);
187        hasher.update(&timestamp_secs.to_le_bytes());
188        hasher.finalize().as_bytes().to_vec()
189    }
190
191    #[inline]
192    fn build_message(
193        ip_octets: &[u8],
194        public_key: &[u8],
195        salt: &[u8],
196        timestamp_secs: u64,
197    ) -> Vec<u8> {
198        let mut message = Vec::with_capacity(ip_octets.len() + public_key.len() + salt.len() + 8);
199        message.extend_from_slice(ip_octets);
200        message.extend_from_slice(public_key);
201        message.extend_from_slice(salt);
202        message.extend_from_slice(&timestamp_secs.to_le_bytes());
203        message
204    }
205}
206
207// ============================================================================
208// Backward-Compatible Type Aliases and Wrappers
209// ============================================================================
210
211/// IPv6-based node identity that binds node ID to actual network location
212#[derive(Debug, Clone, Serialize, Deserialize)]
213#[allow(dead_code)]
214pub struct IPv6NodeID {
215    /// Derived node ID (BLAKE3 of ipv6_addr + public_key + salt)
216    pub node_id: Vec<u8>,
217    /// IPv6 address this node ID is bound to
218    pub ipv6_addr: Ipv6Addr,
219    /// ML-DSA public key for signatures
220    pub public_key: Vec<u8>,
221    /// Signature proving ownership of the IPv6 address and keys
222    pub signature: Vec<u8>,
223    /// Timestamp when this ID was generated (seconds since epoch)
224    pub timestamp_secs: u64,
225    /// Salt used in node ID generation (for freshness)
226    pub salt: Vec<u8>,
227}
228
229/// Configuration for IP diversity enforcement at multiple subnet levels
230#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct IPDiversityConfig {
232    // === IPv6 subnet limits (existing) ===
233    /// Maximum nodes per /64 subnet (default: 1)
234    pub max_nodes_per_ipv6_64: usize,
235    /// Maximum nodes per /48 allocation (default: 3)
236    pub max_nodes_per_ipv6_48: usize,
237    /// Maximum nodes per /32 region (default: 10)
238    pub max_nodes_per_ipv6_32: usize,
239
240    // === IPv4 subnet limits ===
241    /// Optional guaranteed minimum for nodes per single IPv4 address (/32).
242    /// When `None` (default), the dynamic per-IP limit is used alone.
243    /// When `Some(n)`, the effective limit is `max(n, dynamic_per_ip)`,
244    /// ensuring at least `n` nodes are allowed even when the dynamic
245    /// formula yields a lower value (e.g. during bootstrap).
246    #[serde(default, skip_serializing_if = "Option::is_none")]
247    pub max_nodes_per_ipv4_32: Option<usize>,
248    /// Optional guaranteed minimum for nodes per /24 subnet (Class C).
249    /// When `None` (default), `dynamic_per_ip * 3` is used alone.
250    /// When `Some(n)`, the effective limit is `max(n, dynamic_per_ip * 3)`.
251    #[serde(default, skip_serializing_if = "Option::is_none")]
252    pub max_nodes_per_ipv4_24: Option<usize>,
253    /// Optional guaranteed minimum for nodes per /16 subnet (Class B).
254    /// When `None` (default), `dynamic_per_ip * 10` is used alone.
255    /// When `Some(n)`, the effective limit is `max(n, dynamic_per_ip * 10)`.
256    #[serde(default, skip_serializing_if = "Option::is_none")]
257    pub max_nodes_per_ipv4_16: Option<usize>,
258
259    // === Per-protocol limit overrides ===
260    /// Optional floor for all IPv4 subnet limits. When set, the effective
261    /// limit at every IPv4 prefix level (/32, /24, /16) will be at least
262    /// this value, overriding the dynamic calculation and existing hard caps.
263    #[serde(default, skip_serializing_if = "Option::is_none")]
264    pub ipv4_limit_floor: Option<usize>,
265    /// Optional ceiling for all IPv4 subnet limits. When set, the effective
266    /// limit at every IPv4 prefix level will be at most this value.
267    #[serde(default, skip_serializing_if = "Option::is_none")]
268    pub ipv4_limit_ceiling: Option<usize>,
269    /// Optional floor for all IPv6 subnet limits.
270    #[serde(default, skip_serializing_if = "Option::is_none")]
271    pub ipv6_limit_floor: Option<usize>,
272    /// Optional ceiling for all IPv6 subnet limits.
273    #[serde(default, skip_serializing_if = "Option::is_none")]
274    pub ipv6_limit_ceiling: Option<usize>,
275
276    // === Network-relative limits ===
277    /// Absolute maximum nodes allowed per single IP (default: 50)
278    pub max_per_ip_cap: usize,
279    /// Maximum fraction of network any single IP can represent (default: 0.005 = 0.5%)
280    pub max_network_fraction: f64,
281
282    // === ASN and GeoIP (existing) ===
283    /// Maximum nodes per AS number (default: 20)
284    pub max_nodes_per_asn: usize,
285    /// Enable GeoIP-based diversity checks
286    pub enable_geolocation_check: bool,
287    /// Minimum number of different countries required
288    pub min_geographic_diversity: usize,
289}
290
291/// Analysis of an IPv6 address for diversity enforcement
292#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
293pub struct IPAnalysis {
294    /// Whether the original address is a loopback address (::1 or
295    /// an IPv4-mapped loopback like ::ffff:127.0.0.1).
296    #[serde(default)]
297    pub is_loopback: bool,
298    /// /64 subnet (host allocation)
299    pub subnet_64: Ipv6Addr,
300    /// /48 subnet (site allocation)
301    pub subnet_48: Ipv6Addr,
302    /// /32 subnet (ISP allocation)
303    pub subnet_32: Ipv6Addr,
304    /// Autonomous System Number (if available)
305    pub asn: Option<u32>,
306    /// Country code from GeoIP lookup
307    pub country: Option<String>,
308    /// Whether this is a known hosting/VPS provider
309    pub is_hosting_provider: bool,
310    /// Whether this is a known VPN provider
311    pub is_vpn_provider: bool,
312    /// Historical reputation score for this IP range
313    pub reputation_score: f64,
314}
315
316/// Analysis of an IPv4 address for diversity enforcement
317#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
318#[allow(dead_code)]
319pub struct IPv4Analysis {
320    /// The exact IPv4 address
321    pub ip_addr: Ipv4Addr,
322    /// /24 subnet (Class C equivalent)
323    pub subnet_24: Ipv4Addr,
324    /// /16 subnet (Class B equivalent)
325    pub subnet_16: Ipv4Addr,
326    /// /8 subnet (Class A equivalent)
327    pub subnet_8: Ipv4Addr,
328    /// Autonomous System Number (if available)
329    pub asn: Option<u32>,
330    /// Country code from GeoIP lookup
331    pub country: Option<String>,
332    /// Whether this is a known hosting/VPS provider
333    pub is_hosting_provider: bool,
334    /// Whether this is a known VPN provider
335    pub is_vpn_provider: bool,
336    /// Historical reputation score for this IP range
337    pub reputation_score: f64,
338}
339
340/// Unified IP analysis that handles both IPv4 and IPv6 addresses
341#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
342#[allow(dead_code)]
343pub enum UnifiedIPAnalysis {
344    /// IPv4 address analysis
345    IPv4(IPv4Analysis),
346    /// IPv6 address analysis
347    IPv6(IPAnalysis),
348}
349
350impl Default for IPDiversityConfig {
351    fn default() -> Self {
352        Self {
353            // IPv6 limits
354            max_nodes_per_ipv6_64: 1,
355            max_nodes_per_ipv6_48: 3,
356            max_nodes_per_ipv6_32: 10,
357            // IPv4 limits — None = purely dynamic (no static cap)
358            max_nodes_per_ipv4_32: None,
359            max_nodes_per_ipv4_24: None,
360            max_nodes_per_ipv4_16: None,
361            // Per-protocol overrides — None = no override
362            ipv4_limit_floor: None,
363            ipv4_limit_ceiling: None,
364            ipv6_limit_floor: None,
365            ipv6_limit_ceiling: None,
366            // Network-relative limits
367            max_per_ip_cap: 50,          // Hard cap of 50 nodes per IP
368            max_network_fraction: 0.005, // 0.5% of network max
369            // ASN and GeoIP
370            max_nodes_per_asn: 20,
371            enable_geolocation_check: true,
372            min_geographic_diversity: 3,
373        }
374    }
375}
376
377impl IPDiversityConfig {
378    /// Create a testnet configuration with relaxed diversity requirements.
379    ///
380    /// This is useful for testing environments like Digital Ocean where all nodes
381    /// share the same ASN (AS14061). The relaxed limits allow many nodes from the
382    /// same provider while still maintaining some diversity tracking.
383    ///
384    /// # Warning
385    ///
386    /// This configuration should NEVER be used in production as it significantly
387    /// weakens Sybil attack protection.
388    #[must_use]
389    pub fn testnet() -> Self {
390        Self {
391            // IPv6 relaxed limits
392            max_nodes_per_ipv6_64: 100,  // Allow many nodes per /64 subnet
393            max_nodes_per_ipv6_48: 500,  // Allow many nodes per /48 allocation
394            max_nodes_per_ipv6_32: 1000, // Allow many nodes per /32 region
395            // IPv4 — guaranteed minimums prevent bootstrap deadlock when
396            // many nodes share few public IPs (e.g. 10 nodes per VM).
397            // Without these, the dynamic per-IP formula starts at 1 for
398            // small networks and never grows past the number of unique IPs.
399            max_nodes_per_ipv4_32: Some(100),
400            max_nodes_per_ipv4_24: Some(500),
401            max_nodes_per_ipv4_16: Some(1000),
402            // Floors ensure small testnets (< 100 nodes) aren't bottlenecked
403            // by the dynamic per-IP formula, which yields 1 for tiny networks.
404            ipv4_limit_floor: Some(5),
405            ipv4_limit_ceiling: None,
406            ipv6_limit_floor: Some(5),
407            ipv6_limit_ceiling: None,
408            // Network-relative limits (relaxed for testnet)
409            max_per_ip_cap: 100,       // Higher cap for testing
410            max_network_fraction: 0.1, // Allow 10% of network from one IP (relaxed from 0.5%)
411            // ASN and GeoIP
412            max_nodes_per_asn: 5000, // Allow many nodes from same ASN (e.g., Digital Ocean)
413            enable_geolocation_check: false, // Disable geo checks for testing
414            min_geographic_diversity: 1, // Single region is acceptable for testing
415        }
416    }
417
418    /// Create a permissive configuration that effectively disables diversity checks.
419    ///
420    /// This is useful for local development and unit testing where all nodes
421    /// run on localhost or the same machine.
422    #[must_use]
423    pub fn permissive() -> Self {
424        Self {
425            // IPv6 - effectively disabled
426            max_nodes_per_ipv6_64: usize::MAX,
427            max_nodes_per_ipv6_48: usize::MAX,
428            max_nodes_per_ipv6_32: usize::MAX,
429            // IPv4 — no static caps
430            max_nodes_per_ipv4_32: None,
431            max_nodes_per_ipv4_24: None,
432            max_nodes_per_ipv4_16: None,
433            // Per-protocol overrides — None = no override
434            ipv4_limit_floor: None,
435            ipv4_limit_ceiling: None,
436            ipv6_limit_floor: None,
437            ipv6_limit_ceiling: None,
438            // Network-relative - effectively disabled
439            max_per_ip_cap: usize::MAX,
440            max_network_fraction: 1.0, // Allow 100% of network
441            // ASN and GeoIP
442            max_nodes_per_asn: usize::MAX,
443            enable_geolocation_check: false,
444            min_geographic_diversity: 0,
445        }
446    }
447
448    /// Check if this is a testnet or permissive configuration.
449    #[must_use]
450    pub fn is_relaxed(&self) -> bool {
451        self.max_nodes_per_asn > 100 || !self.enable_geolocation_check
452    }
453}
454
455#[allow(dead_code)]
456impl IPv6NodeID {
457    /// Generate a new IPv6-based node ID
458    ///
459    /// Delegates to `GenericIpNodeID` for the core generation logic.
460    pub fn generate(
461        ipv6_addr: Ipv6Addr,
462        secret: &MlDsaSecretKey,
463        public: &MlDsaPublicKey,
464    ) -> Result<Self> {
465        let generic = GenericIpNodeID::generate(ipv6_addr, secret, public)?;
466        Ok(Self::from_generic(generic))
467    }
468
469    /// Verify that this node ID is valid and properly signed
470    pub fn verify(&self) -> Result<bool> {
471        self.to_generic().verify()
472    }
473
474    /// Extract /64 subnet from IPv6 address
475    pub fn extract_subnet_64(&self) -> Ipv6Addr {
476        let octets = self.ipv6_addr.octets();
477        let mut subnet = [0u8; 16];
478        subnet[..8].copy_from_slice(&octets[..8]);
479        Ipv6Addr::from(subnet)
480    }
481
482    /// Extract /48 subnet from IPv6 address
483    pub fn extract_subnet_48(&self) -> Ipv6Addr {
484        let octets = self.ipv6_addr.octets();
485        let mut subnet = [0u8; 16];
486        subnet[..6].copy_from_slice(&octets[..6]);
487        Ipv6Addr::from(subnet)
488    }
489
490    /// Extract /32 subnet from IPv6 address
491    pub fn extract_subnet_32(&self) -> Ipv6Addr {
492        let octets = self.ipv6_addr.octets();
493        let mut subnet = [0u8; 16];
494        subnet[..4].copy_from_slice(&octets[..4]);
495        Ipv6Addr::from(subnet)
496    }
497
498    // Conversion helpers for delegation
499
500    fn from_generic(g: GenericIpNodeID<Ipv6Addr>) -> Self {
501        Self {
502            node_id: g.node_id,
503            ipv6_addr: g.ip_addr,
504            public_key: g.public_key,
505            signature: g.signature,
506            timestamp_secs: g.timestamp_secs,
507            salt: g.salt,
508        }
509    }
510
511    fn to_generic(&self) -> GenericIpNodeID<Ipv6Addr> {
512        GenericIpNodeID {
513            node_id: self.node_id.clone(),
514            ip_addr: self.ipv6_addr,
515            public_key: self.public_key.clone(),
516            signature: self.signature.clone(),
517            timestamp_secs: self.timestamp_secs,
518            salt: self.salt.clone(),
519        }
520    }
521}
522
523/// IPv4-based node identity that binds node ID to actual network location
524/// Mirrors IPv6NodeID for security parity on IPv4 networks
525#[derive(Debug, Clone, Serialize, Deserialize)]
526#[allow(dead_code)]
527pub struct IPv4NodeID {
528    /// Derived node ID (BLAKE3 of ipv4_addr + public_key + salt + timestamp)
529    pub node_id: Vec<u8>,
530    /// IPv4 address this node ID is bound to
531    pub ipv4_addr: Ipv4Addr,
532    /// ML-DSA public key for signatures
533    pub public_key: Vec<u8>,
534    /// Signature proving ownership of the IPv4 address and keys
535    pub signature: Vec<u8>,
536    /// Timestamp when this ID was generated (seconds since epoch)
537    pub timestamp_secs: u64,
538    /// Salt used in node ID generation (for freshness)
539    pub salt: Vec<u8>,
540}
541
542#[allow(dead_code)]
543impl IPv4NodeID {
544    /// Generate a new IPv4-based node ID
545    ///
546    /// Delegates to `GenericIpNodeID` for the core generation logic.
547    pub fn generate(
548        ipv4_addr: Ipv4Addr,
549        secret: &MlDsaSecretKey,
550        public: &MlDsaPublicKey,
551    ) -> Result<Self> {
552        let generic = GenericIpNodeID::generate(ipv4_addr, secret, public)?;
553        Ok(Self::from_generic(generic))
554    }
555
556    /// Verify that this node ID is valid and properly signed
557    pub fn verify(&self) -> Result<bool> {
558        self.to_generic().verify()
559    }
560
561    /// Extract /24 subnet from IPv4 address (Class C / most ISP allocations)
562    pub fn extract_subnet_24(&self) -> Ipv4Addr {
563        let octets = self.ipv4_addr.octets();
564        Ipv4Addr::new(octets[0], octets[1], octets[2], 0)
565    }
566
567    /// Extract /16 subnet from IPv4 address (Class B / large ISP allocations)
568    pub fn extract_subnet_16(&self) -> Ipv4Addr {
569        let octets = self.ipv4_addr.octets();
570        Ipv4Addr::new(octets[0], octets[1], 0, 0)
571    }
572
573    /// Extract /8 subnet from IPv4 address (Class A / regional allocations)
574    pub fn extract_subnet_8(&self) -> Ipv4Addr {
575        let octets = self.ipv4_addr.octets();
576        Ipv4Addr::new(octets[0], 0, 0, 0)
577    }
578
579    /// Get the age of this node ID in seconds
580    pub fn age_secs(&self) -> u64 {
581        self.to_generic().age_secs()
582    }
583
584    /// Check if the node ID has expired (older than max_age)
585    pub fn is_expired(&self, max_age: Duration) -> bool {
586        self.to_generic().is_expired(max_age)
587    }
588
589    // Conversion helpers for delegation
590
591    fn from_generic(g: GenericIpNodeID<Ipv4Addr>) -> Self {
592        Self {
593            node_id: g.node_id,
594            ipv4_addr: g.ip_addr,
595            public_key: g.public_key,
596            signature: g.signature,
597            timestamp_secs: g.timestamp_secs,
598            salt: g.salt,
599        }
600    }
601
602    fn to_generic(&self) -> GenericIpNodeID<Ipv4Addr> {
603        GenericIpNodeID {
604            node_id: self.node_id.clone(),
605            ip_addr: self.ipv4_addr,
606            public_key: self.public_key.clone(),
607            signature: self.signature.clone(),
608            timestamp_secs: self.timestamp_secs,
609            salt: self.salt.clone(),
610        }
611    }
612}
613
614/// IP diversity enforcement system
615#[derive(Debug)]
616pub struct IPDiversityEnforcer {
617    config: IPDiversityConfig,
618    /// Allow loopback addresses (127.0.0.1, ::1) to bypass diversity checks.
619    ///
620    /// This flag is intentionally separate from `IPDiversityConfig` so that it
621    /// has a single source of truth in the owning component (`NodeConfig`,
622    /// `BootstrapManager`, etc.) rather than being copied into every config.
623    allow_loopback: bool,
624    // IPv6 tracking (LRU caches with max 50k entries to prevent memory DoS)
625    subnet_64_counts: LruCache<Ipv6Addr, usize>,
626    subnet_48_counts: LruCache<Ipv6Addr, usize>,
627    subnet_32_counts: LruCache<Ipv6Addr, usize>,
628    // IPv4 tracking (LRU caches with max 50k entries to prevent memory DoS)
629    #[allow(dead_code)]
630    ipv4_32_counts: LruCache<Ipv4Addr, usize>, // Per exact IP
631    #[allow(dead_code)]
632    ipv4_24_counts: LruCache<Ipv4Addr, usize>, // Per /24 subnet
633    #[allow(dead_code)]
634    ipv4_16_counts: LruCache<Ipv4Addr, usize>, // Per /16 subnet
635    // Shared tracking (LRU caches with max 50k entries to prevent memory DoS)
636    asn_counts: LruCache<u32, usize>,
637    country_counts: LruCache<String, usize>,
638    geo_provider: Option<Arc<dyn GeoProvider + Send + Sync>>,
639    // Network size for dynamic limits
640    #[allow(dead_code)]
641    network_size: usize,
642}
643
644#[allow(dead_code)]
645impl IPDiversityEnforcer {
646    /// Create a new IP diversity enforcer with loopback disabled.
647    pub fn new(config: IPDiversityConfig) -> Self {
648        Self::with_loopback(config, false)
649    }
650
651    /// Create a new IP diversity enforcer with explicit loopback setting.
652    ///
653    /// `allow_loopback` should come from the single source of truth
654    /// (e.g. `NodeConfig.allow_loopback`), not from the diversity config.
655    pub fn with_loopback(config: IPDiversityConfig, allow_loopback: bool) -> Self {
656        let cache_size = NonZeroUsize::new(MAX_SUBNET_TRACKING).unwrap_or(NonZeroUsize::MIN);
657        Self {
658            config,
659            allow_loopback,
660            // IPv6 (LRU caches with bounded size)
661            subnet_64_counts: LruCache::new(cache_size),
662            subnet_48_counts: LruCache::new(cache_size),
663            subnet_32_counts: LruCache::new(cache_size),
664            // IPv4 (LRU caches with bounded size)
665            ipv4_32_counts: LruCache::new(cache_size),
666            ipv4_24_counts: LruCache::new(cache_size),
667            ipv4_16_counts: LruCache::new(cache_size),
668            // Shared (LRU caches with bounded size)
669            asn_counts: LruCache::new(cache_size),
670            country_counts: LruCache::new(cache_size),
671            geo_provider: None,
672            network_size: 0,
673        }
674    }
675
676    /// Create a new IP diversity enforcer with a GeoIP/ASN provider
677    pub fn with_geo_provider(
678        config: IPDiversityConfig,
679        provider: Arc<dyn GeoProvider + Send + Sync>,
680    ) -> Self {
681        let mut s = Self::new(config);
682        s.geo_provider = Some(provider);
683        s
684    }
685
686    /// Analyze an IPv6 address for diversity enforcement
687    pub fn analyze_ip(&self, ipv6_addr: Ipv6Addr) -> Result<IPAnalysis> {
688        let subnet_64 = Self::extract_subnet_prefix(ipv6_addr, 64);
689        let subnet_48 = Self::extract_subnet_prefix(ipv6_addr, 48);
690        let subnet_32 = Self::extract_subnet_prefix(ipv6_addr, 32);
691
692        // GeoIP/ASN lookup via provider if available
693        let (asn, country, is_hosting_provider, is_vpn_provider) =
694            if let Some(p) = &self.geo_provider {
695                let info = p.lookup(ipv6_addr);
696                (
697                    info.asn,
698                    info.country,
699                    info.is_hosting_provider,
700                    info.is_vpn_provider,
701                )
702            } else {
703                (None, None, false, false)
704            };
705
706        // Default reputation for new IPs
707        let reputation_score = 0.5;
708
709        // Detect loopback: native ::1 or IPv4-mapped 127.0.0.0/8.
710        let is_loopback = ipv6_addr.is_loopback()
711            || ipv6_addr
712                .to_ipv4_mapped()
713                .is_some_and(|v4| v4.is_loopback());
714
715        Ok(IPAnalysis {
716            is_loopback,
717            subnet_64,
718            subnet_48,
719            subnet_32,
720            asn,
721            country,
722            is_hosting_provider,
723            is_vpn_provider,
724            reputation_score,
725        })
726    }
727
728    /// Check if a new node can be accepted based on IP diversity constraints
729    pub fn can_accept_node(&self, ip_analysis: &IPAnalysis) -> bool {
730        // Loopback addresses bypass diversity checks only when explicitly allowed.
731        if ip_analysis.is_loopback && self.allow_loopback {
732            return true;
733        }
734
735        // Determine limits based on hosting provider status
736        let (limit_64, limit_48, limit_32, limit_asn) =
737            if ip_analysis.is_hosting_provider || ip_analysis.is_vpn_provider {
738                // Stricter limits for hosting providers (halved)
739                (
740                    std::cmp::max(1, self.config.max_nodes_per_ipv6_64 / 2),
741                    std::cmp::max(1, self.config.max_nodes_per_ipv6_48 / 2),
742                    std::cmp::max(1, self.config.max_nodes_per_ipv6_32 / 2),
743                    std::cmp::max(1, self.config.max_nodes_per_asn / 2),
744                )
745            } else {
746                // Regular limits for normal nodes
747                (
748                    self.config.max_nodes_per_ipv6_64,
749                    self.config.max_nodes_per_ipv6_48,
750                    self.config.max_nodes_per_ipv6_32,
751                    self.config.max_nodes_per_asn,
752                )
753            };
754
755        // Check /64 subnet limit (use peek() for read-only access)
756        if let Some(&count) = self.subnet_64_counts.peek(&ip_analysis.subnet_64)
757            && count >= limit_64
758        {
759            return false;
760        }
761
762        // Check /48 subnet limit (use peek() for read-only access)
763        if let Some(&count) = self.subnet_48_counts.peek(&ip_analysis.subnet_48)
764            && count >= limit_48
765        {
766            return false;
767        }
768
769        // Check /32 subnet limit (use peek() for read-only access)
770        if let Some(&count) = self.subnet_32_counts.peek(&ip_analysis.subnet_32)
771            && count >= limit_32
772        {
773            return false;
774        }
775
776        // Check ASN limit (use peek() for read-only access)
777        if let Some(asn) = ip_analysis.asn
778            && let Some(&count) = self.asn_counts.peek(&asn)
779            && count >= limit_asn
780        {
781            return false;
782        }
783
784        true
785    }
786
787    /// Add a node to the diversity tracking
788    pub fn add_node(&mut self, ip_analysis: &IPAnalysis) -> Result<()> {
789        if !self.can_accept_node(ip_analysis) {
790            return Err(anyhow!("IP diversity limits exceeded"));
791        }
792
793        // Update counts (optimized: single hash lookup per cache)
794        let count_64 = self
795            .subnet_64_counts
796            .get(&ip_analysis.subnet_64)
797            .copied()
798            .unwrap_or(0)
799            + 1;
800        self.subnet_64_counts.put(ip_analysis.subnet_64, count_64);
801
802        let count_48 = self
803            .subnet_48_counts
804            .get(&ip_analysis.subnet_48)
805            .copied()
806            .unwrap_or(0)
807            + 1;
808        self.subnet_48_counts.put(ip_analysis.subnet_48, count_48);
809
810        let count_32 = self
811            .subnet_32_counts
812            .get(&ip_analysis.subnet_32)
813            .copied()
814            .unwrap_or(0)
815            + 1;
816        self.subnet_32_counts.put(ip_analysis.subnet_32, count_32);
817
818        if let Some(asn) = ip_analysis.asn {
819            let count = self.asn_counts.get(&asn).copied().unwrap_or(0) + 1;
820            self.asn_counts.put(asn, count);
821        }
822
823        if let Some(ref country) = ip_analysis.country {
824            let count = self.country_counts.get(country).copied().unwrap_or(0) + 1;
825            self.country_counts.put(country.clone(), count);
826        }
827
828        Ok(())
829    }
830
831    /// Remove a node from diversity tracking
832    pub fn remove_node(&mut self, ip_analysis: &IPAnalysis) {
833        // Optimized: pop removes and returns value in single hash operation
834        if let Some(count) = self.subnet_64_counts.pop(&ip_analysis.subnet_64) {
835            let new_count = count.saturating_sub(1);
836            if new_count > 0 {
837                self.subnet_64_counts.put(ip_analysis.subnet_64, new_count);
838            }
839        }
840
841        if let Some(count) = self.subnet_48_counts.pop(&ip_analysis.subnet_48) {
842            let new_count = count.saturating_sub(1);
843            if new_count > 0 {
844                self.subnet_48_counts.put(ip_analysis.subnet_48, new_count);
845            }
846        }
847
848        if let Some(count) = self.subnet_32_counts.pop(&ip_analysis.subnet_32) {
849            let new_count = count.saturating_sub(1);
850            if new_count > 0 {
851                self.subnet_32_counts.put(ip_analysis.subnet_32, new_count);
852            }
853        }
854
855        if let Some(asn) = ip_analysis.asn
856            && let Some(count) = self.asn_counts.pop(&asn)
857        {
858            let new_count = count.saturating_sub(1);
859            if new_count > 0 {
860                self.asn_counts.put(asn, new_count);
861            }
862        }
863
864        if let Some(ref country) = ip_analysis.country
865            && let Some(count) = self.country_counts.pop(country)
866        {
867            let new_count = count.saturating_sub(1);
868            if new_count > 0 {
869                self.country_counts.put(country.clone(), new_count);
870            }
871        }
872    }
873
874    /// Extract network prefix of specified length from IPv6 address
875    pub fn extract_subnet_prefix(addr: Ipv6Addr, prefix_len: u8) -> Ipv6Addr {
876        let octets = addr.octets();
877        let mut subnet = [0u8; 16];
878
879        let bytes_to_copy = (prefix_len / 8) as usize;
880        let remaining_bits = prefix_len % 8;
881
882        // Copy full bytes
883        if bytes_to_copy < 16 {
884            subnet[..bytes_to_copy].copy_from_slice(&octets[..bytes_to_copy]);
885        } else {
886            subnet.copy_from_slice(&octets);
887        }
888
889        // Handle partial byte
890        if remaining_bits > 0 && bytes_to_copy < 16 {
891            let mask = 0xFF << (8 - remaining_bits);
892            subnet[bytes_to_copy] = octets[bytes_to_copy] & mask;
893        }
894
895        Ipv6Addr::from(subnet)
896    }
897
898    /// Get diversity statistics
899    pub fn get_diversity_stats(&self) -> DiversityStats {
900        // LRU cache API: use iter() instead of values()
901        let max_nodes_per_ipv6_64 = self
902            .subnet_64_counts
903            .iter()
904            .map(|(_, &v)| v)
905            .max()
906            .unwrap_or(0);
907        let max_nodes_per_ipv6_48 = self
908            .subnet_48_counts
909            .iter()
910            .map(|(_, &v)| v)
911            .max()
912            .unwrap_or(0);
913        let max_nodes_per_ipv6_32 = self
914            .subnet_32_counts
915            .iter()
916            .map(|(_, &v)| v)
917            .max()
918            .unwrap_or(0);
919        let max_nodes_per_ipv4_32 = self
920            .ipv4_32_counts
921            .iter()
922            .map(|(_, &v)| v)
923            .max()
924            .unwrap_or(0);
925        let max_nodes_per_ipv4_24 = self
926            .ipv4_24_counts
927            .iter()
928            .map(|(_, &v)| v)
929            .max()
930            .unwrap_or(0);
931        let max_nodes_per_ipv4_16 = self
932            .ipv4_16_counts
933            .iter()
934            .map(|(_, &v)| v)
935            .max()
936            .unwrap_or(0);
937
938        DiversityStats {
939            total_64_subnets: self.subnet_64_counts.len(),
940            total_48_subnets: self.subnet_48_counts.len(),
941            total_32_subnets: self.subnet_32_counts.len(),
942            total_asns: self.asn_counts.len(),
943            total_countries: self.country_counts.len(),
944            max_nodes_per_ipv6_64,
945            max_nodes_per_ipv6_48,
946            max_nodes_per_ipv6_32,
947            // IPv4 stats
948            total_ipv4_32: self.ipv4_32_counts.len(),
949            total_ipv4_24_subnets: self.ipv4_24_counts.len(),
950            total_ipv4_16_subnets: self.ipv4_16_counts.len(),
951            max_nodes_per_ipv4_32,
952            max_nodes_per_ipv4_24,
953            max_nodes_per_ipv4_16,
954        }
955    }
956
957    // === IPv4 Methods (new) ===
958
959    /// Set the current network size for dynamic limit calculation
960    pub fn set_network_size(&mut self, size: usize) {
961        self.network_size = size;
962    }
963
964    /// Get the current network size
965    pub fn get_network_size(&self) -> usize {
966        self.network_size
967    }
968
969    /// Calculate the dynamic per-IP limit: min(cap, floor(network_size * fraction))
970    /// Formula: min(50, floor(network_size * 0.005))
971    pub fn get_per_ip_limit(&self) -> usize {
972        let fraction_limit =
973            (self.network_size as f64 * self.config.max_network_fraction).floor() as usize;
974        std::cmp::min(self.config.max_per_ip_cap, std::cmp::max(1, fraction_limit))
975    }
976
977    /// Extract /24 subnet from IPv4 address
978    fn extract_ipv4_subnet_24(addr: Ipv4Addr) -> Ipv4Addr {
979        let octets = addr.octets();
980        Ipv4Addr::new(octets[0], octets[1], octets[2], 0)
981    }
982
983    /// Extract /16 subnet from IPv4 address
984    fn extract_ipv4_subnet_16(addr: Ipv4Addr) -> Ipv4Addr {
985        let octets = addr.octets();
986        Ipv4Addr::new(octets[0], octets[1], 0, 0)
987    }
988
989    /// Extract /8 subnet from IPv4 address
990    fn extract_ipv4_subnet_8(addr: Ipv4Addr) -> Ipv4Addr {
991        let octets = addr.octets();
992        Ipv4Addr::new(octets[0], 0, 0, 0)
993    }
994
995    /// Analyze an IPv4 address for diversity enforcement
996    pub fn analyze_ipv4(&self, ipv4_addr: Ipv4Addr) -> Result<IPv4Analysis> {
997        let subnet_24 = Self::extract_ipv4_subnet_24(ipv4_addr);
998        let subnet_16 = Self::extract_ipv4_subnet_16(ipv4_addr);
999        let subnet_8 = Self::extract_ipv4_subnet_8(ipv4_addr);
1000
1001        // For IPv4, we don't have GeoIP lookup yet (would need IPv4 support in GeoProvider)
1002        // Using defaults for now
1003        let asn = None;
1004        let country = None;
1005        let is_hosting_provider = false;
1006        let is_vpn_provider = false;
1007        let reputation_score = 0.5;
1008
1009        Ok(IPv4Analysis {
1010            ip_addr: ipv4_addr,
1011            subnet_24,
1012            subnet_16,
1013            subnet_8,
1014            asn,
1015            country,
1016            is_hosting_provider,
1017            is_vpn_provider,
1018            reputation_score,
1019        })
1020    }
1021
1022    /// Analyze any IP address (IPv4 or IPv6) for diversity enforcement
1023    pub fn analyze_unified(&self, addr: std::net::IpAddr) -> Result<UnifiedIPAnalysis> {
1024        match addr {
1025            std::net::IpAddr::V4(ipv4) => {
1026                let analysis = self.analyze_ipv4(ipv4)?;
1027                Ok(UnifiedIPAnalysis::IPv4(analysis))
1028            }
1029            std::net::IpAddr::V6(ipv6) => {
1030                let analysis = self.analyze_ip(ipv6)?;
1031                Ok(UnifiedIPAnalysis::IPv6(analysis))
1032            }
1033        }
1034    }
1035
1036    /// Check if a node can be accepted based on unified IP diversity constraints
1037    pub fn can_accept_unified(&self, analysis: &UnifiedIPAnalysis) -> bool {
1038        match analysis {
1039            UnifiedIPAnalysis::IPv4(ipv4_analysis) => self.can_accept_ipv4(ipv4_analysis),
1040            UnifiedIPAnalysis::IPv6(ipv6_analysis) => self.can_accept_node(ipv6_analysis),
1041        }
1042    }
1043
1044    /// Check if an IPv4 node can be accepted based on diversity constraints
1045    fn can_accept_ipv4(&self, analysis: &IPv4Analysis) -> bool {
1046        // Loopback addresses bypass diversity checks only when explicitly allowed.
1047        if analysis.ip_addr.is_loopback() && self.allow_loopback {
1048            return true;
1049        }
1050
1051        // Get dynamic per-IP limit
1052        let per_ip_limit = self.get_per_ip_limit();
1053
1054        // Determine multipliers for subnet limits
1055        let limit_32 = self
1056            .config
1057            .max_nodes_per_ipv4_32
1058            .map_or(per_ip_limit, |floor| floor.max(per_ip_limit));
1059        let limit_24 = self
1060            .config
1061            .max_nodes_per_ipv4_24
1062            .map_or(per_ip_limit * 3, |floor| floor.max(per_ip_limit * 3));
1063        let limit_16 = self
1064            .config
1065            .max_nodes_per_ipv4_16
1066            .map_or(per_ip_limit * 10, |floor| floor.max(per_ip_limit * 10));
1067
1068        // Apply stricter limits for hosting/VPN providers
1069        let (limit_32, limit_24, limit_16) =
1070            if analysis.is_hosting_provider || analysis.is_vpn_provider {
1071                (
1072                    std::cmp::max(1, limit_32 / 2),
1073                    std::cmp::max(1, limit_24 / 2),
1074                    std::cmp::max(1, limit_16 / 2),
1075                )
1076            } else {
1077                (limit_32, limit_24, limit_16)
1078            };
1079
1080        // Check /32 (exact IP) limit (use peek() for read-only access)
1081        if let Some(&count) = self.ipv4_32_counts.peek(&analysis.ip_addr)
1082            && count >= limit_32
1083        {
1084            return false;
1085        }
1086
1087        // Check /24 subnet limit (use peek() for read-only access)
1088        if let Some(&count) = self.ipv4_24_counts.peek(&analysis.subnet_24)
1089            && count >= limit_24
1090        {
1091            return false;
1092        }
1093
1094        // Check /16 subnet limit (use peek() for read-only access)
1095        if let Some(&count) = self.ipv4_16_counts.peek(&analysis.subnet_16)
1096            && count >= limit_16
1097        {
1098            return false;
1099        }
1100
1101        // Check ASN limit (shared with IPv6, use peek() for read-only access)
1102        if let Some(asn) = analysis.asn
1103            && let Some(&count) = self.asn_counts.peek(&asn)
1104            && count >= self.config.max_nodes_per_asn
1105        {
1106            return false;
1107        }
1108
1109        true
1110    }
1111
1112    /// Add a unified node to the diversity tracking
1113    pub fn add_unified(&mut self, analysis: &UnifiedIPAnalysis) -> Result<()> {
1114        match analysis {
1115            UnifiedIPAnalysis::IPv4(ipv4_analysis) => self.add_ipv4(ipv4_analysis),
1116            UnifiedIPAnalysis::IPv6(ipv6_analysis) => self.add_node(ipv6_analysis),
1117        }
1118    }
1119
1120    /// Add an IPv4 node to diversity tracking
1121    fn add_ipv4(&mut self, analysis: &IPv4Analysis) -> Result<()> {
1122        if !self.can_accept_ipv4(analysis) {
1123            return Err(anyhow!("IPv4 diversity limits exceeded"));
1124        }
1125
1126        // Update counts (optimized: single hash lookup per cache)
1127        let count_32 = self
1128            .ipv4_32_counts
1129            .get(&analysis.ip_addr)
1130            .copied()
1131            .unwrap_or(0)
1132            + 1;
1133        self.ipv4_32_counts.put(analysis.ip_addr, count_32);
1134
1135        let count_24 = self
1136            .ipv4_24_counts
1137            .get(&analysis.subnet_24)
1138            .copied()
1139            .unwrap_or(0)
1140            + 1;
1141        self.ipv4_24_counts.put(analysis.subnet_24, count_24);
1142
1143        let count_16 = self
1144            .ipv4_16_counts
1145            .get(&analysis.subnet_16)
1146            .copied()
1147            .unwrap_or(0)
1148            + 1;
1149        self.ipv4_16_counts.put(analysis.subnet_16, count_16);
1150
1151        if let Some(asn) = analysis.asn {
1152            let count = self.asn_counts.get(&asn).copied().unwrap_or(0) + 1;
1153            self.asn_counts.put(asn, count);
1154        }
1155
1156        if let Some(ref country) = analysis.country {
1157            let count = self.country_counts.get(country).copied().unwrap_or(0) + 1;
1158            self.country_counts.put(country.clone(), count);
1159        }
1160
1161        Ok(())
1162    }
1163
1164    /// Remove a unified node from diversity tracking
1165    pub fn remove_unified(&mut self, analysis: &UnifiedIPAnalysis) {
1166        match analysis {
1167            UnifiedIPAnalysis::IPv4(ipv4_analysis) => self.remove_ipv4(ipv4_analysis),
1168            UnifiedIPAnalysis::IPv6(ipv6_analysis) => self.remove_node(ipv6_analysis),
1169        }
1170    }
1171
1172    /// Remove an IPv4 node from diversity tracking
1173    fn remove_ipv4(&mut self, analysis: &IPv4Analysis) {
1174        // Optimized: pop removes and returns value in single hash operation
1175        if let Some(count) = self.ipv4_32_counts.pop(&analysis.ip_addr) {
1176            let new_count = count.saturating_sub(1);
1177            if new_count > 0 {
1178                self.ipv4_32_counts.put(analysis.ip_addr, new_count);
1179            }
1180        }
1181
1182        if let Some(count) = self.ipv4_24_counts.pop(&analysis.subnet_24) {
1183            let new_count = count.saturating_sub(1);
1184            if new_count > 0 {
1185                self.ipv4_24_counts.put(analysis.subnet_24, new_count);
1186            }
1187        }
1188
1189        if let Some(count) = self.ipv4_16_counts.pop(&analysis.subnet_16) {
1190            let new_count = count.saturating_sub(1);
1191            if new_count > 0 {
1192                self.ipv4_16_counts.put(analysis.subnet_16, new_count);
1193            }
1194        }
1195
1196        if let Some(asn) = analysis.asn
1197            && let Some(count) = self.asn_counts.pop(&asn)
1198        {
1199            let new_count = count.saturating_sub(1);
1200            if new_count > 0 {
1201                self.asn_counts.put(asn, new_count);
1202            }
1203        }
1204
1205        if let Some(ref country) = analysis.country
1206            && let Some(count) = self.country_counts.pop(country)
1207        {
1208            let new_count = count.saturating_sub(1);
1209            if new_count > 0 {
1210                self.country_counts.put(country.clone(), new_count);
1211            }
1212        }
1213    }
1214}
1215
1216#[cfg(test)]
1217impl IPDiversityEnforcer {
1218    #[allow(dead_code)]
1219    pub fn config(&self) -> &IPDiversityConfig {
1220        &self.config
1221    }
1222}
1223
1224/// GeoIP/ASN provider trait
1225pub trait GeoProvider: std::fmt::Debug {
1226    fn lookup(&self, ip: Ipv6Addr) -> GeoInfo;
1227}
1228
1229/// Geo information
1230#[derive(Debug, Clone)]
1231pub struct GeoInfo {
1232    pub asn: Option<u32>,
1233    pub country: Option<String>,
1234    pub is_hosting_provider: bool,
1235    pub is_vpn_provider: bool,
1236}
1237
1238/// A simple in-memory caching wrapper for a GeoProvider
1239#[derive(Debug)]
1240#[allow(dead_code)]
1241pub struct CachedGeoProvider<P: GeoProvider> {
1242    inner: P,
1243    cache: parking_lot::RwLock<HashMap<Ipv6Addr, GeoInfo>>,
1244}
1245
1246#[allow(dead_code)]
1247impl<P: GeoProvider> CachedGeoProvider<P> {
1248    pub fn new(inner: P) -> Self {
1249        Self {
1250            inner,
1251            cache: parking_lot::RwLock::new(HashMap::new()),
1252        }
1253    }
1254}
1255
1256impl<P: GeoProvider> GeoProvider for CachedGeoProvider<P> {
1257    fn lookup(&self, ip: Ipv6Addr) -> GeoInfo {
1258        if let Some(info) = self.cache.read().get(&ip).cloned() {
1259            return info;
1260        }
1261        let info = self.inner.lookup(ip);
1262        self.cache.write().insert(ip, info.clone());
1263        info
1264    }
1265}
1266
1267/// Stub provider returning no ASN/GeoIP info
1268#[derive(Debug)]
1269#[allow(dead_code)]
1270pub struct StubGeoProvider;
1271impl GeoProvider for StubGeoProvider {
1272    fn lookup(&self, _ip: Ipv6Addr) -> GeoInfo {
1273        GeoInfo {
1274            asn: None,
1275            country: None,
1276            is_hosting_provider: false,
1277            is_vpn_provider: false,
1278        }
1279    }
1280}
1281
1282/// Diversity statistics for monitoring
1283#[derive(Debug, Clone, Serialize, Deserialize)]
1284#[allow(dead_code)]
1285pub struct DiversityStats {
1286    // === IPv6 stats ===
1287    /// Number of unique /64 subnets represented
1288    pub total_64_subnets: usize,
1289    /// Number of unique /48 subnets represented
1290    pub total_48_subnets: usize,
1291    /// Number of unique /32 subnets represented
1292    pub total_32_subnets: usize,
1293    /// Maximum nodes in any single /64 subnet
1294    pub max_nodes_per_ipv6_64: usize,
1295    /// Maximum nodes in any single /48 subnet
1296    pub max_nodes_per_ipv6_48: usize,
1297    /// Maximum nodes in any single /32 subnet
1298    pub max_nodes_per_ipv6_32: usize,
1299
1300    // === IPv4 stats (new) ===
1301    /// Number of unique IPv4 addresses (/32)
1302    pub total_ipv4_32: usize,
1303    /// Number of unique /24 subnets represented
1304    pub total_ipv4_24_subnets: usize,
1305    /// Number of unique /16 subnets represented
1306    pub total_ipv4_16_subnets: usize,
1307    /// Maximum nodes from any single IPv4 address
1308    pub max_nodes_per_ipv4_32: usize,
1309    /// Maximum nodes in any single /24 subnet
1310    pub max_nodes_per_ipv4_24: usize,
1311    /// Maximum nodes in any single /16 subnet
1312    pub max_nodes_per_ipv4_16: usize,
1313
1314    // === Shared stats ===
1315    /// Number of unique ASNs represented
1316    pub total_asns: usize,
1317    /// Number of unique countries represented
1318    pub total_countries: usize,
1319}
1320
1321// Ed25519 compatibility removed
1322
1323#[cfg(test)]
1324mod tests {
1325    use super::*;
1326    use crate::quantum_crypto::generate_ml_dsa_keypair;
1327
1328    fn create_test_keypair() -> (MlDsaPublicKey, MlDsaSecretKey) {
1329        generate_ml_dsa_keypair().expect("Failed to generate test keypair")
1330    }
1331
1332    fn create_test_ipv6() -> Ipv6Addr {
1333        Ipv6Addr::new(
1334            0x2001, 0xdb8, 0x85a3, 0x0000, 0x0000, 0x8a2e, 0x0370, 0x7334,
1335        )
1336    }
1337
1338    fn create_test_diversity_config() -> IPDiversityConfig {
1339        IPDiversityConfig {
1340            // IPv6 limits
1341            max_nodes_per_ipv6_64: 1,
1342            max_nodes_per_ipv6_48: 3,
1343            max_nodes_per_ipv6_32: 10,
1344            // IPv4 limits — None = purely dynamic
1345            max_nodes_per_ipv4_32: None,
1346            max_nodes_per_ipv4_24: None,
1347            max_nodes_per_ipv4_16: None,
1348            ipv4_limit_floor: None,
1349            ipv4_limit_ceiling: None,
1350            ipv6_limit_floor: None,
1351            ipv6_limit_ceiling: None,
1352            // Network-relative limits
1353            max_per_ip_cap: 50,
1354            max_network_fraction: 0.005,
1355            // ASN and GeoIP
1356            max_nodes_per_asn: 20,
1357            enable_geolocation_check: true,
1358            min_geographic_diversity: 3,
1359        }
1360    }
1361
1362    #[test]
1363    fn test_ipv6_node_id_generation() -> Result<()> {
1364        let (public_key, secret_key) = create_test_keypair();
1365        let ipv6_addr = create_test_ipv6();
1366
1367        let node_id = IPv6NodeID::generate(ipv6_addr, &secret_key, &public_key)?;
1368
1369        assert_eq!(node_id.ipv6_addr, ipv6_addr);
1370        assert_eq!(node_id.public_key.len(), 1952); // ML-DSA-65 public key size
1371        assert_eq!(node_id.signature.len(), 3309); // ML-DSA-65 signature size
1372        assert_eq!(node_id.node_id.len(), 32); // BLAKE3 output
1373        assert_eq!(node_id.salt.len(), 16);
1374        assert!(node_id.timestamp_secs > 0);
1375
1376        Ok(())
1377    }
1378
1379    #[test]
1380    fn test_ipv6_node_id_verification() -> Result<()> {
1381        let (public_key, secret_key) = create_test_keypair();
1382        let ipv6_addr = create_test_ipv6();
1383
1384        let node_id = IPv6NodeID::generate(ipv6_addr, &secret_key, &public_key)?;
1385        let is_valid = node_id.verify()?;
1386
1387        assert!(is_valid);
1388
1389        Ok(())
1390    }
1391
1392    #[test]
1393    fn test_ipv6_node_id_verification_fails_with_wrong_data() -> Result<()> {
1394        let (public_key, secret_key) = create_test_keypair();
1395        let ipv6_addr = create_test_ipv6();
1396
1397        let mut node_id = IPv6NodeID::generate(ipv6_addr, &secret_key, &public_key)?;
1398
1399        // Tamper with the node ID
1400        node_id.node_id[0] ^= 0xFF;
1401        let is_valid = node_id.verify()?;
1402        assert!(!is_valid);
1403
1404        // Test with wrong signature length
1405        let mut node_id2 = IPv6NodeID::generate(ipv6_addr, &secret_key, &public_key)?;
1406        node_id2.signature = vec![0u8; 32]; // Wrong length (should be 3309 for ML-DSA)
1407        let is_valid2 = node_id2.verify()?;
1408        assert!(!is_valid2);
1409
1410        // Test with wrong public key length
1411        let mut node_id3 = IPv6NodeID::generate(ipv6_addr, &secret_key, &public_key)?;
1412        node_id3.public_key = vec![0u8; 16]; // Wrong length (should be 1952 for ML-DSA-65)
1413        let is_valid3 = node_id3.verify()?;
1414        assert!(!is_valid3);
1415
1416        Ok(())
1417    }
1418
1419    #[test]
1420    fn test_ipv6_subnet_extraction() -> Result<()> {
1421        let (public_key, secret_key) = create_test_keypair();
1422        let ipv6_addr = Ipv6Addr::new(
1423            0x2001, 0xdb8, 0x85a3, 0x1234, 0x5678, 0x8a2e, 0x0370, 0x7334,
1424        );
1425
1426        let node_id = IPv6NodeID::generate(ipv6_addr, &secret_key, &public_key)?;
1427
1428        // Test /64 subnet extraction
1429        let subnet_64 = node_id.extract_subnet_64();
1430        let expected_64 = Ipv6Addr::new(0x2001, 0xdb8, 0x85a3, 0x1234, 0, 0, 0, 0);
1431        assert_eq!(subnet_64, expected_64);
1432
1433        // Test /48 subnet extraction
1434        let subnet_48 = node_id.extract_subnet_48();
1435        let expected_48 = Ipv6Addr::new(0x2001, 0xdb8, 0x85a3, 0, 0, 0, 0, 0);
1436        assert_eq!(subnet_48, expected_48);
1437
1438        // Test /32 subnet extraction
1439        let subnet_32 = node_id.extract_subnet_32();
1440        let expected_32 = Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0);
1441        assert_eq!(subnet_32, expected_32);
1442
1443        Ok(())
1444    }
1445
1446    // =========== IPv4 Node ID Tests ===========
1447
1448    fn create_test_ipv4() -> Ipv4Addr {
1449        Ipv4Addr::new(192, 168, 1, 100)
1450    }
1451
1452    #[test]
1453    fn test_ipv4_node_id_generation() -> Result<()> {
1454        let (public_key, secret_key) = create_test_keypair();
1455        let ipv4_addr = create_test_ipv4();
1456
1457        let node_id = IPv4NodeID::generate(ipv4_addr, &secret_key, &public_key)?;
1458
1459        assert_eq!(node_id.ipv4_addr, ipv4_addr);
1460        assert_eq!(node_id.public_key.len(), 1952); // ML-DSA-65 public key size
1461        assert_eq!(node_id.signature.len(), 3309); // ML-DSA-65 signature size
1462        assert_eq!(node_id.node_id.len(), 32); // BLAKE3 output
1463        assert_eq!(node_id.salt.len(), 16);
1464        assert!(node_id.timestamp_secs > 0);
1465
1466        Ok(())
1467    }
1468
1469    #[test]
1470    fn test_ipv4_node_id_verification() -> Result<()> {
1471        let (public_key, secret_key) = create_test_keypair();
1472        let ipv4_addr = create_test_ipv4();
1473
1474        let node_id = IPv4NodeID::generate(ipv4_addr, &secret_key, &public_key)?;
1475        let is_valid = node_id.verify()?;
1476
1477        assert!(is_valid);
1478
1479        Ok(())
1480    }
1481
1482    #[test]
1483    fn test_ipv4_node_id_verification_fails_with_wrong_data() -> Result<()> {
1484        let (public_key, secret_key) = create_test_keypair();
1485        let ipv4_addr = create_test_ipv4();
1486
1487        let mut node_id = IPv4NodeID::generate(ipv4_addr, &secret_key, &public_key)?;
1488
1489        // Tamper with the node ID
1490        node_id.node_id[0] ^= 0xFF;
1491        let is_valid = node_id.verify()?;
1492        assert!(!is_valid);
1493
1494        // Test with wrong signature length
1495        let mut node_id2 = IPv4NodeID::generate(ipv4_addr, &secret_key, &public_key)?;
1496        node_id2.signature = vec![0u8; 32]; // Wrong length (should be 3309 for ML-DSA)
1497        let is_valid2 = node_id2.verify()?;
1498        assert!(!is_valid2);
1499
1500        // Test with wrong public key length
1501        let mut node_id3 = IPv4NodeID::generate(ipv4_addr, &secret_key, &public_key)?;
1502        node_id3.public_key = vec![0u8; 16]; // Wrong length (should be 1952 for ML-DSA-65)
1503        let is_valid3 = node_id3.verify()?;
1504        assert!(!is_valid3);
1505
1506        Ok(())
1507    }
1508
1509    #[test]
1510    fn test_ipv4_subnet_extraction() -> Result<()> {
1511        let (public_key, secret_key) = create_test_keypair();
1512        let ipv4_addr = Ipv4Addr::new(192, 168, 42, 100);
1513
1514        let node_id = IPv4NodeID::generate(ipv4_addr, &secret_key, &public_key)?;
1515
1516        // Test /24 subnet extraction
1517        let subnet_24 = node_id.extract_subnet_24();
1518        let expected_24 = Ipv4Addr::new(192, 168, 42, 0);
1519        assert_eq!(subnet_24, expected_24);
1520
1521        // Test /16 subnet extraction
1522        let subnet_16 = node_id.extract_subnet_16();
1523        let expected_16 = Ipv4Addr::new(192, 168, 0, 0);
1524        assert_eq!(subnet_16, expected_16);
1525
1526        // Test /8 subnet extraction
1527        let subnet_8 = node_id.extract_subnet_8();
1528        let expected_8 = Ipv4Addr::new(192, 0, 0, 0);
1529        assert_eq!(subnet_8, expected_8);
1530
1531        Ok(())
1532    }
1533
1534    #[test]
1535    fn test_ipv4_node_id_age() -> Result<()> {
1536        let (public_key, secret_key) = create_test_keypair();
1537        let ipv4_addr = create_test_ipv4();
1538
1539        let node_id = IPv4NodeID::generate(ipv4_addr, &secret_key, &public_key)?;
1540
1541        // Age should be very small (just created)
1542        assert!(node_id.age_secs() < 5);
1543
1544        // Not expired with a 1 hour max age
1545        assert!(!node_id.is_expired(Duration::from_secs(3600)));
1546
1547        // A freshly created node with 0 age is NOT expired (0 > 0 is false)
1548        // This is correct behavior - a 0-second-old node is not older than 0 seconds
1549        assert!(!node_id.is_expired(Duration::from_secs(0)));
1550
1551        Ok(())
1552    }
1553
1554    #[test]
1555    fn test_ipv4_different_addresses_different_node_ids() -> Result<()> {
1556        let (public_key, secret_key) = create_test_keypair();
1557        let addr1 = Ipv4Addr::new(192, 168, 1, 1);
1558        let addr2 = Ipv4Addr::new(192, 168, 1, 2);
1559
1560        let node_id1 = IPv4NodeID::generate(addr1, &secret_key, &public_key)?;
1561        let node_id2 = IPv4NodeID::generate(addr2, &secret_key, &public_key)?;
1562
1563        // Different addresses should produce different node IDs
1564        assert_ne!(node_id1.node_id, node_id2.node_id);
1565
1566        // Both should verify successfully
1567        assert!(node_id1.verify()?);
1568        assert!(node_id2.verify()?);
1569
1570        Ok(())
1571    }
1572
1573    // =========== End IPv4 Tests ===========
1574
1575    #[test]
1576    fn test_ip_diversity_config_default() {
1577        let config = IPDiversityConfig::default();
1578
1579        assert_eq!(config.max_nodes_per_ipv6_64, 1);
1580        assert_eq!(config.max_nodes_per_ipv6_48, 3);
1581        assert_eq!(config.max_nodes_per_ipv6_32, 10);
1582        assert_eq!(config.max_nodes_per_asn, 20);
1583        assert!(config.enable_geolocation_check);
1584        assert_eq!(config.min_geographic_diversity, 3);
1585    }
1586
1587    #[test]
1588    fn test_ip_diversity_enforcer_creation() {
1589        let config = create_test_diversity_config();
1590        let enforcer = IPDiversityEnforcer::with_loopback(config.clone(), true);
1591
1592        assert_eq!(
1593            enforcer.config.max_nodes_per_ipv6_64,
1594            config.max_nodes_per_ipv6_64
1595        );
1596        assert_eq!(enforcer.subnet_64_counts.len(), 0);
1597        assert_eq!(enforcer.subnet_48_counts.len(), 0);
1598        assert_eq!(enforcer.subnet_32_counts.len(), 0);
1599    }
1600
1601    #[test]
1602    fn test_ip_analysis() -> Result<()> {
1603        let config = create_test_diversity_config();
1604        let enforcer = IPDiversityEnforcer::new(config);
1605
1606        let ipv6_addr = create_test_ipv6();
1607        let analysis = enforcer.analyze_ip(ipv6_addr)?;
1608
1609        assert_eq!(
1610            analysis.subnet_64,
1611            IPDiversityEnforcer::extract_subnet_prefix(ipv6_addr, 64)
1612        );
1613        assert_eq!(
1614            analysis.subnet_48,
1615            IPDiversityEnforcer::extract_subnet_prefix(ipv6_addr, 48)
1616        );
1617        assert_eq!(
1618            analysis.subnet_32,
1619            IPDiversityEnforcer::extract_subnet_prefix(ipv6_addr, 32)
1620        );
1621        assert!(analysis.asn.is_none()); // Not implemented in test
1622        assert!(analysis.country.is_none()); // Not implemented in test
1623        assert!(!analysis.is_hosting_provider);
1624        assert!(!analysis.is_vpn_provider);
1625        assert_eq!(analysis.reputation_score, 0.5);
1626
1627        Ok(())
1628    }
1629
1630    #[test]
1631    fn test_can_accept_node_basic() -> Result<()> {
1632        let config = create_test_diversity_config();
1633        let enforcer = IPDiversityEnforcer::new(config);
1634
1635        let ipv6_addr = create_test_ipv6();
1636        let analysis = enforcer.analyze_ip(ipv6_addr)?;
1637
1638        // Should accept first node
1639        assert!(enforcer.can_accept_node(&analysis));
1640
1641        Ok(())
1642    }
1643
1644    #[test]
1645    fn test_add_and_remove_node() -> Result<()> {
1646        let config = create_test_diversity_config();
1647        let mut enforcer = IPDiversityEnforcer::new(config);
1648
1649        let ipv6_addr = create_test_ipv6();
1650        let analysis = enforcer.analyze_ip(ipv6_addr)?;
1651
1652        // Add node
1653        enforcer.add_node(&analysis)?;
1654        assert_eq!(enforcer.subnet_64_counts.get(&analysis.subnet_64), Some(&1));
1655        assert_eq!(enforcer.subnet_48_counts.get(&analysis.subnet_48), Some(&1));
1656        assert_eq!(enforcer.subnet_32_counts.get(&analysis.subnet_32), Some(&1));
1657
1658        // Remove node
1659        enforcer.remove_node(&analysis);
1660        assert_eq!(enforcer.subnet_64_counts.get(&analysis.subnet_64), None);
1661        assert_eq!(enforcer.subnet_48_counts.get(&analysis.subnet_48), None);
1662        assert_eq!(enforcer.subnet_32_counts.get(&analysis.subnet_32), None);
1663
1664        Ok(())
1665    }
1666
1667    #[test]
1668    fn test_diversity_limits_enforcement() -> Result<()> {
1669        let config = create_test_diversity_config();
1670        let mut enforcer = IPDiversityEnforcer::new(config);
1671
1672        let ipv6_addr1 = Ipv6Addr::new(
1673            0x2001, 0xdb8, 0x85a3, 0x1234, 0x5678, 0x8a2e, 0x0370, 0x7334,
1674        );
1675        let ipv6_addr2 = Ipv6Addr::new(
1676            0x2001, 0xdb8, 0x85a3, 0x1234, 0x5678, 0x8a2e, 0x0370, 0x7335,
1677        ); // Same /64
1678
1679        let analysis1 = enforcer.analyze_ip(ipv6_addr1)?;
1680        let analysis2 = enforcer.analyze_ip(ipv6_addr2)?;
1681
1682        // First node should be accepted
1683        assert!(enforcer.can_accept_node(&analysis1));
1684        enforcer.add_node(&analysis1)?;
1685
1686        // Second node in same /64 should be rejected (max_nodes_per_ipv6_64 = 1)
1687        assert!(!enforcer.can_accept_node(&analysis2));
1688
1689        // But adding should fail
1690        let result = enforcer.add_node(&analysis2);
1691        assert!(result.is_err());
1692        assert!(
1693            result
1694                .unwrap_err()
1695                .to_string()
1696                .contains("IP diversity limits exceeded")
1697        );
1698
1699        Ok(())
1700    }
1701
1702    #[test]
1703    fn test_hosting_provider_stricter_limits() -> Result<()> {
1704        let config = IPDiversityConfig {
1705            max_nodes_per_ipv6_64: 4, // Set higher limit for regular nodes
1706            max_nodes_per_ipv6_48: 8,
1707            ..create_test_diversity_config()
1708        };
1709        let mut enforcer = IPDiversityEnforcer::new(config);
1710
1711        let ipv6_addr = create_test_ipv6();
1712        let mut analysis = enforcer.analyze_ip(ipv6_addr)?;
1713        analysis.is_hosting_provider = true;
1714
1715        // Should accept first hosting provider node
1716        assert!(enforcer.can_accept_node(&analysis));
1717        enforcer.add_node(&analysis)?;
1718
1719        // Add second hosting provider node in same /64 (should be accepted with limit=2)
1720        let ipv6_addr2 = Ipv6Addr::new(
1721            0x2001, 0xdb8, 0x85a3, 0x0000, 0x0000, 0x8a2e, 0x0370, 0x7335,
1722        );
1723        let mut analysis2 = enforcer.analyze_ip(ipv6_addr2)?;
1724        analysis2.is_hosting_provider = true;
1725        analysis2.subnet_64 = analysis.subnet_64; // Force same subnet
1726
1727        assert!(enforcer.can_accept_node(&analysis2));
1728        enforcer.add_node(&analysis2)?;
1729
1730        // Should reject third hosting provider node in same /64 (exceeds limit=2)
1731        let ipv6_addr3 = Ipv6Addr::new(
1732            0x2001, 0xdb8, 0x85a3, 0x0000, 0x0000, 0x8a2e, 0x0370, 0x7336,
1733        );
1734        let mut analysis3 = enforcer.analyze_ip(ipv6_addr3)?;
1735        analysis3.is_hosting_provider = true;
1736        analysis3.subnet_64 = analysis.subnet_64; // Force same subnet
1737
1738        assert!(!enforcer.can_accept_node(&analysis3));
1739
1740        Ok(())
1741    }
1742
1743    #[test]
1744    fn test_diversity_stats() -> Result<()> {
1745        let config = create_test_diversity_config();
1746        let mut enforcer = IPDiversityEnforcer::new(config);
1747
1748        // Add some nodes with different subnets
1749        let addresses = [
1750            Ipv6Addr::new(
1751                0x2001, 0xdb8, 0x85a3, 0x1234, 0x5678, 0x8a2e, 0x0370, 0x7334,
1752            ),
1753            Ipv6Addr::new(
1754                0x2001, 0xdb8, 0x85a4, 0x1234, 0x5678, 0x8a2e, 0x0370, 0x7334,
1755            ), // Different /48
1756            Ipv6Addr::new(
1757                0x2002, 0xdb8, 0x85a3, 0x1234, 0x5678, 0x8a2e, 0x0370, 0x7334,
1758            ), // Different /32
1759        ];
1760
1761        for addr in addresses {
1762            let analysis = enforcer.analyze_ip(addr)?;
1763            enforcer.add_node(&analysis)?;
1764        }
1765
1766        let stats = enforcer.get_diversity_stats();
1767        assert_eq!(stats.total_64_subnets, 3);
1768        assert_eq!(stats.total_48_subnets, 3);
1769        assert_eq!(stats.total_32_subnets, 2); // Two /32 prefixes
1770        assert_eq!(stats.max_nodes_per_ipv6_64, 1);
1771        assert_eq!(stats.max_nodes_per_ipv6_48, 1);
1772        assert_eq!(stats.max_nodes_per_ipv6_32, 2); // 2001:db8 has 2 nodes
1773
1774        Ok(())
1775    }
1776
1777    #[test]
1778    fn test_extract_subnet_prefix() {
1779        let addr = Ipv6Addr::new(
1780            0x2001, 0xdb8, 0x85a3, 0x1234, 0x5678, 0x8a2e, 0x0370, 0x7334,
1781        );
1782
1783        // Test /64 prefix
1784        let prefix_64 = IPDiversityEnforcer::extract_subnet_prefix(addr, 64);
1785        let expected_64 = Ipv6Addr::new(0x2001, 0xdb8, 0x85a3, 0x1234, 0, 0, 0, 0);
1786        assert_eq!(prefix_64, expected_64);
1787
1788        // Test /48 prefix
1789        let prefix_48 = IPDiversityEnforcer::extract_subnet_prefix(addr, 48);
1790        let expected_48 = Ipv6Addr::new(0x2001, 0xdb8, 0x85a3, 0, 0, 0, 0, 0);
1791        assert_eq!(prefix_48, expected_48);
1792
1793        // Test /32 prefix
1794        let prefix_32 = IPDiversityEnforcer::extract_subnet_prefix(addr, 32);
1795        let expected_32 = Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0);
1796        assert_eq!(prefix_32, expected_32);
1797
1798        // Test /56 prefix (partial byte)
1799        let prefix_56 = IPDiversityEnforcer::extract_subnet_prefix(addr, 56);
1800        let expected_56 = Ipv6Addr::new(0x2001, 0xdb8, 0x85a3, 0x1200, 0, 0, 0, 0);
1801        assert_eq!(prefix_56, expected_56);
1802
1803        // Test /128 prefix (full address)
1804        let prefix_128 = IPDiversityEnforcer::extract_subnet_prefix(addr, 128);
1805        assert_eq!(prefix_128, addr);
1806    }
1807
1808    #[test]
1809    fn test_security_types_keypair() {
1810        let (public_key, secret_key) =
1811            generate_ml_dsa_keypair().expect("Failed to generate keypair");
1812
1813        let public_key_bytes = public_key.as_bytes();
1814        assert_eq!(public_key_bytes.len(), 1952); // ML-DSA-65 public key size
1815
1816        let message = b"test message";
1817        let signature = ml_dsa_sign(&secret_key, message).expect("Failed to sign message");
1818        assert_eq!(signature.as_bytes().len(), 3309); // ML-DSA-65 signature size
1819
1820        // Verify the signature
1821        assert!(ml_dsa_verify(&public_key, message, &signature).is_ok());
1822    }
1823
1824    #[test]
1825    fn test_ip_analysis_structure() {
1826        let analysis = IPAnalysis {
1827            is_loopback: false,
1828            subnet_64: Ipv6Addr::new(0x2001, 0xdb8, 0x85a3, 0x1234, 0, 0, 0, 0),
1829            subnet_48: Ipv6Addr::new(0x2001, 0xdb8, 0x85a3, 0, 0, 0, 0, 0),
1830            subnet_32: Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0),
1831            asn: Some(64512),
1832            country: Some("US".to_string()),
1833            is_hosting_provider: true,
1834            is_vpn_provider: false,
1835            reputation_score: 0.75,
1836        };
1837
1838        assert_eq!(analysis.asn, Some(64512));
1839        assert_eq!(analysis.country, Some("US".to_string()));
1840        assert!(analysis.is_hosting_provider);
1841        assert!(!analysis.is_vpn_provider);
1842        assert_eq!(analysis.reputation_score, 0.75);
1843    }
1844
1845    #[test]
1846    fn test_diversity_stats_structure() {
1847        let stats = DiversityStats {
1848            // IPv6 stats
1849            total_64_subnets: 100,
1850            total_48_subnets: 50,
1851            total_32_subnets: 25,
1852            max_nodes_per_ipv6_64: 1,
1853            max_nodes_per_ipv6_48: 3,
1854            max_nodes_per_ipv6_32: 10,
1855            // IPv4 stats
1856            total_ipv4_32: 80,
1857            total_ipv4_24_subnets: 40,
1858            total_ipv4_16_subnets: 20,
1859            max_nodes_per_ipv4_32: 1,
1860            max_nodes_per_ipv4_24: 3,
1861            max_nodes_per_ipv4_16: 10,
1862            // Shared stats
1863            total_asns: 15,
1864            total_countries: 8,
1865        };
1866
1867        // IPv6 assertions
1868        assert_eq!(stats.total_64_subnets, 100);
1869        assert_eq!(stats.total_48_subnets, 50);
1870        assert_eq!(stats.total_32_subnets, 25);
1871        assert_eq!(stats.max_nodes_per_ipv6_64, 1);
1872        assert_eq!(stats.max_nodes_per_ipv6_48, 3);
1873        assert_eq!(stats.max_nodes_per_ipv6_32, 10);
1874        // IPv4 assertions
1875        assert_eq!(stats.total_ipv4_32, 80);
1876        assert_eq!(stats.total_ipv4_24_subnets, 40);
1877        assert_eq!(stats.total_ipv4_16_subnets, 20);
1878        assert_eq!(stats.max_nodes_per_ipv4_32, 1);
1879        assert_eq!(stats.max_nodes_per_ipv4_24, 3);
1880        assert_eq!(stats.max_nodes_per_ipv4_16, 10);
1881        // Shared assertions
1882        assert_eq!(stats.total_asns, 15);
1883        assert_eq!(stats.total_countries, 8);
1884    }
1885
1886    #[test]
1887    fn test_multiple_same_subnet_nodes() -> Result<()> {
1888        let config = IPDiversityConfig {
1889            max_nodes_per_ipv6_64: 3, // Allow more nodes in same /64
1890            max_nodes_per_ipv6_48: 5,
1891            max_nodes_per_ipv6_32: 10,
1892            ..create_test_diversity_config()
1893        };
1894        let mut enforcer = IPDiversityEnforcer::new(config);
1895
1896        let _base_addr = Ipv6Addr::new(
1897            0x2001, 0xdb8, 0x85a3, 0x1234, 0x5678, 0x8a2e, 0x0370, 0x0000,
1898        );
1899
1900        // Add 3 nodes in same /64 subnet
1901        for i in 1..=3 {
1902            let addr = Ipv6Addr::new(0x2001, 0xdb8, 0x85a3, 0x1234, 0x5678, 0x8a2e, 0x0370, i);
1903            let analysis = enforcer.analyze_ip(addr)?;
1904            assert!(enforcer.can_accept_node(&analysis));
1905            enforcer.add_node(&analysis)?;
1906        }
1907
1908        // 4th node should be rejected
1909        let addr4 = Ipv6Addr::new(0x2001, 0xdb8, 0x85a3, 0x1234, 0x5678, 0x8a2e, 0x0370, 4);
1910        let analysis4 = enforcer.analyze_ip(addr4)?;
1911        assert!(!enforcer.can_accept_node(&analysis4));
1912
1913        let stats = enforcer.get_diversity_stats();
1914        assert_eq!(stats.total_64_subnets, 1);
1915        assert_eq!(stats.max_nodes_per_ipv6_64, 3);
1916
1917        Ok(())
1918    }
1919
1920    #[test]
1921    fn test_asn_and_country_tracking() -> Result<()> {
1922        let config = create_test_diversity_config();
1923        let mut enforcer = IPDiversityEnforcer::new(config);
1924
1925        // Create analysis with ASN and country
1926        let ipv6_addr = create_test_ipv6();
1927        let mut analysis = enforcer.analyze_ip(ipv6_addr)?;
1928        analysis.asn = Some(64512);
1929        analysis.country = Some("US".to_string());
1930
1931        enforcer.add_node(&analysis)?;
1932
1933        assert_eq!(enforcer.asn_counts.get(&64512), Some(&1));
1934        assert_eq!(enforcer.country_counts.get("US"), Some(&1));
1935
1936        // Remove and check cleanup
1937        enforcer.remove_node(&analysis);
1938        assert!(!enforcer.asn_counts.contains(&64512));
1939        assert!(!enforcer.country_counts.contains("US"));
1940
1941        Ok(())
1942    }
1943}