Split cache.rs and config.rs into directory modules, create utils/

Module reorganization for three large single-file modules:

cache.rs (792 lines) split into cache/ directory:
- cache/mod.rs: CacheError, CacheStats, re-exports
- cache/entry.rs: CacheEntry with TTL/LRU tracking
- cache/coord_cache.rs: CoordCache (address-to-coordinate mappings)
- cache/route_cache.rs: RouteCache + CachedCoords (discovery routes)
- Added 22 new tests filling coverage gaps across all submodules

config.rs (1318 lines) split into config/ directory:
- config/mod.rs: ConfigError, IdentityConfig, Config struct with file
  loading/merge logic, all 24 integration tests
- config/node.rs: NodeConfig + 9 subsection structs (Limits, RateLimit,
  Retry, Cache, Discovery, Tree, Bloom, Session, Buffers)
- config/transport.rs: TransportInstances<T>, TransportsConfig, UdpConfig
- config/peer.rs: ConnectPolicy, PeerAddress, PeerConfig

DnsConfig and TunConfig moved to upper/config.rs to co-locate with the
upper layer components they configure (TUN interface, DNS responder).

index.rs moved to utils/index.rs as cross-cutting infrastructure that
serves both node and peer layers.
This commit is contained in:
Johnathan Corgan
2026-02-15 17:56:32 +00:00
parent d71e48b0f2
commit 930f139787
20 changed files with 2482 additions and 2117 deletions
+284
View File
@@ -0,0 +1,284 @@
//! Session Index Allocator
//!
//! Manages allocation of 32-bit session indices for O(1) packet dispatch.
//! Each Noise session receives a unique index chosen by the receiver;
//! incoming encrypted packets include the receiver's index for fast lookup.
//!
//! ## Design
//!
//! - Indices are random (cryptographically secure) to prevent guessing
//! - Unique per transport to avoid collision between transports
//! - 32-bit space supports ~65K concurrent sessions before birthday collision
//! - Indices are rotated on rekey for anti-correlation
//!
//! ## Wire Format
//!
//! Encrypted frames include receiver_idx for session lookup:
//!
//! ```text
//! [0x00][receiver_idx:4 LE][counter:8 LE][ciphertext+tag]
//! ```
use rand::Rng;
use std::collections::HashSet;
use thiserror::Error;
/// Errors related to index allocation.
#[derive(Debug, Error)]
pub enum IndexError {
#[error("no available indices (too many active sessions)")]
Exhausted,
#[error("index {0} not found")]
NotFound(u32),
#[error("index {0} already in use")]
AlreadyInUse(u32),
}
/// A 32-bit session index.
///
/// Wrapper type for type safety and clarity in APIs.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct SessionIndex(u32);
impl SessionIndex {
/// Create from raw u32.
pub fn new(value: u32) -> Self {
Self(value)
}
/// Get the raw u32 value.
pub fn as_u32(&self) -> u32 {
self.0
}
/// Convert to little-endian bytes.
pub fn to_le_bytes(&self) -> [u8; 4] {
self.0.to_le_bytes()
}
/// Create from little-endian bytes.
pub fn from_le_bytes(bytes: [u8; 4]) -> Self {
Self(u32::from_le_bytes(bytes))
}
}
impl std::fmt::Display for SessionIndex {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:08x}", self.0)
}
}
/// Allocator for session indices within a single transport.
///
/// Manages a pool of random 32-bit indices, tracking which are in use
/// to prevent collision. Thread-safe for single-threaded async use.
#[derive(Debug)]
pub struct IndexAllocator {
/// Set of currently allocated indices.
in_use: HashSet<u32>,
/// Maximum allocation attempts before giving up.
max_attempts: usize,
}
impl IndexAllocator {
/// Create a new index allocator.
pub fn new() -> Self {
Self {
in_use: HashSet::new(),
max_attempts: 100,
}
}
/// Create with a specific max attempts limit.
pub fn with_max_attempts(max_attempts: usize) -> Self {
Self {
in_use: HashSet::new(),
max_attempts,
}
}
/// Allocate a new random index.
///
/// Returns a cryptographically random 32-bit index that is not
/// currently in use. Returns error if allocation fails after
/// max_attempts tries (indicates too many active sessions).
pub fn allocate(&mut self) -> Result<SessionIndex, IndexError> {
let mut rng = rand::thread_rng();
for _ in 0..self.max_attempts {
let candidate = rng.r#gen::<u32>();
if !self.in_use.contains(&candidate) {
self.in_use.insert(candidate);
return Ok(SessionIndex(candidate));
}
}
Err(IndexError::Exhausted)
}
/// Free an index, returning it to the available pool.
///
/// Returns error if the index was not allocated.
pub fn free(&mut self, index: SessionIndex) -> Result<(), IndexError> {
if self.in_use.remove(&index.0) {
Ok(())
} else {
Err(IndexError::NotFound(index.0))
}
}
/// Check if an index is currently allocated.
pub fn is_allocated(&self, index: SessionIndex) -> bool {
self.in_use.contains(&index.0)
}
/// Number of currently allocated indices.
pub fn count(&self) -> usize {
self.in_use.len()
}
/// Check if the allocator is empty (no indices allocated).
pub fn is_empty(&self) -> bool {
self.in_use.is_empty()
}
/// Reserve a specific index (for testing or migration).
///
/// Returns error if the index is already in use.
pub fn reserve(&mut self, index: SessionIndex) -> Result<(), IndexError> {
if self.in_use.contains(&index.0) {
Err(IndexError::AlreadyInUse(index.0))
} else {
self.in_use.insert(index.0);
Ok(())
}
}
/// Clear all allocations (use with caution).
pub fn clear(&mut self) {
self.in_use.clear();
}
}
impl Default for IndexAllocator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_session_index_roundtrip() {
let idx = SessionIndex::new(0x12345678);
assert_eq!(idx.as_u32(), 0x12345678);
let bytes = idx.to_le_bytes();
assert_eq!(bytes, [0x78, 0x56, 0x34, 0x12]);
let restored = SessionIndex::from_le_bytes(bytes);
assert_eq!(restored, idx);
}
#[test]
fn test_session_index_display() {
let idx = SessionIndex::new(0x000000ff);
assert_eq!(format!("{}", idx), "000000ff");
let idx = SessionIndex::new(0xdeadbeef);
assert_eq!(format!("{}", idx), "deadbeef");
}
#[test]
fn test_allocator_basic() {
let mut alloc = IndexAllocator::new();
assert!(alloc.is_empty());
assert_eq!(alloc.count(), 0);
let idx1 = alloc.allocate().unwrap();
assert!(!alloc.is_empty());
assert_eq!(alloc.count(), 1);
assert!(alloc.is_allocated(idx1));
let idx2 = alloc.allocate().unwrap();
assert_eq!(alloc.count(), 2);
assert!(alloc.is_allocated(idx2));
assert_ne!(idx1, idx2);
alloc.free(idx1).unwrap();
assert_eq!(alloc.count(), 1);
assert!(!alloc.is_allocated(idx1));
assert!(alloc.is_allocated(idx2));
}
#[test]
fn test_allocator_free_not_found() {
let mut alloc = IndexAllocator::new();
let result = alloc.free(SessionIndex::new(12345));
assert!(matches!(result, Err(IndexError::NotFound(12345))));
}
#[test]
fn test_allocator_reserve() {
let mut alloc = IndexAllocator::new();
let idx = SessionIndex::new(0xdeadbeef);
alloc.reserve(idx).unwrap();
assert!(alloc.is_allocated(idx));
// Double reserve fails
let result = alloc.reserve(idx);
assert!(matches!(result, Err(IndexError::AlreadyInUse(0xdeadbeef))));
}
#[test]
fn test_allocator_uniqueness() {
let mut alloc = IndexAllocator::new();
let mut indices = Vec::new();
// Allocate 1000 indices and verify all unique
for _ in 0..1000 {
let idx = alloc.allocate().unwrap();
assert!(!indices.contains(&idx));
indices.push(idx);
}
assert_eq!(alloc.count(), 1000);
}
#[test]
fn test_allocator_clear() {
let mut alloc = IndexAllocator::new();
for _ in 0..10 {
alloc.allocate().unwrap();
}
assert_eq!(alloc.count(), 10);
alloc.clear();
assert!(alloc.is_empty());
assert_eq!(alloc.count(), 0);
}
#[test]
fn test_allocator_reuse_after_free() {
let mut alloc = IndexAllocator::new();
let idx = alloc.allocate().unwrap();
alloc.free(idx).unwrap();
// The specific index might not be reused immediately (random),
// but the count should allow allocation
let idx2 = alloc.allocate().unwrap();
assert_eq!(alloc.count(), 1);
// Can now reserve the original index if it wasn't randomly reused
if idx != idx2 {
alloc.reserve(idx).unwrap();
}
}
}
+6
View File
@@ -0,0 +1,6 @@
//! Utility modules.
//!
//! Shared infrastructure that doesn't belong to a specific protocol layer:
//! session index allocation and other cross-cutting concerns.
pub mod index;