mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
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:
-792
@@ -1,792 +0,0 @@
|
||||
//! Caching Entities
|
||||
//!
|
||||
//! Coordinate and route caching for FIPS routing. The CoordCache stores
|
||||
//! address-to-coordinate mappings populated by session setup, while
|
||||
//! RouteCache stores coordinates learned from discovery queries.
|
||||
|
||||
use crate::tree::TreeCoordinate;
|
||||
use crate::NodeAddr;
|
||||
use std::collections::HashMap;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Default maximum entries in coordinate cache.
|
||||
pub const DEFAULT_COORD_CACHE_SIZE: usize = 50_000;
|
||||
|
||||
/// Default TTL for coordinate cache entries (5 minutes in milliseconds).
|
||||
pub const DEFAULT_COORD_CACHE_TTL_MS: u64 = 300_000;
|
||||
|
||||
/// Default maximum entries in route cache.
|
||||
pub const DEFAULT_ROUTE_CACHE_SIZE: usize = 10_000;
|
||||
|
||||
/// Errors related to cache operations.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum CacheError {
|
||||
#[error("cache full: max {max} entries")]
|
||||
CacheFull { max: usize },
|
||||
|
||||
#[error("entry not found")]
|
||||
NotFound,
|
||||
|
||||
#[error("entry expired")]
|
||||
Expired,
|
||||
}
|
||||
|
||||
/// A cached coordinate entry.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CacheEntry {
|
||||
/// The cached coordinates.
|
||||
coords: TreeCoordinate,
|
||||
/// When this entry was created (Unix milliseconds).
|
||||
created_at: u64,
|
||||
/// When this entry was last used (Unix milliseconds).
|
||||
last_used: u64,
|
||||
/// When this entry expires (Unix milliseconds).
|
||||
expires_at: u64,
|
||||
}
|
||||
|
||||
impl CacheEntry {
|
||||
/// Create a new cache entry.
|
||||
pub fn new(coords: TreeCoordinate, current_time_ms: u64, ttl_ms: u64) -> Self {
|
||||
Self {
|
||||
coords,
|
||||
created_at: current_time_ms,
|
||||
last_used: current_time_ms,
|
||||
expires_at: current_time_ms.saturating_add(ttl_ms),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the cached coordinates.
|
||||
pub fn coords(&self) -> &TreeCoordinate {
|
||||
&self.coords
|
||||
}
|
||||
|
||||
/// Get the creation timestamp.
|
||||
pub fn created_at(&self) -> u64 {
|
||||
self.created_at
|
||||
}
|
||||
|
||||
/// Get the last used timestamp.
|
||||
pub fn last_used(&self) -> u64 {
|
||||
self.last_used
|
||||
}
|
||||
|
||||
/// Get the expiry timestamp.
|
||||
pub fn expires_at(&self) -> u64 {
|
||||
self.expires_at
|
||||
}
|
||||
|
||||
/// Check if this entry has expired.
|
||||
pub fn is_expired(&self, current_time_ms: u64) -> bool {
|
||||
current_time_ms > self.expires_at
|
||||
}
|
||||
|
||||
/// Touch the entry to update last_used time.
|
||||
pub fn touch(&mut self, current_time_ms: u64) {
|
||||
self.last_used = current_time_ms;
|
||||
}
|
||||
|
||||
/// Refresh the expiry time.
|
||||
pub fn refresh(&mut self, current_time_ms: u64, ttl_ms: u64) {
|
||||
self.expires_at = current_time_ms.saturating_add(ttl_ms);
|
||||
self.last_used = current_time_ms;
|
||||
}
|
||||
|
||||
/// Update the coordinates and refresh timestamps.
|
||||
pub fn update(&mut self, coords: TreeCoordinate, current_time_ms: u64, ttl_ms: u64) {
|
||||
self.coords = coords;
|
||||
self.last_used = current_time_ms;
|
||||
self.expires_at = current_time_ms.saturating_add(ttl_ms);
|
||||
}
|
||||
|
||||
/// Time since last use (for LRU eviction).
|
||||
pub fn idle_time(&self, current_time_ms: u64) -> u64 {
|
||||
current_time_ms.saturating_sub(self.last_used)
|
||||
}
|
||||
|
||||
/// Age of the entry.
|
||||
pub fn age(&self, current_time_ms: u64) -> u64 {
|
||||
current_time_ms.saturating_sub(self.created_at)
|
||||
}
|
||||
|
||||
/// Time until expiry (0 if already expired).
|
||||
pub fn time_to_expiry(&self, current_time_ms: u64) -> u64 {
|
||||
self.expires_at.saturating_sub(current_time_ms)
|
||||
}
|
||||
}
|
||||
|
||||
/// Coordinate cache for routing decisions.
|
||||
///
|
||||
/// Maps node addresses to their tree coordinates, enabling data packets
|
||||
/// to be routed without carrying coordinates in every packet. Populated
|
||||
/// by SessionSetup packets.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CoordCache {
|
||||
/// NodeAddr -> coordinates mapping.
|
||||
entries: HashMap<NodeAddr, CacheEntry>,
|
||||
/// Maximum number of entries.
|
||||
max_entries: usize,
|
||||
/// Default TTL for entries (milliseconds).
|
||||
default_ttl_ms: u64,
|
||||
}
|
||||
|
||||
impl CoordCache {
|
||||
/// Create a new coordinate cache.
|
||||
pub fn new(max_entries: usize, default_ttl_ms: u64) -> Self {
|
||||
Self {
|
||||
entries: HashMap::with_capacity(max_entries.min(1000)),
|
||||
max_entries,
|
||||
default_ttl_ms,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a cache with default parameters.
|
||||
pub fn with_defaults() -> Self {
|
||||
Self::new(DEFAULT_COORD_CACHE_SIZE, DEFAULT_COORD_CACHE_TTL_MS)
|
||||
}
|
||||
|
||||
/// Get the maximum capacity.
|
||||
pub fn max_entries(&self) -> usize {
|
||||
self.max_entries
|
||||
}
|
||||
|
||||
/// Get the default TTL.
|
||||
pub fn default_ttl_ms(&self) -> u64 {
|
||||
self.default_ttl_ms
|
||||
}
|
||||
|
||||
/// Set the default TTL.
|
||||
pub fn set_default_ttl_ms(&mut self, ttl_ms: u64) {
|
||||
self.default_ttl_ms = ttl_ms;
|
||||
}
|
||||
|
||||
/// Insert or update a cache entry.
|
||||
pub fn insert(&mut self, addr: NodeAddr, coords: TreeCoordinate, current_time_ms: u64) {
|
||||
// Update existing entry if present
|
||||
if let Some(entry) = self.entries.get_mut(&addr) {
|
||||
entry.update(coords, current_time_ms, self.default_ttl_ms);
|
||||
return;
|
||||
}
|
||||
|
||||
// Evict if at capacity
|
||||
if self.entries.len() >= self.max_entries {
|
||||
self.evict_one(current_time_ms);
|
||||
}
|
||||
|
||||
let entry = CacheEntry::new(coords, current_time_ms, self.default_ttl_ms);
|
||||
self.entries.insert(addr, entry);
|
||||
}
|
||||
|
||||
/// Insert with a custom TTL.
|
||||
pub fn insert_with_ttl(
|
||||
&mut self,
|
||||
addr: NodeAddr,
|
||||
coords: TreeCoordinate,
|
||||
current_time_ms: u64,
|
||||
ttl_ms: u64,
|
||||
) {
|
||||
if let Some(entry) = self.entries.get_mut(&addr) {
|
||||
entry.update(coords, current_time_ms, ttl_ms);
|
||||
return;
|
||||
}
|
||||
|
||||
if self.entries.len() >= self.max_entries {
|
||||
self.evict_one(current_time_ms);
|
||||
}
|
||||
|
||||
let entry = CacheEntry::new(coords, current_time_ms, ttl_ms);
|
||||
self.entries.insert(addr, entry);
|
||||
}
|
||||
|
||||
/// Look up coordinates for an address (without touching).
|
||||
pub fn get(&self, addr: &NodeAddr, current_time_ms: u64) -> Option<&TreeCoordinate> {
|
||||
self.entries.get(addr).and_then(|entry| {
|
||||
if entry.is_expired(current_time_ms) {
|
||||
None
|
||||
} else {
|
||||
Some(entry.coords())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Look up coordinates and touch (update last_used).
|
||||
pub fn get_and_touch(
|
||||
&mut self,
|
||||
addr: &NodeAddr,
|
||||
current_time_ms: u64,
|
||||
) -> Option<&TreeCoordinate> {
|
||||
// Check and remove if expired
|
||||
if let Some(entry) = self.entries.get(addr)
|
||||
&& entry.is_expired(current_time_ms)
|
||||
{
|
||||
self.entries.remove(addr);
|
||||
return None;
|
||||
}
|
||||
|
||||
// Touch and return
|
||||
if let Some(entry) = self.entries.get_mut(addr) {
|
||||
entry.touch(current_time_ms);
|
||||
Some(entry.coords())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the full cache entry.
|
||||
pub fn get_entry(&self, addr: &NodeAddr) -> Option<&CacheEntry> {
|
||||
self.entries.get(addr)
|
||||
}
|
||||
|
||||
/// Remove an entry.
|
||||
pub fn remove(&mut self, addr: &NodeAddr) -> Option<CacheEntry> {
|
||||
self.entries.remove(addr)
|
||||
}
|
||||
|
||||
/// Check if an address is cached (and not expired).
|
||||
pub fn contains(&self, addr: &NodeAddr, current_time_ms: u64) -> bool {
|
||||
self.get(addr, current_time_ms).is_some()
|
||||
}
|
||||
|
||||
/// Number of entries (including expired).
|
||||
pub fn len(&self) -> usize {
|
||||
self.entries.len()
|
||||
}
|
||||
|
||||
/// Check if empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.entries.is_empty()
|
||||
}
|
||||
|
||||
/// Remove all expired entries.
|
||||
pub fn purge_expired(&mut self, current_time_ms: u64) -> usize {
|
||||
let before = self.entries.len();
|
||||
self.entries
|
||||
.retain(|_, entry| !entry.is_expired(current_time_ms));
|
||||
before - self.entries.len()
|
||||
}
|
||||
|
||||
/// Clear all entries.
|
||||
pub fn clear(&mut self) {
|
||||
self.entries.clear();
|
||||
}
|
||||
|
||||
/// Evict one entry (expired first, then LRU).
|
||||
fn evict_one(&mut self, current_time_ms: u64) {
|
||||
// First try to evict an expired entry
|
||||
let expired_key = self
|
||||
.entries
|
||||
.iter()
|
||||
.find(|(_, e)| e.is_expired(current_time_ms))
|
||||
.map(|(k, _)| *k);
|
||||
|
||||
if let Some(key) = expired_key {
|
||||
self.entries.remove(&key);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise evict LRU (oldest last_used)
|
||||
let lru_key = self
|
||||
.entries
|
||||
.iter()
|
||||
.max_by_key(|(_, e)| e.idle_time(current_time_ms))
|
||||
.map(|(k, _)| *k);
|
||||
|
||||
if let Some(key) = lru_key {
|
||||
self.entries.remove(&key);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get cache statistics.
|
||||
pub fn stats(&self, current_time_ms: u64) -> CacheStats {
|
||||
let mut expired = 0;
|
||||
let mut total_age = 0u64;
|
||||
|
||||
for entry in self.entries.values() {
|
||||
if entry.is_expired(current_time_ms) {
|
||||
expired += 1;
|
||||
}
|
||||
total_age += entry.age(current_time_ms);
|
||||
}
|
||||
|
||||
CacheStats {
|
||||
entries: self.entries.len(),
|
||||
max_entries: self.max_entries,
|
||||
expired,
|
||||
avg_age_ms: if self.entries.is_empty() {
|
||||
0
|
||||
} else {
|
||||
total_age / self.entries.len() as u64
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CoordCache {
|
||||
fn default() -> Self {
|
||||
Self::with_defaults()
|
||||
}
|
||||
}
|
||||
|
||||
/// Cache statistics.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CacheStats {
|
||||
/// Current number of entries.
|
||||
pub entries: usize,
|
||||
/// Maximum capacity.
|
||||
pub max_entries: usize,
|
||||
/// Number of expired entries.
|
||||
pub expired: usize,
|
||||
/// Average entry age in milliseconds.
|
||||
pub avg_age_ms: u64,
|
||||
}
|
||||
|
||||
impl CacheStats {
|
||||
/// Fill ratio (entries / max_entries).
|
||||
pub fn fill_ratio(&self) -> f64 {
|
||||
if self.max_entries == 0 {
|
||||
0.0
|
||||
} else {
|
||||
self.entries as f64 / self.max_entries as f64
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A cached route from discovery.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CachedCoords {
|
||||
/// The coordinates discovered.
|
||||
coords: TreeCoordinate,
|
||||
/// When this was discovered (Unix milliseconds).
|
||||
discovered_at: u64,
|
||||
/// Last time we used this route (Unix milliseconds).
|
||||
last_used: u64,
|
||||
}
|
||||
|
||||
impl CachedCoords {
|
||||
/// Create a new cached route.
|
||||
pub fn new(coords: TreeCoordinate, discovered_at: u64) -> Self {
|
||||
Self {
|
||||
coords,
|
||||
discovered_at,
|
||||
last_used: discovered_at,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the coordinates.
|
||||
pub fn coords(&self) -> &TreeCoordinate {
|
||||
&self.coords
|
||||
}
|
||||
|
||||
/// Get the discovery timestamp.
|
||||
pub fn discovered_at(&self) -> u64 {
|
||||
self.discovered_at
|
||||
}
|
||||
|
||||
/// Get the last used timestamp.
|
||||
pub fn last_used(&self) -> u64 {
|
||||
self.last_used
|
||||
}
|
||||
|
||||
/// Touch (update last_used).
|
||||
pub fn touch(&mut self, current_time_ms: u64) {
|
||||
self.last_used = current_time_ms;
|
||||
}
|
||||
|
||||
/// Age since discovery.
|
||||
pub fn age(&self, current_time_ms: u64) -> u64 {
|
||||
current_time_ms.saturating_sub(self.discovered_at)
|
||||
}
|
||||
|
||||
/// Idle time since last use.
|
||||
pub fn idle_time(&self, current_time_ms: u64) -> u64 {
|
||||
current_time_ms.saturating_sub(self.last_used)
|
||||
}
|
||||
|
||||
/// Update coordinates (re-discovered).
|
||||
pub fn update(&mut self, coords: TreeCoordinate, current_time_ms: u64) {
|
||||
self.coords = coords;
|
||||
self.discovered_at = current_time_ms;
|
||||
self.last_used = current_time_ms;
|
||||
}
|
||||
}
|
||||
|
||||
/// Route cache for discovered destinations.
|
||||
///
|
||||
/// Separate from CoordCache, this stores routes learned from the discovery
|
||||
/// protocol (LookupRequest/LookupResponse) rather than session establishment.
|
||||
/// Keyed by NodeAddr.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RouteCache {
|
||||
/// NodeAddr -> discovered coordinates.
|
||||
entries: HashMap<NodeAddr, CachedCoords>,
|
||||
/// Maximum entries.
|
||||
max_entries: usize,
|
||||
}
|
||||
|
||||
impl RouteCache {
|
||||
/// Create a new route cache.
|
||||
pub fn new(max_entries: usize) -> Self {
|
||||
Self {
|
||||
entries: HashMap::with_capacity(max_entries.min(1000)),
|
||||
max_entries,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create with default capacity.
|
||||
pub fn with_defaults() -> Self {
|
||||
Self::new(DEFAULT_ROUTE_CACHE_SIZE)
|
||||
}
|
||||
|
||||
/// Get the maximum capacity.
|
||||
pub fn max_entries(&self) -> usize {
|
||||
self.max_entries
|
||||
}
|
||||
|
||||
/// Insert a discovered route.
|
||||
pub fn insert(&mut self, node_addr: NodeAddr, coords: TreeCoordinate, current_time_ms: u64) {
|
||||
// Update existing
|
||||
if let Some(entry) = self.entries.get_mut(&node_addr) {
|
||||
entry.update(coords, current_time_ms);
|
||||
return;
|
||||
}
|
||||
|
||||
// Evict if full
|
||||
if self.entries.len() >= self.max_entries {
|
||||
self.evict_lru(current_time_ms);
|
||||
}
|
||||
|
||||
self.entries
|
||||
.insert(node_addr, CachedCoords::new(coords, current_time_ms));
|
||||
}
|
||||
|
||||
/// Look up a route (without touching).
|
||||
pub fn get(&self, node_addr: &NodeAddr) -> Option<&CachedCoords> {
|
||||
self.entries.get(node_addr)
|
||||
}
|
||||
|
||||
/// Look up and touch.
|
||||
pub fn get_and_touch(
|
||||
&mut self,
|
||||
node_addr: &NodeAddr,
|
||||
current_time_ms: u64,
|
||||
) -> Option<&TreeCoordinate> {
|
||||
if let Some(entry) = self.entries.get_mut(node_addr) {
|
||||
entry.touch(current_time_ms);
|
||||
Some(entry.coords())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a route (e.g., after route failure).
|
||||
pub fn invalidate(&mut self, node_addr: &NodeAddr) -> Option<CachedCoords> {
|
||||
self.entries.remove(node_addr)
|
||||
}
|
||||
|
||||
/// Check if a node is cached.
|
||||
pub fn contains(&self, node_addr: &NodeAddr) -> bool {
|
||||
self.entries.contains_key(node_addr)
|
||||
}
|
||||
|
||||
/// Number of cached routes.
|
||||
pub fn len(&self) -> usize {
|
||||
self.entries.len()
|
||||
}
|
||||
|
||||
/// Check if empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.entries.is_empty()
|
||||
}
|
||||
|
||||
/// Clear all routes.
|
||||
pub fn clear(&mut self) {
|
||||
self.entries.clear();
|
||||
}
|
||||
|
||||
/// Evict routes older than a threshold.
|
||||
pub fn evict_older_than(&mut self, max_age_ms: u64, current_time_ms: u64) -> usize {
|
||||
let before = self.entries.len();
|
||||
self.entries
|
||||
.retain(|_, entry| entry.age(current_time_ms) < max_age_ms);
|
||||
before - self.entries.len()
|
||||
}
|
||||
|
||||
fn evict_lru(&mut self, current_time_ms: u64) {
|
||||
let lru_id = self
|
||||
.entries
|
||||
.iter()
|
||||
.max_by_key(|(_, e)| e.idle_time(current_time_ms))
|
||||
.map(|(k, _)| *k);
|
||||
|
||||
if let Some(id) = lru_id {
|
||||
self.entries.remove(&id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RouteCache {
|
||||
fn default() -> Self {
|
||||
Self::with_defaults()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_node_addr(val: u8) -> NodeAddr {
|
||||
let mut bytes = [0u8; 16];
|
||||
bytes[0] = val;
|
||||
NodeAddr::from_bytes(bytes)
|
||||
}
|
||||
|
||||
fn make_coords(ids: &[u8]) -> TreeCoordinate {
|
||||
TreeCoordinate::from_addrs(ids.iter().map(|&v| make_node_addr(v)).collect()).unwrap()
|
||||
}
|
||||
|
||||
// ===== CacheEntry Tests =====
|
||||
|
||||
#[test]
|
||||
fn test_cache_entry_expiry() {
|
||||
let coords = make_coords(&[1, 0]);
|
||||
let entry = CacheEntry::new(coords, 1000, 500);
|
||||
|
||||
assert!(!entry.is_expired(1000));
|
||||
assert!(!entry.is_expired(1500)); // expires_at = 1500, not yet expired
|
||||
assert!(entry.is_expired(1501)); // one ms after expiry
|
||||
assert!(entry.is_expired(2000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_entry_refresh() {
|
||||
let coords = make_coords(&[1, 0]);
|
||||
let mut entry = CacheEntry::new(coords, 1000, 500);
|
||||
|
||||
assert!(entry.is_expired(1501)); // expires_at = 1500
|
||||
|
||||
entry.refresh(1400, 500); // new expires_at = 1900
|
||||
|
||||
assert!(!entry.is_expired(1600));
|
||||
assert!(!entry.is_expired(1900)); // at exactly expiry, not expired
|
||||
assert!(entry.is_expired(1901)); // one ms after expiry
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_entry_times() {
|
||||
let coords = make_coords(&[1, 0]);
|
||||
let entry = CacheEntry::new(coords, 1000, 500);
|
||||
|
||||
assert_eq!(entry.created_at(), 1000);
|
||||
assert_eq!(entry.last_used(), 1000);
|
||||
assert_eq!(entry.expires_at(), 1500);
|
||||
assert_eq!(entry.age(1200), 200);
|
||||
assert_eq!(entry.idle_time(1200), 200);
|
||||
assert_eq!(entry.time_to_expiry(1200), 300);
|
||||
assert_eq!(entry.time_to_expiry(1600), 0);
|
||||
}
|
||||
|
||||
// ===== CoordCache Tests =====
|
||||
|
||||
#[test]
|
||||
fn test_coord_cache_basic() {
|
||||
let mut cache = CoordCache::new(100, 1000);
|
||||
let addr = make_node_addr(1);
|
||||
let coords = make_coords(&[1, 0]);
|
||||
|
||||
cache.insert(addr, coords.clone(), 0);
|
||||
|
||||
assert!(cache.contains(&addr, 0));
|
||||
assert_eq!(cache.get(&addr, 0), Some(&coords));
|
||||
assert_eq!(cache.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coord_cache_expiry() {
|
||||
let mut cache = CoordCache::new(100, 1000);
|
||||
let addr = make_node_addr(1);
|
||||
let coords = make_coords(&[1, 0]);
|
||||
|
||||
cache.insert(addr, coords, 0);
|
||||
|
||||
assert!(cache.contains(&addr, 500));
|
||||
assert!(!cache.contains(&addr, 1500));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coord_cache_update() {
|
||||
let mut cache = CoordCache::new(100, 1000);
|
||||
let addr = make_node_addr(1);
|
||||
|
||||
cache.insert(addr, make_coords(&[1, 0]), 0);
|
||||
cache.insert(addr, make_coords(&[1, 2, 0]), 500);
|
||||
|
||||
assert_eq!(cache.len(), 1);
|
||||
let coords = cache.get(&addr, 500).unwrap();
|
||||
assert_eq!(coords.depth(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coord_cache_eviction() {
|
||||
let mut cache = CoordCache::new(2, 10000);
|
||||
|
||||
let addr1 = make_node_addr(1);
|
||||
let addr2 = make_node_addr(2);
|
||||
let addr3 = make_node_addr(3);
|
||||
|
||||
cache.insert(addr1, make_coords(&[1, 0]), 0);
|
||||
cache.insert(addr2, make_coords(&[2, 0]), 100);
|
||||
|
||||
// Touch addr2 to make it more recent
|
||||
let _ = cache.get_and_touch(&addr2, 200);
|
||||
|
||||
// Insert addr3, should evict addr1 (LRU)
|
||||
cache.insert(addr3, make_coords(&[3, 0]), 300);
|
||||
|
||||
assert!(!cache.contains(&addr1, 300));
|
||||
assert!(cache.contains(&addr2, 300));
|
||||
assert!(cache.contains(&addr3, 300));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coord_cache_evict_expired_first() {
|
||||
let mut cache = CoordCache::new(2, 100);
|
||||
|
||||
cache.insert(make_node_addr(1), make_coords(&[1, 0]), 0);
|
||||
cache.insert(make_node_addr(2), make_coords(&[2, 0]), 50);
|
||||
|
||||
// At time 150, addr1 is expired, addr2 is not
|
||||
cache.insert(make_node_addr(3), make_coords(&[3, 0]), 150);
|
||||
|
||||
// addr1 should be evicted (expired), not addr2 (LRU but not expired)
|
||||
assert!(!cache.contains(&make_node_addr(1), 150));
|
||||
assert!(cache.contains(&make_node_addr(2), 150));
|
||||
assert!(cache.contains(&make_node_addr(3), 150));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coord_cache_purge_expired() {
|
||||
let mut cache = CoordCache::new(100, 100);
|
||||
|
||||
cache.insert(make_node_addr(1), make_coords(&[1, 0]), 0); // expires at 100
|
||||
cache.insert(make_node_addr(2), make_coords(&[2, 0]), 50); // expires at 150
|
||||
cache.insert(make_node_addr(3), make_coords(&[3, 0]), 200); // expires at 300
|
||||
|
||||
assert_eq!(cache.len(), 3);
|
||||
|
||||
let purged = cache.purge_expired(151); // both addr1 and addr2 expired
|
||||
|
||||
// Entry 1 and 2 expired, entry 3 still valid
|
||||
assert_eq!(purged, 2);
|
||||
assert_eq!(cache.len(), 1);
|
||||
assert!(cache.contains(&make_node_addr(3), 151));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coord_cache_stats() {
|
||||
let mut cache = CoordCache::new(100, 100);
|
||||
|
||||
cache.insert(make_node_addr(1), make_coords(&[1, 0]), 0);
|
||||
cache.insert(make_node_addr(2), make_coords(&[2, 0]), 50);
|
||||
|
||||
let stats = cache.stats(150);
|
||||
|
||||
assert_eq!(stats.entries, 2);
|
||||
assert_eq!(stats.max_entries, 100);
|
||||
assert_eq!(stats.expired, 1); // addr1 expired
|
||||
assert!(stats.avg_age_ms > 0);
|
||||
}
|
||||
|
||||
// ===== CachedCoords Tests =====
|
||||
|
||||
#[test]
|
||||
fn test_cached_coords() {
|
||||
let coords = make_coords(&[1, 0]);
|
||||
let mut cached = CachedCoords::new(coords.clone(), 1000);
|
||||
|
||||
assert_eq!(cached.coords(), &coords);
|
||||
assert_eq!(cached.discovered_at(), 1000);
|
||||
assert_eq!(cached.last_used(), 1000);
|
||||
|
||||
cached.touch(1500);
|
||||
assert_eq!(cached.last_used(), 1500);
|
||||
assert_eq!(cached.idle_time(1600), 100);
|
||||
assert_eq!(cached.age(1600), 600);
|
||||
}
|
||||
|
||||
// ===== RouteCache Tests =====
|
||||
|
||||
#[test]
|
||||
fn test_route_cache_basic() {
|
||||
let mut cache = RouteCache::new(100);
|
||||
let node = make_node_addr(1);
|
||||
let coords = make_coords(&[1, 0]);
|
||||
|
||||
cache.insert(node, coords.clone(), 0);
|
||||
|
||||
assert!(cache.contains(&node));
|
||||
assert_eq!(cache.get(&node).unwrap().coords(), &coords);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_route_cache_invalidate() {
|
||||
let mut cache = RouteCache::new(100);
|
||||
let node = make_node_addr(1);
|
||||
let coords = make_coords(&[1, 0]);
|
||||
|
||||
cache.insert(node, coords, 0);
|
||||
assert!(cache.contains(&node));
|
||||
|
||||
cache.invalidate(&node);
|
||||
assert!(!cache.contains(&node));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_route_cache_lru_eviction() {
|
||||
let mut cache = RouteCache::new(2);
|
||||
|
||||
let node1 = make_node_addr(1);
|
||||
let node2 = make_node_addr(2);
|
||||
let node3 = make_node_addr(3);
|
||||
|
||||
cache.insert(node1, make_coords(&[1, 0]), 0);
|
||||
cache.insert(node2, make_coords(&[2, 0]), 100);
|
||||
|
||||
// Touch node2
|
||||
let _ = cache.get_and_touch(&node2, 200);
|
||||
|
||||
// Insert node3
|
||||
cache.insert(node3, make_coords(&[3, 0]), 300);
|
||||
|
||||
// node1 should be evicted
|
||||
assert!(!cache.contains(&node1));
|
||||
assert!(cache.contains(&node2));
|
||||
assert!(cache.contains(&node3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_route_cache_evict_older_than() {
|
||||
let mut cache = RouteCache::new(100);
|
||||
|
||||
cache.insert(make_node_addr(1), make_coords(&[1, 0]), 0);
|
||||
cache.insert(make_node_addr(2), make_coords(&[2, 0]), 500);
|
||||
cache.insert(make_node_addr(3), make_coords(&[3, 0]), 1000);
|
||||
|
||||
let evicted = cache.evict_older_than(600, 1000);
|
||||
|
||||
assert_eq!(evicted, 1); // node1 is > 600ms old
|
||||
assert_eq!(cache.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_route_cache_update() {
|
||||
let mut cache = RouteCache::new(100);
|
||||
let node = make_node_addr(1);
|
||||
|
||||
cache.insert(node, make_coords(&[1, 0]), 0);
|
||||
cache.insert(node, make_coords(&[1, 2, 0]), 500);
|
||||
|
||||
assert_eq!(cache.len(), 1);
|
||||
let cached = cache.get(&node).unwrap();
|
||||
assert_eq!(cached.coords().depth(), 2);
|
||||
assert_eq!(cached.discovered_at(), 500);
|
||||
}
|
||||
}
|
||||
Vendored
+477
@@ -0,0 +1,477 @@
|
||||
//! Coordinate cache for routing decisions.
|
||||
//!
|
||||
//! Maps node addresses to their tree coordinates, enabling data packets
|
||||
//! to be routed without carrying coordinates in every packet. Populated
|
||||
//! by SessionSetup packets.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::entry::CacheEntry;
|
||||
use super::CacheStats;
|
||||
use crate::tree::TreeCoordinate;
|
||||
use crate::NodeAddr;
|
||||
|
||||
/// Default maximum entries in coordinate cache.
|
||||
pub const DEFAULT_COORD_CACHE_SIZE: usize = 50_000;
|
||||
|
||||
/// Default TTL for coordinate cache entries (5 minutes in milliseconds).
|
||||
pub const DEFAULT_COORD_CACHE_TTL_MS: u64 = 300_000;
|
||||
|
||||
/// Coordinate cache for routing decisions.
|
||||
///
|
||||
/// Maps node addresses to their tree coordinates, enabling data packets
|
||||
/// to be routed without carrying coordinates in every packet. Populated
|
||||
/// by SessionSetup packets.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CoordCache {
|
||||
/// NodeAddr -> coordinates mapping.
|
||||
entries: HashMap<NodeAddr, CacheEntry>,
|
||||
/// Maximum number of entries.
|
||||
max_entries: usize,
|
||||
/// Default TTL for entries (milliseconds).
|
||||
default_ttl_ms: u64,
|
||||
}
|
||||
|
||||
impl CoordCache {
|
||||
/// Create a new coordinate cache.
|
||||
pub fn new(max_entries: usize, default_ttl_ms: u64) -> Self {
|
||||
Self {
|
||||
entries: HashMap::with_capacity(max_entries.min(1000)),
|
||||
max_entries,
|
||||
default_ttl_ms,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a cache with default parameters.
|
||||
pub fn with_defaults() -> Self {
|
||||
Self::new(DEFAULT_COORD_CACHE_SIZE, DEFAULT_COORD_CACHE_TTL_MS)
|
||||
}
|
||||
|
||||
/// Get the maximum capacity.
|
||||
pub fn max_entries(&self) -> usize {
|
||||
self.max_entries
|
||||
}
|
||||
|
||||
/// Get the default TTL.
|
||||
pub fn default_ttl_ms(&self) -> u64 {
|
||||
self.default_ttl_ms
|
||||
}
|
||||
|
||||
/// Set the default TTL.
|
||||
pub fn set_default_ttl_ms(&mut self, ttl_ms: u64) {
|
||||
self.default_ttl_ms = ttl_ms;
|
||||
}
|
||||
|
||||
/// Insert or update a cache entry.
|
||||
pub fn insert(&mut self, addr: NodeAddr, coords: TreeCoordinate, current_time_ms: u64) {
|
||||
// Update existing entry if present
|
||||
if let Some(entry) = self.entries.get_mut(&addr) {
|
||||
entry.update(coords, current_time_ms, self.default_ttl_ms);
|
||||
return;
|
||||
}
|
||||
|
||||
// Evict if at capacity
|
||||
if self.entries.len() >= self.max_entries {
|
||||
self.evict_one(current_time_ms);
|
||||
}
|
||||
|
||||
let entry = CacheEntry::new(coords, current_time_ms, self.default_ttl_ms);
|
||||
self.entries.insert(addr, entry);
|
||||
}
|
||||
|
||||
/// Insert with a custom TTL.
|
||||
pub fn insert_with_ttl(
|
||||
&mut self,
|
||||
addr: NodeAddr,
|
||||
coords: TreeCoordinate,
|
||||
current_time_ms: u64,
|
||||
ttl_ms: u64,
|
||||
) {
|
||||
if let Some(entry) = self.entries.get_mut(&addr) {
|
||||
entry.update(coords, current_time_ms, ttl_ms);
|
||||
return;
|
||||
}
|
||||
|
||||
if self.entries.len() >= self.max_entries {
|
||||
self.evict_one(current_time_ms);
|
||||
}
|
||||
|
||||
let entry = CacheEntry::new(coords, current_time_ms, ttl_ms);
|
||||
self.entries.insert(addr, entry);
|
||||
}
|
||||
|
||||
/// Look up coordinates for an address (without touching).
|
||||
pub fn get(&self, addr: &NodeAddr, current_time_ms: u64) -> Option<&TreeCoordinate> {
|
||||
self.entries.get(addr).and_then(|entry| {
|
||||
if entry.is_expired(current_time_ms) {
|
||||
None
|
||||
} else {
|
||||
Some(entry.coords())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Look up coordinates and touch (update last_used).
|
||||
pub fn get_and_touch(
|
||||
&mut self,
|
||||
addr: &NodeAddr,
|
||||
current_time_ms: u64,
|
||||
) -> Option<&TreeCoordinate> {
|
||||
// Check and remove if expired
|
||||
if let Some(entry) = self.entries.get(addr)
|
||||
&& entry.is_expired(current_time_ms)
|
||||
{
|
||||
self.entries.remove(addr);
|
||||
return None;
|
||||
}
|
||||
|
||||
// Touch and return
|
||||
if let Some(entry) = self.entries.get_mut(addr) {
|
||||
entry.touch(current_time_ms);
|
||||
Some(entry.coords())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the full cache entry.
|
||||
pub fn get_entry(&self, addr: &NodeAddr) -> Option<&CacheEntry> {
|
||||
self.entries.get(addr)
|
||||
}
|
||||
|
||||
/// Remove an entry.
|
||||
pub fn remove(&mut self, addr: &NodeAddr) -> Option<CacheEntry> {
|
||||
self.entries.remove(addr)
|
||||
}
|
||||
|
||||
/// Check if an address is cached (and not expired).
|
||||
pub fn contains(&self, addr: &NodeAddr, current_time_ms: u64) -> bool {
|
||||
self.get(addr, current_time_ms).is_some()
|
||||
}
|
||||
|
||||
/// Number of entries (including expired).
|
||||
pub fn len(&self) -> usize {
|
||||
self.entries.len()
|
||||
}
|
||||
|
||||
/// Check if empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.entries.is_empty()
|
||||
}
|
||||
|
||||
/// Remove all expired entries.
|
||||
pub fn purge_expired(&mut self, current_time_ms: u64) -> usize {
|
||||
let before = self.entries.len();
|
||||
self.entries
|
||||
.retain(|_, entry| !entry.is_expired(current_time_ms));
|
||||
before - self.entries.len()
|
||||
}
|
||||
|
||||
/// Clear all entries.
|
||||
pub fn clear(&mut self) {
|
||||
self.entries.clear();
|
||||
}
|
||||
|
||||
/// Evict one entry (expired first, then LRU).
|
||||
fn evict_one(&mut self, current_time_ms: u64) {
|
||||
// First try to evict an expired entry
|
||||
let expired_key = self
|
||||
.entries
|
||||
.iter()
|
||||
.find(|(_, e)| e.is_expired(current_time_ms))
|
||||
.map(|(k, _)| *k);
|
||||
|
||||
if let Some(key) = expired_key {
|
||||
self.entries.remove(&key);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise evict LRU (oldest last_used)
|
||||
let lru_key = self
|
||||
.entries
|
||||
.iter()
|
||||
.max_by_key(|(_, e)| e.idle_time(current_time_ms))
|
||||
.map(|(k, _)| *k);
|
||||
|
||||
if let Some(key) = lru_key {
|
||||
self.entries.remove(&key);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get cache statistics.
|
||||
pub fn stats(&self, current_time_ms: u64) -> CacheStats {
|
||||
let mut expired = 0;
|
||||
let mut total_age = 0u64;
|
||||
|
||||
for entry in self.entries.values() {
|
||||
if entry.is_expired(current_time_ms) {
|
||||
expired += 1;
|
||||
}
|
||||
total_age += entry.age(current_time_ms);
|
||||
}
|
||||
|
||||
CacheStats {
|
||||
entries: self.entries.len(),
|
||||
max_entries: self.max_entries,
|
||||
expired,
|
||||
avg_age_ms: if self.entries.is_empty() {
|
||||
0
|
||||
} else {
|
||||
total_age / self.entries.len() as u64
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CoordCache {
|
||||
fn default() -> Self {
|
||||
Self::with_defaults()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_node_addr(val: u8) -> NodeAddr {
|
||||
let mut bytes = [0u8; 16];
|
||||
bytes[0] = val;
|
||||
NodeAddr::from_bytes(bytes)
|
||||
}
|
||||
|
||||
fn make_coords(ids: &[u8]) -> TreeCoordinate {
|
||||
TreeCoordinate::from_addrs(ids.iter().map(|&v| make_node_addr(v)).collect()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coord_cache_basic() {
|
||||
let mut cache = CoordCache::new(100, 1000);
|
||||
let addr = make_node_addr(1);
|
||||
let coords = make_coords(&[1, 0]);
|
||||
|
||||
cache.insert(addr, coords.clone(), 0);
|
||||
|
||||
assert!(cache.contains(&addr, 0));
|
||||
assert_eq!(cache.get(&addr, 0), Some(&coords));
|
||||
assert_eq!(cache.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coord_cache_expiry() {
|
||||
let mut cache = CoordCache::new(100, 1000);
|
||||
let addr = make_node_addr(1);
|
||||
let coords = make_coords(&[1, 0]);
|
||||
|
||||
cache.insert(addr, coords, 0);
|
||||
|
||||
assert!(cache.contains(&addr, 500));
|
||||
assert!(!cache.contains(&addr, 1500));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coord_cache_update() {
|
||||
let mut cache = CoordCache::new(100, 1000);
|
||||
let addr = make_node_addr(1);
|
||||
|
||||
cache.insert(addr, make_coords(&[1, 0]), 0);
|
||||
cache.insert(addr, make_coords(&[1, 2, 0]), 500);
|
||||
|
||||
assert_eq!(cache.len(), 1);
|
||||
let coords = cache.get(&addr, 500).unwrap();
|
||||
assert_eq!(coords.depth(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coord_cache_eviction() {
|
||||
let mut cache = CoordCache::new(2, 10000);
|
||||
|
||||
let addr1 = make_node_addr(1);
|
||||
let addr2 = make_node_addr(2);
|
||||
let addr3 = make_node_addr(3);
|
||||
|
||||
cache.insert(addr1, make_coords(&[1, 0]), 0);
|
||||
cache.insert(addr2, make_coords(&[2, 0]), 100);
|
||||
|
||||
// Touch addr2 to make it more recent
|
||||
let _ = cache.get_and_touch(&addr2, 200);
|
||||
|
||||
// Insert addr3, should evict addr1 (LRU)
|
||||
cache.insert(addr3, make_coords(&[3, 0]), 300);
|
||||
|
||||
assert!(!cache.contains(&addr1, 300));
|
||||
assert!(cache.contains(&addr2, 300));
|
||||
assert!(cache.contains(&addr3, 300));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coord_cache_evict_expired_first() {
|
||||
let mut cache = CoordCache::new(2, 100);
|
||||
|
||||
cache.insert(make_node_addr(1), make_coords(&[1, 0]), 0);
|
||||
cache.insert(make_node_addr(2), make_coords(&[2, 0]), 50);
|
||||
|
||||
// At time 150, addr1 is expired, addr2 is not
|
||||
cache.insert(make_node_addr(3), make_coords(&[3, 0]), 150);
|
||||
|
||||
// addr1 should be evicted (expired), not addr2 (LRU but not expired)
|
||||
assert!(!cache.contains(&make_node_addr(1), 150));
|
||||
assert!(cache.contains(&make_node_addr(2), 150));
|
||||
assert!(cache.contains(&make_node_addr(3), 150));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coord_cache_purge_expired() {
|
||||
let mut cache = CoordCache::new(100, 100);
|
||||
|
||||
cache.insert(make_node_addr(1), make_coords(&[1, 0]), 0); // expires at 100
|
||||
cache.insert(make_node_addr(2), make_coords(&[2, 0]), 50); // expires at 150
|
||||
cache.insert(make_node_addr(3), make_coords(&[3, 0]), 200); // expires at 300
|
||||
|
||||
assert_eq!(cache.len(), 3);
|
||||
|
||||
let purged = cache.purge_expired(151); // both addr1 and addr2 expired
|
||||
|
||||
// Entry 1 and 2 expired, entry 3 still valid
|
||||
assert_eq!(purged, 2);
|
||||
assert_eq!(cache.len(), 1);
|
||||
assert!(cache.contains(&make_node_addr(3), 151));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coord_cache_stats() {
|
||||
let mut cache = CoordCache::new(100, 100);
|
||||
|
||||
cache.insert(make_node_addr(1), make_coords(&[1, 0]), 0);
|
||||
cache.insert(make_node_addr(2), make_coords(&[2, 0]), 50);
|
||||
|
||||
let stats = cache.stats(150);
|
||||
|
||||
assert_eq!(stats.entries, 2);
|
||||
assert_eq!(stats.max_entries, 100);
|
||||
assert_eq!(stats.expired, 1); // addr1 expired
|
||||
assert!(stats.avg_age_ms > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coord_cache_insert_with_ttl() {
|
||||
let mut cache = CoordCache::new(100, 1000);
|
||||
let addr = make_node_addr(1);
|
||||
|
||||
cache.insert_with_ttl(addr, make_coords(&[1, 0]), 0, 200);
|
||||
|
||||
// Should expire at 200, not the default 1000
|
||||
assert!(cache.contains(&addr, 100));
|
||||
assert!(!cache.contains(&addr, 201));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coord_cache_insert_with_ttl_update() {
|
||||
let mut cache = CoordCache::new(100, 1000);
|
||||
let addr = make_node_addr(1);
|
||||
|
||||
cache.insert_with_ttl(addr, make_coords(&[1, 0]), 0, 200);
|
||||
cache.insert_with_ttl(addr, make_coords(&[1, 2, 0]), 100, 300);
|
||||
|
||||
assert_eq!(cache.len(), 1);
|
||||
let coords = cache.get(&addr, 100).unwrap();
|
||||
assert_eq!(coords.depth(), 2);
|
||||
// New TTL: 100 + 300 = 400
|
||||
assert!(cache.contains(&addr, 399));
|
||||
assert!(!cache.contains(&addr, 401));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coord_cache_get_and_touch_removes_expired() {
|
||||
let mut cache = CoordCache::new(100, 100);
|
||||
let addr = make_node_addr(1);
|
||||
|
||||
cache.insert(addr, make_coords(&[1, 0]), 0);
|
||||
assert_eq!(cache.len(), 1);
|
||||
|
||||
// Entry expired at time 200
|
||||
let result = cache.get_and_touch(&addr, 200);
|
||||
assert!(result.is_none());
|
||||
// Entry should be removed from the map
|
||||
assert_eq!(cache.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coord_cache_get_entry() {
|
||||
let mut cache = CoordCache::new(100, 1000);
|
||||
let addr = make_node_addr(1);
|
||||
|
||||
cache.insert(addr, make_coords(&[1, 0]), 500);
|
||||
|
||||
let entry = cache.get_entry(&addr).unwrap();
|
||||
assert_eq!(entry.created_at(), 500);
|
||||
assert_eq!(entry.expires_at(), 1500);
|
||||
|
||||
assert!(cache.get_entry(&make_node_addr(99)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coord_cache_remove() {
|
||||
let mut cache = CoordCache::new(100, 1000);
|
||||
let addr = make_node_addr(1);
|
||||
|
||||
cache.insert(addr, make_coords(&[1, 0]), 0);
|
||||
assert_eq!(cache.len(), 1);
|
||||
|
||||
let removed = cache.remove(&addr);
|
||||
assert!(removed.is_some());
|
||||
assert_eq!(cache.len(), 0);
|
||||
|
||||
// Removing again returns None
|
||||
assert!(cache.remove(&addr).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coord_cache_clear_and_is_empty() {
|
||||
let mut cache = CoordCache::new(100, 1000);
|
||||
|
||||
assert!(cache.is_empty());
|
||||
|
||||
cache.insert(make_node_addr(1), make_coords(&[1, 0]), 0);
|
||||
cache.insert(make_node_addr(2), make_coords(&[2, 0]), 0);
|
||||
|
||||
assert!(!cache.is_empty());
|
||||
|
||||
cache.clear();
|
||||
assert!(cache.is_empty());
|
||||
assert_eq!(cache.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coord_cache_default() {
|
||||
let cache = CoordCache::default();
|
||||
|
||||
assert_eq!(cache.max_entries(), DEFAULT_COORD_CACHE_SIZE);
|
||||
assert_eq!(cache.default_ttl_ms(), DEFAULT_COORD_CACHE_TTL_MS);
|
||||
assert!(cache.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coord_cache_set_default_ttl() {
|
||||
let mut cache = CoordCache::new(100, 1000);
|
||||
let addr = make_node_addr(1);
|
||||
|
||||
cache.set_default_ttl_ms(200);
|
||||
assert_eq!(cache.default_ttl_ms(), 200);
|
||||
|
||||
cache.insert(addr, make_coords(&[1, 0]), 0);
|
||||
// New TTL applies: expires at 200
|
||||
assert!(cache.contains(&addr, 100));
|
||||
assert!(!cache.contains(&addr, 201));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coord_cache_stats_empty() {
|
||||
let cache = CoordCache::new(100, 1000);
|
||||
let stats = cache.stats(0);
|
||||
|
||||
assert_eq!(stats.entries, 0);
|
||||
assert_eq!(stats.max_entries, 100);
|
||||
assert_eq!(stats.expired, 0);
|
||||
assert_eq!(stats.avg_age_ms, 0);
|
||||
}
|
||||
}
|
||||
Vendored
+176
@@ -0,0 +1,176 @@
|
||||
//! Cache entry with TTL and LRU tracking.
|
||||
|
||||
use crate::tree::TreeCoordinate;
|
||||
|
||||
/// A cached coordinate entry.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CacheEntry {
|
||||
/// The cached coordinates.
|
||||
coords: TreeCoordinate,
|
||||
/// When this entry was created (Unix milliseconds).
|
||||
created_at: u64,
|
||||
/// When this entry was last used (Unix milliseconds).
|
||||
last_used: u64,
|
||||
/// When this entry expires (Unix milliseconds).
|
||||
expires_at: u64,
|
||||
}
|
||||
|
||||
impl CacheEntry {
|
||||
/// Create a new cache entry.
|
||||
pub fn new(coords: TreeCoordinate, current_time_ms: u64, ttl_ms: u64) -> Self {
|
||||
Self {
|
||||
coords,
|
||||
created_at: current_time_ms,
|
||||
last_used: current_time_ms,
|
||||
expires_at: current_time_ms.saturating_add(ttl_ms),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the cached coordinates.
|
||||
pub fn coords(&self) -> &TreeCoordinate {
|
||||
&self.coords
|
||||
}
|
||||
|
||||
/// Get the creation timestamp.
|
||||
pub fn created_at(&self) -> u64 {
|
||||
self.created_at
|
||||
}
|
||||
|
||||
/// Get the last used timestamp.
|
||||
pub fn last_used(&self) -> u64 {
|
||||
self.last_used
|
||||
}
|
||||
|
||||
/// Get the expiry timestamp.
|
||||
pub fn expires_at(&self) -> u64 {
|
||||
self.expires_at
|
||||
}
|
||||
|
||||
/// Check if this entry has expired.
|
||||
pub fn is_expired(&self, current_time_ms: u64) -> bool {
|
||||
current_time_ms > self.expires_at
|
||||
}
|
||||
|
||||
/// Touch the entry to update last_used time.
|
||||
pub fn touch(&mut self, current_time_ms: u64) {
|
||||
self.last_used = current_time_ms;
|
||||
}
|
||||
|
||||
/// Refresh the expiry time.
|
||||
pub fn refresh(&mut self, current_time_ms: u64, ttl_ms: u64) {
|
||||
self.expires_at = current_time_ms.saturating_add(ttl_ms);
|
||||
self.last_used = current_time_ms;
|
||||
}
|
||||
|
||||
/// Update the coordinates and refresh timestamps.
|
||||
pub fn update(&mut self, coords: TreeCoordinate, current_time_ms: u64, ttl_ms: u64) {
|
||||
self.coords = coords;
|
||||
self.last_used = current_time_ms;
|
||||
self.expires_at = current_time_ms.saturating_add(ttl_ms);
|
||||
}
|
||||
|
||||
/// Time since last use (for LRU eviction).
|
||||
pub fn idle_time(&self, current_time_ms: u64) -> u64 {
|
||||
current_time_ms.saturating_sub(self.last_used)
|
||||
}
|
||||
|
||||
/// Age of the entry.
|
||||
pub fn age(&self, current_time_ms: u64) -> u64 {
|
||||
current_time_ms.saturating_sub(self.created_at)
|
||||
}
|
||||
|
||||
/// Time until expiry (0 if already expired).
|
||||
pub fn time_to_expiry(&self, current_time_ms: u64) -> u64 {
|
||||
self.expires_at.saturating_sub(current_time_ms)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::NodeAddr;
|
||||
|
||||
fn make_node_addr(val: u8) -> NodeAddr {
|
||||
let mut bytes = [0u8; 16];
|
||||
bytes[0] = val;
|
||||
NodeAddr::from_bytes(bytes)
|
||||
}
|
||||
|
||||
fn make_coords(ids: &[u8]) -> TreeCoordinate {
|
||||
TreeCoordinate::from_addrs(ids.iter().map(|&v| make_node_addr(v)).collect()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_entry_expiry() {
|
||||
let coords = make_coords(&[1, 0]);
|
||||
let entry = CacheEntry::new(coords, 1000, 500);
|
||||
|
||||
assert!(!entry.is_expired(1000));
|
||||
assert!(!entry.is_expired(1500)); // expires_at = 1500, not yet expired
|
||||
assert!(entry.is_expired(1501)); // one ms after expiry
|
||||
assert!(entry.is_expired(2000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_entry_refresh() {
|
||||
let coords = make_coords(&[1, 0]);
|
||||
let mut entry = CacheEntry::new(coords, 1000, 500);
|
||||
|
||||
assert!(entry.is_expired(1501)); // expires_at = 1500
|
||||
|
||||
entry.refresh(1400, 500); // new expires_at = 1900
|
||||
|
||||
assert!(!entry.is_expired(1600));
|
||||
assert!(!entry.is_expired(1900)); // at exactly expiry, not expired
|
||||
assert!(entry.is_expired(1901)); // one ms after expiry
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_entry_times() {
|
||||
let coords = make_coords(&[1, 0]);
|
||||
let entry = CacheEntry::new(coords, 1000, 500);
|
||||
|
||||
assert_eq!(entry.created_at(), 1000);
|
||||
assert_eq!(entry.last_used(), 1000);
|
||||
assert_eq!(entry.expires_at(), 1500);
|
||||
assert_eq!(entry.age(1200), 200);
|
||||
assert_eq!(entry.idle_time(1200), 200);
|
||||
assert_eq!(entry.time_to_expiry(1200), 300);
|
||||
assert_eq!(entry.time_to_expiry(1600), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_entry_touch() {
|
||||
let coords = make_coords(&[1, 0]);
|
||||
let mut entry = CacheEntry::new(coords, 1000, 500);
|
||||
|
||||
assert_eq!(entry.last_used(), 1000);
|
||||
entry.touch(1300);
|
||||
assert_eq!(entry.last_used(), 1300);
|
||||
// Touch doesn't affect expiry
|
||||
assert_eq!(entry.expires_at(), 1500);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_entry_update() {
|
||||
let mut entry = CacheEntry::new(make_coords(&[1, 0]), 1000, 500);
|
||||
|
||||
let new_coords = make_coords(&[1, 2, 0]);
|
||||
entry.update(new_coords.clone(), 2000, 600);
|
||||
|
||||
assert_eq!(entry.coords(), &new_coords);
|
||||
assert_eq!(entry.last_used(), 2000);
|
||||
assert_eq!(entry.expires_at(), 2600);
|
||||
// created_at is unchanged
|
||||
assert_eq!(entry.created_at(), 1000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_entry_saturating_add() {
|
||||
let coords = make_coords(&[1, 0]);
|
||||
let entry = CacheEntry::new(coords, u64::MAX - 10, 100);
|
||||
|
||||
// Should saturate to u64::MAX rather than overflow
|
||||
assert_eq!(entry.expires_at(), u64::MAX);
|
||||
}
|
||||
}
|
||||
Vendored
+102
@@ -0,0 +1,102 @@
|
||||
//! Caching Entities
|
||||
//!
|
||||
//! Coordinate and route caching for FIPS routing. The CoordCache stores
|
||||
//! address-to-coordinate mappings populated by session setup, while
|
||||
//! RouteCache stores coordinates learned from discovery queries.
|
||||
|
||||
mod coord_cache;
|
||||
mod entry;
|
||||
mod route_cache;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
pub use coord_cache::{CoordCache, DEFAULT_COORD_CACHE_SIZE, DEFAULT_COORD_CACHE_TTL_MS};
|
||||
pub use entry::CacheEntry;
|
||||
pub use route_cache::{CachedCoords, RouteCache, DEFAULT_ROUTE_CACHE_SIZE};
|
||||
|
||||
/// Errors related to cache operations.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum CacheError {
|
||||
#[error("cache full: max {max} entries")]
|
||||
CacheFull { max: usize },
|
||||
|
||||
#[error("entry not found")]
|
||||
NotFound,
|
||||
|
||||
#[error("entry expired")]
|
||||
Expired,
|
||||
}
|
||||
|
||||
/// Cache statistics.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CacheStats {
|
||||
/// Current number of entries.
|
||||
pub entries: usize,
|
||||
/// Maximum capacity.
|
||||
pub max_entries: usize,
|
||||
/// Number of expired entries.
|
||||
pub expired: usize,
|
||||
/// Average entry age in milliseconds.
|
||||
pub avg_age_ms: u64,
|
||||
}
|
||||
|
||||
impl CacheStats {
|
||||
/// Fill ratio (entries / max_entries).
|
||||
pub fn fill_ratio(&self) -> f64 {
|
||||
if self.max_entries == 0 {
|
||||
0.0
|
||||
} else {
|
||||
self.entries as f64 / self.max_entries as f64
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_cache_stats_fill_ratio() {
|
||||
let stats = CacheStats {
|
||||
entries: 50,
|
||||
max_entries: 100,
|
||||
expired: 0,
|
||||
avg_age_ms: 0,
|
||||
};
|
||||
assert!((stats.fill_ratio() - 0.5).abs() < f64::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_stats_fill_ratio_zero_max() {
|
||||
let stats = CacheStats {
|
||||
entries: 0,
|
||||
max_entries: 0,
|
||||
expired: 0,
|
||||
avg_age_ms: 0,
|
||||
};
|
||||
assert!((stats.fill_ratio() - 0.0).abs() < f64::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_stats_fill_ratio_full() {
|
||||
let stats = CacheStats {
|
||||
entries: 100,
|
||||
max_entries: 100,
|
||||
expired: 10,
|
||||
avg_age_ms: 500,
|
||||
};
|
||||
assert!((stats.fill_ratio() - 1.0).abs() < f64::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_error_display() {
|
||||
let full = CacheError::CacheFull { max: 1000 };
|
||||
assert_eq!(full.to_string(), "cache full: max 1000 entries");
|
||||
|
||||
let not_found = CacheError::NotFound;
|
||||
assert_eq!(not_found.to_string(), "entry not found");
|
||||
|
||||
let expired = CacheError::Expired;
|
||||
assert_eq!(expired.to_string(), "entry expired");
|
||||
}
|
||||
}
|
||||
Vendored
+364
@@ -0,0 +1,364 @@
|
||||
//! Route cache for discovered destinations.
|
||||
//!
|
||||
//! Separate from CoordCache, this stores routes learned from the discovery
|
||||
//! protocol (LookupRequest/LookupResponse) rather than session establishment.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::tree::TreeCoordinate;
|
||||
use crate::NodeAddr;
|
||||
|
||||
/// Default maximum entries in route cache.
|
||||
pub const DEFAULT_ROUTE_CACHE_SIZE: usize = 10_000;
|
||||
|
||||
/// A cached route from discovery.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CachedCoords {
|
||||
/// The coordinates discovered.
|
||||
coords: TreeCoordinate,
|
||||
/// When this was discovered (Unix milliseconds).
|
||||
discovered_at: u64,
|
||||
/// Last time we used this route (Unix milliseconds).
|
||||
last_used: u64,
|
||||
}
|
||||
|
||||
impl CachedCoords {
|
||||
/// Create a new cached route.
|
||||
pub fn new(coords: TreeCoordinate, discovered_at: u64) -> Self {
|
||||
Self {
|
||||
coords,
|
||||
discovered_at,
|
||||
last_used: discovered_at,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the coordinates.
|
||||
pub fn coords(&self) -> &TreeCoordinate {
|
||||
&self.coords
|
||||
}
|
||||
|
||||
/// Get the discovery timestamp.
|
||||
pub fn discovered_at(&self) -> u64 {
|
||||
self.discovered_at
|
||||
}
|
||||
|
||||
/// Get the last used timestamp.
|
||||
pub fn last_used(&self) -> u64 {
|
||||
self.last_used
|
||||
}
|
||||
|
||||
/// Touch (update last_used).
|
||||
pub fn touch(&mut self, current_time_ms: u64) {
|
||||
self.last_used = current_time_ms;
|
||||
}
|
||||
|
||||
/// Age since discovery.
|
||||
pub fn age(&self, current_time_ms: u64) -> u64 {
|
||||
current_time_ms.saturating_sub(self.discovered_at)
|
||||
}
|
||||
|
||||
/// Idle time since last use.
|
||||
pub fn idle_time(&self, current_time_ms: u64) -> u64 {
|
||||
current_time_ms.saturating_sub(self.last_used)
|
||||
}
|
||||
|
||||
/// Update coordinates (re-discovered).
|
||||
pub fn update(&mut self, coords: TreeCoordinate, current_time_ms: u64) {
|
||||
self.coords = coords;
|
||||
self.discovered_at = current_time_ms;
|
||||
self.last_used = current_time_ms;
|
||||
}
|
||||
}
|
||||
|
||||
/// Route cache for discovered destinations.
|
||||
///
|
||||
/// Separate from CoordCache, this stores routes learned from the discovery
|
||||
/// protocol (LookupRequest/LookupResponse) rather than session establishment.
|
||||
/// Keyed by NodeAddr.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RouteCache {
|
||||
/// NodeAddr -> discovered coordinates.
|
||||
entries: HashMap<NodeAddr, CachedCoords>,
|
||||
/// Maximum entries.
|
||||
max_entries: usize,
|
||||
}
|
||||
|
||||
impl RouteCache {
|
||||
/// Create a new route cache.
|
||||
pub fn new(max_entries: usize) -> Self {
|
||||
Self {
|
||||
entries: HashMap::with_capacity(max_entries.min(1000)),
|
||||
max_entries,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create with default capacity.
|
||||
pub fn with_defaults() -> Self {
|
||||
Self::new(DEFAULT_ROUTE_CACHE_SIZE)
|
||||
}
|
||||
|
||||
/// Get the maximum capacity.
|
||||
pub fn max_entries(&self) -> usize {
|
||||
self.max_entries
|
||||
}
|
||||
|
||||
/// Insert a discovered route.
|
||||
pub fn insert(&mut self, node_addr: NodeAddr, coords: TreeCoordinate, current_time_ms: u64) {
|
||||
// Update existing
|
||||
if let Some(entry) = self.entries.get_mut(&node_addr) {
|
||||
entry.update(coords, current_time_ms);
|
||||
return;
|
||||
}
|
||||
|
||||
// Evict if full
|
||||
if self.entries.len() >= self.max_entries {
|
||||
self.evict_lru(current_time_ms);
|
||||
}
|
||||
|
||||
self.entries
|
||||
.insert(node_addr, CachedCoords::new(coords, current_time_ms));
|
||||
}
|
||||
|
||||
/// Look up a route (without touching).
|
||||
pub fn get(&self, node_addr: &NodeAddr) -> Option<&CachedCoords> {
|
||||
self.entries.get(node_addr)
|
||||
}
|
||||
|
||||
/// Look up and touch.
|
||||
pub fn get_and_touch(
|
||||
&mut self,
|
||||
node_addr: &NodeAddr,
|
||||
current_time_ms: u64,
|
||||
) -> Option<&TreeCoordinate> {
|
||||
if let Some(entry) = self.entries.get_mut(node_addr) {
|
||||
entry.touch(current_time_ms);
|
||||
Some(entry.coords())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a route (e.g., after route failure).
|
||||
pub fn invalidate(&mut self, node_addr: &NodeAddr) -> Option<CachedCoords> {
|
||||
self.entries.remove(node_addr)
|
||||
}
|
||||
|
||||
/// Check if a node is cached.
|
||||
pub fn contains(&self, node_addr: &NodeAddr) -> bool {
|
||||
self.entries.contains_key(node_addr)
|
||||
}
|
||||
|
||||
/// Number of cached routes.
|
||||
pub fn len(&self) -> usize {
|
||||
self.entries.len()
|
||||
}
|
||||
|
||||
/// Check if empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.entries.is_empty()
|
||||
}
|
||||
|
||||
/// Clear all routes.
|
||||
pub fn clear(&mut self) {
|
||||
self.entries.clear();
|
||||
}
|
||||
|
||||
/// Evict routes older than a threshold.
|
||||
pub fn evict_older_than(&mut self, max_age_ms: u64, current_time_ms: u64) -> usize {
|
||||
let before = self.entries.len();
|
||||
self.entries
|
||||
.retain(|_, entry| entry.age(current_time_ms) < max_age_ms);
|
||||
before - self.entries.len()
|
||||
}
|
||||
|
||||
fn evict_lru(&mut self, current_time_ms: u64) {
|
||||
let lru_id = self
|
||||
.entries
|
||||
.iter()
|
||||
.max_by_key(|(_, e)| e.idle_time(current_time_ms))
|
||||
.map(|(k, _)| *k);
|
||||
|
||||
if let Some(id) = lru_id {
|
||||
self.entries.remove(&id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RouteCache {
|
||||
fn default() -> Self {
|
||||
Self::with_defaults()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_node_addr(val: u8) -> NodeAddr {
|
||||
let mut bytes = [0u8; 16];
|
||||
bytes[0] = val;
|
||||
NodeAddr::from_bytes(bytes)
|
||||
}
|
||||
|
||||
fn make_coords(ids: &[u8]) -> TreeCoordinate {
|
||||
TreeCoordinate::from_addrs(ids.iter().map(|&v| make_node_addr(v)).collect()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cached_coords() {
|
||||
let coords = make_coords(&[1, 0]);
|
||||
let mut cached = CachedCoords::new(coords.clone(), 1000);
|
||||
|
||||
assert_eq!(cached.coords(), &coords);
|
||||
assert_eq!(cached.discovered_at(), 1000);
|
||||
assert_eq!(cached.last_used(), 1000);
|
||||
|
||||
cached.touch(1500);
|
||||
assert_eq!(cached.last_used(), 1500);
|
||||
assert_eq!(cached.idle_time(1600), 100);
|
||||
assert_eq!(cached.age(1600), 600);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_route_cache_basic() {
|
||||
let mut cache = RouteCache::new(100);
|
||||
let node = make_node_addr(1);
|
||||
let coords = make_coords(&[1, 0]);
|
||||
|
||||
cache.insert(node, coords.clone(), 0);
|
||||
|
||||
assert!(cache.contains(&node));
|
||||
assert_eq!(cache.get(&node).unwrap().coords(), &coords);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_route_cache_invalidate() {
|
||||
let mut cache = RouteCache::new(100);
|
||||
let node = make_node_addr(1);
|
||||
let coords = make_coords(&[1, 0]);
|
||||
|
||||
cache.insert(node, coords, 0);
|
||||
assert!(cache.contains(&node));
|
||||
|
||||
cache.invalidate(&node);
|
||||
assert!(!cache.contains(&node));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_route_cache_lru_eviction() {
|
||||
let mut cache = RouteCache::new(2);
|
||||
|
||||
let node1 = make_node_addr(1);
|
||||
let node2 = make_node_addr(2);
|
||||
let node3 = make_node_addr(3);
|
||||
|
||||
cache.insert(node1, make_coords(&[1, 0]), 0);
|
||||
cache.insert(node2, make_coords(&[2, 0]), 100);
|
||||
|
||||
// Touch node2
|
||||
let _ = cache.get_and_touch(&node2, 200);
|
||||
|
||||
// Insert node3
|
||||
cache.insert(node3, make_coords(&[3, 0]), 300);
|
||||
|
||||
// node1 should be evicted
|
||||
assert!(!cache.contains(&node1));
|
||||
assert!(cache.contains(&node2));
|
||||
assert!(cache.contains(&node3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_route_cache_evict_older_than() {
|
||||
let mut cache = RouteCache::new(100);
|
||||
|
||||
cache.insert(make_node_addr(1), make_coords(&[1, 0]), 0);
|
||||
cache.insert(make_node_addr(2), make_coords(&[2, 0]), 500);
|
||||
cache.insert(make_node_addr(3), make_coords(&[3, 0]), 1000);
|
||||
|
||||
let evicted = cache.evict_older_than(600, 1000);
|
||||
|
||||
assert_eq!(evicted, 1); // node1 is > 600ms old
|
||||
assert_eq!(cache.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_route_cache_update() {
|
||||
let mut cache = RouteCache::new(100);
|
||||
let node = make_node_addr(1);
|
||||
|
||||
cache.insert(node, make_coords(&[1, 0]), 0);
|
||||
cache.insert(node, make_coords(&[1, 2, 0]), 500);
|
||||
|
||||
assert_eq!(cache.len(), 1);
|
||||
let cached = cache.get(&node).unwrap();
|
||||
assert_eq!(cached.coords().depth(), 2);
|
||||
assert_eq!(cached.discovered_at(), 500);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cached_coords_update() {
|
||||
let mut cached = CachedCoords::new(make_coords(&[1, 0]), 1000);
|
||||
|
||||
let new_coords = make_coords(&[1, 2, 0]);
|
||||
cached.update(new_coords.clone(), 2000);
|
||||
|
||||
assert_eq!(cached.coords(), &new_coords);
|
||||
assert_eq!(cached.discovered_at(), 2000);
|
||||
assert_eq!(cached.last_used(), 2000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_route_cache_get_and_touch() {
|
||||
let mut cache = RouteCache::new(100);
|
||||
let node = make_node_addr(1);
|
||||
let coords = make_coords(&[1, 0]);
|
||||
|
||||
cache.insert(node, coords.clone(), 0);
|
||||
|
||||
let result = cache.get_and_touch(&node, 500);
|
||||
assert_eq!(result, Some(&coords));
|
||||
|
||||
// Verify last_used was updated
|
||||
let entry = cache.get(&node).unwrap();
|
||||
assert_eq!(entry.last_used(), 500);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_route_cache_get_and_touch_missing() {
|
||||
let mut cache = RouteCache::new(100);
|
||||
let result = cache.get_and_touch(&make_node_addr(99), 0);
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_route_cache_clear_and_is_empty() {
|
||||
let mut cache = RouteCache::new(100);
|
||||
|
||||
assert!(cache.is_empty());
|
||||
|
||||
cache.insert(make_node_addr(1), make_coords(&[1, 0]), 0);
|
||||
cache.insert(make_node_addr(2), make_coords(&[2, 0]), 0);
|
||||
|
||||
assert!(!cache.is_empty());
|
||||
assert_eq!(cache.len(), 2);
|
||||
|
||||
cache.clear();
|
||||
assert!(cache.is_empty());
|
||||
assert_eq!(cache.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_route_cache_default() {
|
||||
let cache = RouteCache::default();
|
||||
|
||||
assert_eq!(cache.max_entries(), DEFAULT_ROUTE_CACHE_SIZE);
|
||||
assert!(cache.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_route_cache_invalidate_missing() {
|
||||
let mut cache = RouteCache::new(100);
|
||||
let result = cache.invalidate(&make_node_addr(99));
|
||||
assert!(result.is_none());
|
||||
}
|
||||
}
|
||||
-1318
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,620 @@
|
||||
//! FIPS Configuration System
|
||||
//!
|
||||
//! Loads configuration from YAML files with a cascading priority system:
|
||||
//! 1. `./fips.yaml` (current directory - highest priority)
|
||||
//! 2. `~/.config/fips/fips.yaml` (user config directory)
|
||||
//! 3. `/etc/fips/fips.yaml` (system - lowest priority)
|
||||
//!
|
||||
//! Values from higher priority files override those from lower priority files.
|
||||
//!
|
||||
//! # YAML Structure
|
||||
//!
|
||||
//! The YAML structure mirrors the sysctl-style paths in the architecture docs.
|
||||
//! For example, `node.identity.nsec` in the docs corresponds to:
|
||||
//!
|
||||
//! ```yaml
|
||||
//! node:
|
||||
//! identity:
|
||||
//! nsec: "nsec1..."
|
||||
//! ```
|
||||
|
||||
mod node;
|
||||
mod peer;
|
||||
mod transport;
|
||||
|
||||
use crate::upper::config::{DnsConfig, TunConfig};
|
||||
use crate::{Identity, IdentityError};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
use thiserror::Error;
|
||||
|
||||
pub use node::{
|
||||
BloomConfig, BuffersConfig, CacheConfig, DiscoveryConfig, LimitsConfig, NodeConfig,
|
||||
RateLimitConfig, RetryConfig, SessionConfig, TreeConfig,
|
||||
};
|
||||
pub use peer::{ConnectPolicy, PeerAddress, PeerConfig};
|
||||
pub use transport::{TransportInstances, TransportsConfig, UdpConfig};
|
||||
|
||||
/// Default config filename.
|
||||
const CONFIG_FILENAME: &str = "fips.yaml";
|
||||
|
||||
/// Errors that can occur during configuration loading.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ConfigError {
|
||||
#[error("failed to read config file {path}: {source}")]
|
||||
ReadFile {
|
||||
path: PathBuf,
|
||||
source: std::io::Error,
|
||||
},
|
||||
|
||||
#[error("failed to parse config file {path}: {source}")]
|
||||
ParseYaml {
|
||||
path: PathBuf,
|
||||
source: serde_yaml::Error,
|
||||
},
|
||||
|
||||
#[error("identity error: {0}")]
|
||||
Identity(#[from] IdentityError),
|
||||
}
|
||||
|
||||
/// Identity configuration (`node.identity.*`).
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct IdentityConfig {
|
||||
/// Secret key in nsec (bech32) or hex format (`node.identity.nsec`).
|
||||
/// If not specified, a new keypair will be generated.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub nsec: Option<String>,
|
||||
}
|
||||
|
||||
/// Root configuration structure.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
/// Node configuration (`node.*`).
|
||||
#[serde(default)]
|
||||
pub node: NodeConfig,
|
||||
|
||||
/// TUN interface configuration (`tun.*`).
|
||||
#[serde(default)]
|
||||
pub tun: TunConfig,
|
||||
|
||||
/// DNS responder configuration (`dns.*`).
|
||||
#[serde(default)]
|
||||
pub dns: DnsConfig,
|
||||
|
||||
/// Transport instances (`transports.*`).
|
||||
#[serde(default, skip_serializing_if = "TransportsConfig::is_empty")]
|
||||
pub transports: TransportsConfig,
|
||||
|
||||
/// Static peers to connect to (`peers`).
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub peers: Vec<PeerConfig>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Create a new empty configuration.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Load configuration from the standard search paths.
|
||||
///
|
||||
/// Files are loaded in reverse priority order and merged:
|
||||
/// 1. `/etc/fips/fips.yaml` (loaded first, lowest priority)
|
||||
/// 2. `~/.config/fips/fips.yaml` (user config)
|
||||
/// 3. `./fips.yaml` (loaded last, highest priority)
|
||||
///
|
||||
/// Returns a tuple of (config, paths_loaded) where paths_loaded contains
|
||||
/// the paths that were successfully loaded.
|
||||
pub fn load() -> Result<(Self, Vec<PathBuf>), ConfigError> {
|
||||
let search_paths = Self::search_paths();
|
||||
Self::load_from_paths(&search_paths)
|
||||
}
|
||||
|
||||
/// Load configuration from specific paths.
|
||||
///
|
||||
/// Paths are processed in order, with later paths overriding earlier ones.
|
||||
pub fn load_from_paths(paths: &[PathBuf]) -> Result<(Self, Vec<PathBuf>), ConfigError> {
|
||||
let mut config = Config::default();
|
||||
let mut loaded_paths = Vec::new();
|
||||
|
||||
for path in paths {
|
||||
if path.exists() {
|
||||
let file_config = Self::load_file(path)?;
|
||||
config.merge(file_config);
|
||||
loaded_paths.push(path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok((config, loaded_paths))
|
||||
}
|
||||
|
||||
/// Load configuration from a single file.
|
||||
pub fn load_file(path: &Path) -> Result<Self, ConfigError> {
|
||||
let contents = std::fs::read_to_string(path).map_err(|e| ConfigError::ReadFile {
|
||||
path: path.to_path_buf(),
|
||||
source: e,
|
||||
})?;
|
||||
|
||||
serde_yaml::from_str(&contents).map_err(|e| ConfigError::ParseYaml {
|
||||
path: path.to_path_buf(),
|
||||
source: e,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the standard search paths in priority order (lowest to highest).
|
||||
pub fn search_paths() -> Vec<PathBuf> {
|
||||
let mut paths = Vec::new();
|
||||
|
||||
// System config (lowest priority)
|
||||
paths.push(PathBuf::from("/etc/fips").join(CONFIG_FILENAME));
|
||||
|
||||
// User config directory
|
||||
if let Some(config_dir) = dirs::config_dir() {
|
||||
paths.push(config_dir.join("fips").join(CONFIG_FILENAME));
|
||||
}
|
||||
|
||||
// Home directory (legacy location)
|
||||
if let Some(home_dir) = dirs::home_dir() {
|
||||
paths.push(home_dir.join(".fips.yaml"));
|
||||
}
|
||||
|
||||
// Current directory (highest priority)
|
||||
paths.push(PathBuf::from(".").join(CONFIG_FILENAME));
|
||||
|
||||
paths
|
||||
}
|
||||
|
||||
/// Merge another configuration into this one.
|
||||
///
|
||||
/// Values from `other` override values in `self` when present.
|
||||
pub fn merge(&mut self, other: Config) {
|
||||
// Merge node.identity section
|
||||
if other.node.identity.nsec.is_some() {
|
||||
self.node.identity.nsec = other.node.identity.nsec;
|
||||
}
|
||||
// Merge node.leaf_only
|
||||
if other.node.leaf_only {
|
||||
self.node.leaf_only = true;
|
||||
}
|
||||
// Merge tun section
|
||||
if other.tun.enabled {
|
||||
self.tun.enabled = true;
|
||||
}
|
||||
if other.tun.name.is_some() {
|
||||
self.tun.name = other.tun.name;
|
||||
}
|
||||
if other.tun.mtu.is_some() {
|
||||
self.tun.mtu = other.tun.mtu;
|
||||
}
|
||||
// Merge dns section
|
||||
if other.dns.enabled {
|
||||
self.dns.enabled = true;
|
||||
}
|
||||
if other.dns.bind_addr.is_some() {
|
||||
self.dns.bind_addr = other.dns.bind_addr;
|
||||
}
|
||||
if other.dns.port.is_some() {
|
||||
self.dns.port = other.dns.port;
|
||||
}
|
||||
if other.dns.ttl.is_some() {
|
||||
self.dns.ttl = other.dns.ttl;
|
||||
}
|
||||
// Merge transports section
|
||||
self.transports.merge(other.transports);
|
||||
// Merge peers (replace if non-empty)
|
||||
if !other.peers.is_empty() {
|
||||
self.peers = other.peers;
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an Identity from this configuration.
|
||||
///
|
||||
/// If an nsec is configured, uses that to create the identity.
|
||||
/// Otherwise, generates a new random identity.
|
||||
pub fn create_identity(&self) -> Result<Identity, ConfigError> {
|
||||
match &self.node.identity.nsec {
|
||||
Some(nsec) => Ok(Identity::from_secret_str(nsec)?),
|
||||
None => Ok(Identity::generate()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if an identity is configured (vs. will be generated).
|
||||
pub fn has_identity(&self) -> bool {
|
||||
self.node.identity.nsec.is_some()
|
||||
}
|
||||
|
||||
/// Check if leaf-only mode is configured.
|
||||
pub fn is_leaf_only(&self) -> bool {
|
||||
self.node.leaf_only
|
||||
}
|
||||
|
||||
/// Get the configured peers.
|
||||
pub fn peers(&self) -> &[PeerConfig] {
|
||||
&self.peers
|
||||
}
|
||||
|
||||
/// Get peers that should auto-connect on startup.
|
||||
pub fn auto_connect_peers(&self) -> impl Iterator<Item = &PeerConfig> {
|
||||
self.peers.iter().filter(|p| p.is_auto_connect())
|
||||
}
|
||||
|
||||
/// Serialize this configuration to YAML.
|
||||
pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
|
||||
serde_yaml::to_string(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_empty_config() {
|
||||
let config = Config::new();
|
||||
assert!(config.node.identity.nsec.is_none());
|
||||
assert!(!config.has_identity());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_yaml_with_nsec() {
|
||||
let yaml = r#"
|
||||
node:
|
||||
identity:
|
||||
nsec: nsec1qyqsqypqxqszqg9qyqsqypqxqszqg9qyqsqypqxqszqg9qyqsqypqxfnm5g9
|
||||
"#;
|
||||
let config: Config = serde_yaml::from_str(yaml).unwrap();
|
||||
assert!(config.node.identity.nsec.is_some());
|
||||
assert!(config.has_identity());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_yaml_with_hex() {
|
||||
let yaml = r#"
|
||||
node:
|
||||
identity:
|
||||
nsec: "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"
|
||||
"#;
|
||||
let config: Config = serde_yaml::from_str(yaml).unwrap();
|
||||
assert!(config.node.identity.nsec.is_some());
|
||||
|
||||
let identity = config.create_identity().unwrap();
|
||||
assert!(!identity.npub().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_yaml_empty() {
|
||||
let yaml = "";
|
||||
let config: Config = serde_yaml::from_str(yaml).unwrap();
|
||||
assert!(config.node.identity.nsec.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_yaml_partial() {
|
||||
let yaml = r#"
|
||||
node:
|
||||
identity: {}
|
||||
"#;
|
||||
let config: Config = serde_yaml::from_str(yaml).unwrap();
|
||||
assert!(config.node.identity.nsec.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_configs() {
|
||||
let mut base = Config::new();
|
||||
base.node.identity.nsec = Some("base_nsec".to_string());
|
||||
|
||||
let mut override_config = Config::new();
|
||||
override_config.node.identity.nsec = Some("override_nsec".to_string());
|
||||
|
||||
base.merge(override_config);
|
||||
assert_eq!(
|
||||
base.node.identity.nsec,
|
||||
Some("override_nsec".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_preserves_base_when_override_empty() {
|
||||
let mut base = Config::new();
|
||||
base.node.identity.nsec = Some("base_nsec".to_string());
|
||||
|
||||
let override_config = Config::new();
|
||||
|
||||
base.merge(override_config);
|
||||
assert_eq!(base.node.identity.nsec, Some("base_nsec".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_identity_from_nsec() {
|
||||
let mut config = Config::new();
|
||||
config.node.identity.nsec = Some(
|
||||
"0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20".to_string(),
|
||||
);
|
||||
|
||||
let identity = config.create_identity().unwrap();
|
||||
assert!(!identity.npub().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_identity_generates_new() {
|
||||
let config = Config::new();
|
||||
let identity = config.create_identity().unwrap();
|
||||
assert!(!identity.npub().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_from_file() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let config_path = temp_dir.path().join("fips.yaml");
|
||||
|
||||
let yaml = r#"
|
||||
node:
|
||||
identity:
|
||||
nsec: "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"
|
||||
"#;
|
||||
fs::write(&config_path, yaml).unwrap();
|
||||
|
||||
let config = Config::load_file(&config_path).unwrap();
|
||||
assert!(config.node.identity.nsec.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_from_paths_merges() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
|
||||
// Create two config files
|
||||
let low_priority = temp_dir.path().join("low.yaml");
|
||||
let high_priority = temp_dir.path().join("high.yaml");
|
||||
|
||||
fs::write(
|
||||
&low_priority,
|
||||
r#"
|
||||
node:
|
||||
identity:
|
||||
nsec: "low_priority_nsec"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
fs::write(
|
||||
&high_priority,
|
||||
r#"
|
||||
node:
|
||||
identity:
|
||||
nsec: "high_priority_nsec"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let paths = vec![low_priority.clone(), high_priority.clone()];
|
||||
let (config, loaded) = Config::load_from_paths(&paths).unwrap();
|
||||
|
||||
assert_eq!(loaded.len(), 2);
|
||||
assert_eq!(
|
||||
config.node.identity.nsec,
|
||||
Some("high_priority_nsec".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_skips_missing_files() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let existing = temp_dir.path().join("exists.yaml");
|
||||
let missing = temp_dir.path().join("missing.yaml");
|
||||
|
||||
fs::write(
|
||||
&existing,
|
||||
r#"
|
||||
node:
|
||||
identity:
|
||||
nsec: "existing_nsec"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let paths = vec![missing, existing.clone()];
|
||||
let (config, loaded) = Config::load_from_paths(&paths).unwrap();
|
||||
|
||||
assert_eq!(loaded.len(), 1);
|
||||
assert_eq!(loaded[0], existing);
|
||||
assert_eq!(config.node.identity.nsec, Some("existing_nsec".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_paths_includes_expected() {
|
||||
let paths = Config::search_paths();
|
||||
|
||||
// Should include current directory
|
||||
assert!(paths.iter().any(|p| p.ends_with("fips.yaml")));
|
||||
|
||||
// Should include /etc/fips
|
||||
assert!(paths
|
||||
.iter()
|
||||
.any(|p| p.starts_with("/etc/fips") && p.ends_with("fips.yaml")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_yaml() {
|
||||
let mut config = Config::new();
|
||||
config.node.identity.nsec = Some("test_nsec".to_string());
|
||||
|
||||
let yaml = config.to_yaml().unwrap();
|
||||
assert!(yaml.contains("node:"));
|
||||
assert!(yaml.contains("identity:"));
|
||||
assert!(yaml.contains("nsec:"));
|
||||
assert!(yaml.contains("test_nsec"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_yaml_empty_nsec_omitted() {
|
||||
let config = Config::new();
|
||||
let yaml = config.to_yaml().unwrap();
|
||||
|
||||
// Empty nsec should not be serialized
|
||||
assert!(!yaml.contains("nsec:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_transport_single_instance() {
|
||||
let yaml = r#"
|
||||
transports:
|
||||
udp:
|
||||
bind_addr: "0.0.0.0:4000"
|
||||
mtu: 1400
|
||||
"#;
|
||||
let config: Config = serde_yaml::from_str(yaml).unwrap();
|
||||
|
||||
assert_eq!(config.transports.udp.len(), 1);
|
||||
let instances: Vec<_> = config.transports.udp.iter().collect();
|
||||
assert_eq!(instances.len(), 1);
|
||||
assert_eq!(instances[0].0, None); // Single instance has no name
|
||||
assert_eq!(instances[0].1.bind_addr(), "0.0.0.0:4000");
|
||||
assert_eq!(instances[0].1.mtu(), 1400);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_transport_named_instances() {
|
||||
let yaml = r#"
|
||||
transports:
|
||||
udp:
|
||||
main:
|
||||
bind_addr: "0.0.0.0:4000"
|
||||
backup:
|
||||
bind_addr: "192.168.1.100:4001"
|
||||
mtu: 1280
|
||||
"#;
|
||||
let config: Config = serde_yaml::from_str(yaml).unwrap();
|
||||
|
||||
assert_eq!(config.transports.udp.len(), 2);
|
||||
|
||||
let instances: std::collections::HashMap<_, _> =
|
||||
config.transports.udp.iter().collect();
|
||||
|
||||
// Named instances have Some(name)
|
||||
assert!(instances.contains_key(&Some("main")));
|
||||
assert!(instances.contains_key(&Some("backup")));
|
||||
assert_eq!(instances[&Some("main")].bind_addr(), "0.0.0.0:4000");
|
||||
assert_eq!(instances[&Some("backup")].bind_addr(), "192.168.1.100:4001");
|
||||
assert_eq!(instances[&Some("backup")].mtu(), 1280);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_transport_empty() {
|
||||
let yaml = r#"
|
||||
transports: {}
|
||||
"#;
|
||||
let config: Config = serde_yaml::from_str(yaml).unwrap();
|
||||
assert!(config.transports.udp.is_empty());
|
||||
assert!(config.transports.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transport_instances_iter() {
|
||||
// Single instance - no name
|
||||
let single = TransportInstances::Single(UdpConfig {
|
||||
bind_addr: Some("0.0.0.0:4000".to_string()),
|
||||
mtu: None,
|
||||
});
|
||||
let items: Vec<_> = single.iter().collect();
|
||||
assert_eq!(items.len(), 1);
|
||||
assert_eq!(items[0].0, None);
|
||||
|
||||
// Named instances - have names
|
||||
let mut map = HashMap::new();
|
||||
map.insert("a".to_string(), UdpConfig::default());
|
||||
map.insert("b".to_string(), UdpConfig::default());
|
||||
let named = TransportInstances::Named(map);
|
||||
let items: Vec<_> = named.iter().collect();
|
||||
assert_eq!(items.len(), 2);
|
||||
// All named instances should have Some(name)
|
||||
assert!(items.iter().all(|(name, _)| name.is_some()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_peer_config() {
|
||||
let yaml = r#"
|
||||
peers:
|
||||
- npub: "npub1abc123"
|
||||
alias: "gateway"
|
||||
addresses:
|
||||
- transport: udp
|
||||
addr: "192.168.1.1:4000"
|
||||
priority: 1
|
||||
- transport: tor
|
||||
addr: "xyz.onion:4000"
|
||||
priority: 2
|
||||
connect_policy: auto_connect
|
||||
"#;
|
||||
let config: Config = serde_yaml::from_str(yaml).unwrap();
|
||||
|
||||
assert_eq!(config.peers.len(), 1);
|
||||
let peer = &config.peers[0];
|
||||
assert_eq!(peer.npub, "npub1abc123");
|
||||
assert_eq!(peer.alias, Some("gateway".to_string()));
|
||||
assert_eq!(peer.addresses.len(), 2);
|
||||
assert!(peer.is_auto_connect());
|
||||
|
||||
// Check addresses are sorted by priority
|
||||
let sorted = peer.addresses_by_priority();
|
||||
assert_eq!(sorted[0].transport, "udp");
|
||||
assert_eq!(sorted[0].priority, 1);
|
||||
assert_eq!(sorted[1].transport, "tor");
|
||||
assert_eq!(sorted[1].priority, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_peer_minimal() {
|
||||
let yaml = r#"
|
||||
peers:
|
||||
- npub: "npub1xyz"
|
||||
addresses:
|
||||
- transport: udp
|
||||
addr: "10.0.0.1:4000"
|
||||
"#;
|
||||
let config: Config = serde_yaml::from_str(yaml).unwrap();
|
||||
|
||||
assert_eq!(config.peers.len(), 1);
|
||||
let peer = &config.peers[0];
|
||||
assert_eq!(peer.npub, "npub1xyz");
|
||||
assert!(peer.alias.is_none());
|
||||
// Default connect_policy is auto_connect
|
||||
assert!(peer.is_auto_connect());
|
||||
// Default priority is 100
|
||||
assert_eq!(peer.addresses[0].priority, 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_multiple_peers() {
|
||||
let yaml = r#"
|
||||
peers:
|
||||
- npub: "npub1peer1"
|
||||
addresses:
|
||||
- transport: udp
|
||||
addr: "10.0.0.1:4000"
|
||||
- npub: "npub1peer2"
|
||||
addresses:
|
||||
- transport: udp
|
||||
addr: "10.0.0.2:4000"
|
||||
connect_policy: on_demand
|
||||
"#;
|
||||
let config: Config = serde_yaml::from_str(yaml).unwrap();
|
||||
|
||||
assert_eq!(config.peers.len(), 2);
|
||||
assert_eq!(config.auto_connect_peers().count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_peer_config_builder() {
|
||||
let peer = PeerConfig::new("npub1test", "udp", "192.168.1.1:4000")
|
||||
.with_alias("test-peer")
|
||||
.with_address(PeerAddress::with_priority("tor", "xyz.onion:4000", 50));
|
||||
|
||||
assert_eq!(peer.npub, "npub1test");
|
||||
assert_eq!(peer.alias, Some("test-peer".to_string()));
|
||||
assert_eq!(peer.addresses.len(), 2);
|
||||
assert!(peer.is_auto_connect());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
//! Node configuration subsections.
|
||||
//!
|
||||
//! All the `node.*` configuration parameters: resource limits, rate limiting,
|
||||
//! retry/backoff, cache sizing, discovery, spanning tree, bloom filters,
|
||||
//! session management, and internal buffers.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::IdentityConfig;
|
||||
|
||||
// ============================================================================
|
||||
// Node Configuration Subsections
|
||||
// ============================================================================
|
||||
|
||||
/// Resource limits (`node.limits.*`).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LimitsConfig {
|
||||
/// Max handshake-phase connections (`node.limits.max_connections`).
|
||||
#[serde(default = "LimitsConfig::default_max_connections")]
|
||||
pub max_connections: usize,
|
||||
/// Max authenticated peers (`node.limits.max_peers`).
|
||||
#[serde(default = "LimitsConfig::default_max_peers")]
|
||||
pub max_peers: usize,
|
||||
/// Max active links (`node.limits.max_links`).
|
||||
#[serde(default = "LimitsConfig::default_max_links")]
|
||||
pub max_links: usize,
|
||||
/// Max pending inbound handshakes (`node.limits.max_pending_inbound`).
|
||||
#[serde(default = "LimitsConfig::default_max_pending_inbound")]
|
||||
pub max_pending_inbound: usize,
|
||||
}
|
||||
|
||||
impl Default for LimitsConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_connections: 256,
|
||||
max_peers: 128,
|
||||
max_links: 256,
|
||||
max_pending_inbound: 1000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LimitsConfig {
|
||||
fn default_max_connections() -> usize { 256 }
|
||||
fn default_max_peers() -> usize { 128 }
|
||||
fn default_max_links() -> usize { 256 }
|
||||
fn default_max_pending_inbound() -> usize { 1000 }
|
||||
}
|
||||
|
||||
/// Rate limiting (`node.rate_limit.*`).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RateLimitConfig {
|
||||
/// Token bucket burst capacity (`node.rate_limit.handshake_burst`).
|
||||
#[serde(default = "RateLimitConfig::default_handshake_burst")]
|
||||
pub handshake_burst: u32,
|
||||
/// Tokens/sec refill rate (`node.rate_limit.handshake_rate`).
|
||||
#[serde(default = "RateLimitConfig::default_handshake_rate")]
|
||||
pub handshake_rate: f64,
|
||||
/// Stale handshake cleanup timeout in seconds (`node.rate_limit.handshake_timeout_secs`).
|
||||
#[serde(default = "RateLimitConfig::default_handshake_timeout_secs")]
|
||||
pub handshake_timeout_secs: u64,
|
||||
}
|
||||
|
||||
impl Default for RateLimitConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
handshake_burst: 100,
|
||||
handshake_rate: 10.0,
|
||||
handshake_timeout_secs: 30,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RateLimitConfig {
|
||||
fn default_handshake_burst() -> u32 { 100 }
|
||||
fn default_handshake_rate() -> f64 { 10.0 }
|
||||
fn default_handshake_timeout_secs() -> u64 { 30 }
|
||||
}
|
||||
|
||||
/// Retry/backoff configuration (`node.retry.*`).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RetryConfig {
|
||||
/// Max connection retry attempts (`node.retry.max_retries`).
|
||||
#[serde(default = "RetryConfig::default_max_retries")]
|
||||
pub max_retries: u32,
|
||||
/// Base backoff interval in seconds (`node.retry.base_interval_secs`).
|
||||
#[serde(default = "RetryConfig::default_base_interval_secs")]
|
||||
pub base_interval_secs: u64,
|
||||
/// Cap on exponential backoff in seconds (`node.retry.max_backoff_secs`).
|
||||
#[serde(default = "RetryConfig::default_max_backoff_secs")]
|
||||
pub max_backoff_secs: u64,
|
||||
}
|
||||
|
||||
impl Default for RetryConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_retries: 5,
|
||||
base_interval_secs: 5,
|
||||
max_backoff_secs: 300,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RetryConfig {
|
||||
fn default_max_retries() -> u32 { 5 }
|
||||
fn default_base_interval_secs() -> u64 { 5 }
|
||||
fn default_max_backoff_secs() -> u64 { 300 }
|
||||
}
|
||||
|
||||
/// Cache parameters (`node.cache.*`).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CacheConfig {
|
||||
/// Max entries in coord cache (`node.cache.coord_size`).
|
||||
#[serde(default = "CacheConfig::default_coord_size")]
|
||||
pub coord_size: usize,
|
||||
/// Coord cache entry TTL in seconds (`node.cache.coord_ttl_secs`).
|
||||
#[serde(default = "CacheConfig::default_coord_ttl_secs")]
|
||||
pub coord_ttl_secs: u64,
|
||||
/// Max entries in route cache (`node.cache.route_size`).
|
||||
#[serde(default = "CacheConfig::default_route_size")]
|
||||
pub route_size: usize,
|
||||
}
|
||||
|
||||
impl Default for CacheConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
coord_size: 50_000,
|
||||
coord_ttl_secs: 300,
|
||||
route_size: 10_000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheConfig {
|
||||
fn default_coord_size() -> usize { 50_000 }
|
||||
fn default_coord_ttl_secs() -> u64 { 300 }
|
||||
fn default_route_size() -> usize { 10_000 }
|
||||
}
|
||||
|
||||
/// Discovery protocol (`node.discovery.*`).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DiscoveryConfig {
|
||||
/// Hop limit for LookupRequest flood (`node.discovery.ttl`).
|
||||
#[serde(default = "DiscoveryConfig::default_ttl")]
|
||||
pub ttl: u8,
|
||||
/// Lookup completion timeout in seconds (`node.discovery.timeout_secs`).
|
||||
#[serde(default = "DiscoveryConfig::default_timeout_secs")]
|
||||
pub timeout_secs: u64,
|
||||
/// Dedup cache expiry in seconds (`node.discovery.recent_expiry_secs`).
|
||||
#[serde(default = "DiscoveryConfig::default_recent_expiry_secs")]
|
||||
pub recent_expiry_secs: u64,
|
||||
}
|
||||
|
||||
impl Default for DiscoveryConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ttl: 64,
|
||||
timeout_secs: 10,
|
||||
recent_expiry_secs: 10,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DiscoveryConfig {
|
||||
fn default_ttl() -> u8 { 64 }
|
||||
fn default_timeout_secs() -> u64 { 10 }
|
||||
fn default_recent_expiry_secs() -> u64 { 10 }
|
||||
}
|
||||
|
||||
/// Spanning tree (`node.tree.*`).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TreeConfig {
|
||||
/// Root self-announcement interval in seconds (`node.tree.root_refresh_secs`).
|
||||
#[serde(default = "TreeConfig::default_root_refresh_secs")]
|
||||
pub root_refresh_secs: u64,
|
||||
/// Per-peer TreeAnnounce rate limit in ms (`node.tree.announce_min_interval_ms`).
|
||||
#[serde(default = "TreeConfig::default_announce_min_interval_ms")]
|
||||
pub announce_min_interval_ms: u64,
|
||||
/// Min depth improvement to switch parents (`node.tree.parent_switch_threshold`).
|
||||
#[serde(default = "TreeConfig::default_parent_switch_threshold")]
|
||||
pub parent_switch_threshold: usize,
|
||||
}
|
||||
|
||||
impl Default for TreeConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
root_refresh_secs: 1800,
|
||||
announce_min_interval_ms: 500,
|
||||
parent_switch_threshold: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TreeConfig {
|
||||
fn default_root_refresh_secs() -> u64 { 1800 }
|
||||
fn default_announce_min_interval_ms() -> u64 { 500 }
|
||||
fn default_parent_switch_threshold() -> usize { 1 }
|
||||
}
|
||||
|
||||
/// Bloom filter (`node.bloom.*`).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BloomConfig {
|
||||
/// Debounce interval for filter updates in ms (`node.bloom.update_debounce_ms`).
|
||||
#[serde(default = "BloomConfig::default_update_debounce_ms")]
|
||||
pub update_debounce_ms: u64,
|
||||
}
|
||||
|
||||
impl Default for BloomConfig {
|
||||
fn default() -> Self {
|
||||
Self { update_debounce_ms: 500 }
|
||||
}
|
||||
}
|
||||
|
||||
impl BloomConfig {
|
||||
fn default_update_debounce_ms() -> u64 { 500 }
|
||||
}
|
||||
|
||||
/// Session/data plane (`node.session.*`).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SessionConfig {
|
||||
/// Default SessionDatagram hop limit (`node.session.default_hop_limit`).
|
||||
#[serde(default = "SessionConfig::default_hop_limit")]
|
||||
pub default_hop_limit: u8,
|
||||
/// Queue depth per dest during session establishment (`node.session.pending_packets_per_dest`).
|
||||
#[serde(default = "SessionConfig::default_pending_packets_per_dest")]
|
||||
pub pending_packets_per_dest: usize,
|
||||
/// Max destinations with pending packets (`node.session.pending_max_destinations`).
|
||||
#[serde(default = "SessionConfig::default_pending_max_destinations")]
|
||||
pub pending_max_destinations: usize,
|
||||
}
|
||||
|
||||
impl Default for SessionConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
default_hop_limit: 64,
|
||||
pending_packets_per_dest: 16,
|
||||
pending_max_destinations: 256,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SessionConfig {
|
||||
fn default_hop_limit() -> u8 { 64 }
|
||||
fn default_pending_packets_per_dest() -> usize { 16 }
|
||||
fn default_pending_max_destinations() -> usize { 256 }
|
||||
}
|
||||
|
||||
/// Internal buffers (`node.buffers.*`).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BuffersConfig {
|
||||
/// Transport→Node packet channel capacity (`node.buffers.packet_channel`).
|
||||
#[serde(default = "BuffersConfig::default_packet_channel")]
|
||||
pub packet_channel: usize,
|
||||
/// TUN→Node outbound channel capacity (`node.buffers.tun_channel`).
|
||||
#[serde(default = "BuffersConfig::default_tun_channel")]
|
||||
pub tun_channel: usize,
|
||||
/// DNS→Node identity channel capacity (`node.buffers.dns_channel`).
|
||||
#[serde(default = "BuffersConfig::default_dns_channel")]
|
||||
pub dns_channel: usize,
|
||||
}
|
||||
|
||||
impl Default for BuffersConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
packet_channel: 1024,
|
||||
tun_channel: 1024,
|
||||
dns_channel: 64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BuffersConfig {
|
||||
fn default_packet_channel() -> usize { 1024 }
|
||||
fn default_tun_channel() -> usize { 1024 }
|
||||
fn default_dns_channel() -> usize { 64 }
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Node Configuration (Root)
|
||||
// ============================================================================
|
||||
|
||||
/// Node configuration (`node.*`).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NodeConfig {
|
||||
/// Identity configuration (`node.identity.*`).
|
||||
#[serde(default)]
|
||||
pub identity: IdentityConfig,
|
||||
|
||||
/// Leaf-only mode (`node.leaf_only`).
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub leaf_only: bool,
|
||||
|
||||
/// RX loop maintenance tick period in seconds (`node.tick_interval_secs`).
|
||||
#[serde(default = "NodeConfig::default_tick_interval_secs")]
|
||||
pub tick_interval_secs: u64,
|
||||
|
||||
/// Initial RTT estimate for new links in ms (`node.base_rtt_ms`).
|
||||
#[serde(default = "NodeConfig::default_base_rtt_ms")]
|
||||
pub base_rtt_ms: u64,
|
||||
|
||||
/// Resource limits (`node.limits.*`).
|
||||
#[serde(default)]
|
||||
pub limits: LimitsConfig,
|
||||
|
||||
/// Rate limiting (`node.rate_limit.*`).
|
||||
#[serde(default)]
|
||||
pub rate_limit: RateLimitConfig,
|
||||
|
||||
/// Retry/backoff (`node.retry.*`).
|
||||
#[serde(default)]
|
||||
pub retry: RetryConfig,
|
||||
|
||||
/// Cache parameters (`node.cache.*`).
|
||||
#[serde(default)]
|
||||
pub cache: CacheConfig,
|
||||
|
||||
/// Discovery protocol (`node.discovery.*`).
|
||||
#[serde(default)]
|
||||
pub discovery: DiscoveryConfig,
|
||||
|
||||
/// Spanning tree (`node.tree.*`).
|
||||
#[serde(default)]
|
||||
pub tree: TreeConfig,
|
||||
|
||||
/// Bloom filter (`node.bloom.*`).
|
||||
#[serde(default)]
|
||||
pub bloom: BloomConfig,
|
||||
|
||||
/// Session/data plane (`node.session.*`).
|
||||
#[serde(default)]
|
||||
pub session: SessionConfig,
|
||||
|
||||
/// Internal buffers (`node.buffers.*`).
|
||||
#[serde(default)]
|
||||
pub buffers: BuffersConfig,
|
||||
}
|
||||
|
||||
impl Default for NodeConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
identity: IdentityConfig::default(),
|
||||
leaf_only: false,
|
||||
tick_interval_secs: 1,
|
||||
base_rtt_ms: 100,
|
||||
limits: LimitsConfig::default(),
|
||||
rate_limit: RateLimitConfig::default(),
|
||||
retry: RetryConfig::default(),
|
||||
cache: CacheConfig::default(),
|
||||
discovery: DiscoveryConfig::default(),
|
||||
tree: TreeConfig::default(),
|
||||
bloom: BloomConfig::default(),
|
||||
session: SessionConfig::default(),
|
||||
buffers: BuffersConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeConfig {
|
||||
fn default_tick_interval_secs() -> u64 { 1 }
|
||||
fn default_base_rtt_ms() -> u64 { 100 }
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
//! Peer configuration types.
|
||||
//!
|
||||
//! Known peer definitions with transport addresses and connection policies.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Connection policy for a peer.
|
||||
///
|
||||
/// Determines when and how to establish a connection to a peer.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ConnectPolicy {
|
||||
/// Connect to this peer automatically on node startup.
|
||||
/// This is the only policy supported in the initial implementation.
|
||||
#[default]
|
||||
AutoConnect,
|
||||
|
||||
/// Connect only when traffic needs to be routed through this peer (future).
|
||||
OnDemand,
|
||||
|
||||
/// Wait for explicit API call to connect (future).
|
||||
Manual,
|
||||
}
|
||||
|
||||
/// A transport-specific address for reaching a peer.
|
||||
///
|
||||
/// Each peer can have multiple addresses across different transports,
|
||||
/// allowing fallback if one transport is unavailable.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct PeerAddress {
|
||||
/// Transport type (e.g., "udp", "tor", "ethernet").
|
||||
pub transport: String,
|
||||
|
||||
/// Transport-specific address string.
|
||||
///
|
||||
/// Format depends on transport type:
|
||||
/// - UDP: "host:port" (e.g., "192.168.1.1:4000")
|
||||
/// - Tor: "onion_address:port" (e.g., "xyz...abc.onion:4000")
|
||||
/// - Ethernet: "interface/mac" (future)
|
||||
pub addr: String,
|
||||
|
||||
/// Priority for address selection (lower = preferred).
|
||||
/// When multiple addresses are available, lower priority addresses
|
||||
/// are tried first.
|
||||
#[serde(default = "default_priority")]
|
||||
pub priority: u8,
|
||||
}
|
||||
|
||||
fn default_priority() -> u8 {
|
||||
100
|
||||
}
|
||||
|
||||
impl PeerAddress {
|
||||
/// Create a new peer address.
|
||||
pub fn new(transport: impl Into<String>, addr: impl Into<String>) -> Self {
|
||||
Self {
|
||||
transport: transport.into(),
|
||||
addr: addr.into(),
|
||||
priority: default_priority(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new peer address with priority.
|
||||
pub fn with_priority(transport: impl Into<String>, addr: impl Into<String>, priority: u8) -> Self {
|
||||
Self {
|
||||
transport: transport.into(),
|
||||
addr: addr.into(),
|
||||
priority,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for a known peer.
|
||||
///
|
||||
/// Peers are identified by their Nostr public key (npub) and can have
|
||||
/// multiple transport addresses for reaching them.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct PeerConfig {
|
||||
/// The peer's Nostr public key in npub (bech32) or hex format.
|
||||
pub npub: String,
|
||||
|
||||
/// Human-readable alias for the peer (optional).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub alias: Option<String>,
|
||||
|
||||
/// Transport addresses for reaching this peer.
|
||||
/// At least one address is required.
|
||||
pub addresses: Vec<PeerAddress>,
|
||||
|
||||
/// Connection policy for this peer.
|
||||
#[serde(default)]
|
||||
pub connect_policy: ConnectPolicy,
|
||||
}
|
||||
|
||||
impl PeerConfig {
|
||||
/// Create a new peer config with a single address.
|
||||
pub fn new(npub: impl Into<String>, transport: impl Into<String>, addr: impl Into<String>) -> Self {
|
||||
Self {
|
||||
npub: npub.into(),
|
||||
alias: None,
|
||||
addresses: vec![PeerAddress::new(transport, addr)],
|
||||
connect_policy: ConnectPolicy::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set an alias for the peer.
|
||||
pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
|
||||
self.alias = Some(alias.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Add an additional address for the peer.
|
||||
pub fn with_address(mut self, addr: PeerAddress) -> Self {
|
||||
self.addresses.push(addr);
|
||||
self
|
||||
}
|
||||
|
||||
/// Get addresses sorted by priority (lowest first).
|
||||
pub fn addresses_by_priority(&self) -> Vec<&PeerAddress> {
|
||||
let mut addrs: Vec<_> = self.addresses.iter().collect();
|
||||
addrs.sort_by_key(|a| a.priority);
|
||||
addrs
|
||||
}
|
||||
|
||||
/// Check if this peer should auto-connect on startup.
|
||||
pub fn is_auto_connect(&self) -> bool {
|
||||
matches!(self.connect_policy, ConnectPolicy::AutoConnect)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
//! Transport configuration types.
|
||||
//!
|
||||
//! Generic transport instance handling (single vs. named) and
|
||||
//! transport-specific configuration structs.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Default UDP bind address.
|
||||
const DEFAULT_UDP_BIND_ADDR: &str = "0.0.0.0:4000";
|
||||
|
||||
/// Default UDP MTU (IPv6 minimum).
|
||||
const DEFAULT_UDP_MTU: u16 = 1280;
|
||||
|
||||
/// UDP transport instance configuration.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct UdpConfig {
|
||||
/// Bind address (`bind_addr`). Defaults to "0.0.0.0:4000".
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub bind_addr: Option<String>,
|
||||
|
||||
/// UDP MTU (`mtu`). Defaults to 1280 (IPv6 minimum).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub mtu: Option<u16>,
|
||||
}
|
||||
|
||||
impl UdpConfig {
|
||||
/// Get the bind address, using default if not configured.
|
||||
pub fn bind_addr(&self) -> &str {
|
||||
self.bind_addr.as_deref().unwrap_or(DEFAULT_UDP_BIND_ADDR)
|
||||
}
|
||||
|
||||
/// Get the UDP MTU, using default if not configured.
|
||||
pub fn mtu(&self) -> u16 {
|
||||
self.mtu.unwrap_or(DEFAULT_UDP_MTU)
|
||||
}
|
||||
}
|
||||
|
||||
/// Transport instances - either a single config or named instances.
|
||||
///
|
||||
/// Allows both simple single-instance config:
|
||||
/// ```yaml
|
||||
/// transports:
|
||||
/// udp:
|
||||
/// bind_addr: "0.0.0.0:4000"
|
||||
/// ```
|
||||
///
|
||||
/// And multiple named instances:
|
||||
/// ```yaml
|
||||
/// transports:
|
||||
/// udp:
|
||||
/// main:
|
||||
/// bind_addr: "0.0.0.0:4000"
|
||||
/// backup:
|
||||
/// bind_addr: "192.168.1.100:4001"
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum TransportInstances<T> {
|
||||
/// Single unnamed instance (config fields directly under transport type).
|
||||
Single(T),
|
||||
/// Multiple named instances.
|
||||
Named(HashMap<String, T>),
|
||||
}
|
||||
|
||||
impl<T> TransportInstances<T> {
|
||||
/// Get the number of instances.
|
||||
pub fn len(&self) -> usize {
|
||||
match self {
|
||||
TransportInstances::Single(_) => 1,
|
||||
TransportInstances::Named(map) => map.len(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if there are no instances.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
TransportInstances::Single(_) => false,
|
||||
TransportInstances::Named(map) => map.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over all instances as (name, config) pairs.
|
||||
///
|
||||
/// Single instances have `None` as the name.
|
||||
/// Named instances have `Some(name)`.
|
||||
pub fn iter(&self) -> impl Iterator<Item = (Option<&str>, &T)> {
|
||||
match self {
|
||||
TransportInstances::Single(config) => {
|
||||
vec![(None, config)].into_iter()
|
||||
}
|
||||
TransportInstances::Named(map) => {
|
||||
map.iter()
|
||||
.map(|(k, v)| (Some(k.as_str()), v))
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for TransportInstances<T> {
|
||||
fn default() -> Self {
|
||||
TransportInstances::Named(HashMap::new())
|
||||
}
|
||||
}
|
||||
|
||||
/// Transports configuration section.
|
||||
///
|
||||
/// Each transport type can have either a single instance (config directly
|
||||
/// under the type name) or multiple named instances.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct TransportsConfig {
|
||||
/// UDP transport instances.
|
||||
#[serde(default, skip_serializing_if = "is_transport_empty")]
|
||||
pub udp: TransportInstances<UdpConfig>,
|
||||
|
||||
// Future transport types:
|
||||
// #[serde(default, skip_serializing_if = "is_transport_empty")]
|
||||
// pub tcp: TransportInstances<TcpConfig>,
|
||||
//
|
||||
// #[serde(default, skip_serializing_if = "is_transport_empty")]
|
||||
// pub tor: TransportInstances<TorConfig>,
|
||||
}
|
||||
|
||||
/// Helper for skip_serializing_if on TransportInstances.
|
||||
fn is_transport_empty<T>(instances: &TransportInstances<T>) -> bool {
|
||||
instances.is_empty()
|
||||
}
|
||||
|
||||
impl TransportsConfig {
|
||||
/// Check if any transports are configured.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.udp.is_empty()
|
||||
// && self.tcp.is_empty()
|
||||
// && self.tor.is_empty()
|
||||
}
|
||||
|
||||
/// Merge another TransportsConfig into this one.
|
||||
///
|
||||
/// Non-empty transport sections from `other` replace those in `self`.
|
||||
pub fn merge(&mut self, other: TransportsConfig) {
|
||||
if !other.udp.is_empty() {
|
||||
self.udp = other.udp;
|
||||
}
|
||||
// Future: same for tcp, tor, etc.
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -7,8 +7,8 @@ pub mod bloom;
|
||||
pub mod cache;
|
||||
pub mod config;
|
||||
pub mod identity;
|
||||
pub mod index;
|
||||
pub mod noise;
|
||||
pub mod utils;
|
||||
pub mod node;
|
||||
pub mod peer;
|
||||
pub mod protocol;
|
||||
@@ -23,7 +23,8 @@ pub use identity::{
|
||||
};
|
||||
|
||||
// Re-export config types
|
||||
pub use config::{Config, ConfigError, DnsConfig, IdentityConfig, TunConfig, UdpConfig};
|
||||
pub use config::{Config, ConfigError, IdentityConfig, UdpConfig};
|
||||
pub use upper::config::{DnsConfig, TunConfig};
|
||||
|
||||
// Re-export tree types
|
||||
pub use tree::{CoordEntry, ParentDeclaration, TreeCoordinate, TreeError, TreeState};
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ mod tests;
|
||||
|
||||
use crate::bloom::BloomState;
|
||||
use crate::cache::{CoordCache, RouteCache};
|
||||
use crate::index::IndexAllocator;
|
||||
use crate::utils::index::IndexAllocator;
|
||||
use crate::node::session::SessionEntry;
|
||||
use crate::peer::{ActivePeer, PeerConnection};
|
||||
use self::rate_limit::HandshakeRateLimiter;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::*;
|
||||
use crate::index::SessionIndex;
|
||||
use crate::utils::index::SessionIndex;
|
||||
use crate::transport::{packet_channel, LinkDirection, TransportAddr};
|
||||
use crate::PeerIdentity;
|
||||
use std::time::Duration;
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
//! | 0x01 | Noise IK msg1 | 87 bytes | Handshake initiation |
|
||||
//! | 0x02 | Noise IK msg2 | 42 bytes | Handshake response |
|
||||
|
||||
use crate::index::SessionIndex;
|
||||
use crate::utils::index::SessionIndex;
|
||||
use crate::noise::{HANDSHAKE_MSG1_SIZE, HANDSHAKE_MSG2_SIZE, TAG_SIZE};
|
||||
|
||||
// ============================================================================
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
//! ActivePeer holds tree state, Bloom filter, and routing information.
|
||||
|
||||
use crate::bloom::BloomFilter;
|
||||
use crate::index::SessionIndex;
|
||||
use crate::utils::index::SessionIndex;
|
||||
use crate::noise::NoiseSession;
|
||||
use crate::transport::{LinkId, LinkStats, TransportAddr, TransportId};
|
||||
use crate::tree::{ParentDeclaration, TreeCoordinate};
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
//! PeerConnection tracks the Noise IK handshake state and transitions to
|
||||
//! ActivePeer upon successful authentication.
|
||||
|
||||
use crate::index::SessionIndex;
|
||||
use crate::utils::index::SessionIndex;
|
||||
use crate::noise::{self, NoiseError, NoiseSession};
|
||||
use crate::transport::{LinkDirection, LinkId, LinkStats, TransportAddr, TransportId};
|
||||
use crate::PeerIdentity;
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
//! Upper layer configuration types.
|
||||
//!
|
||||
//! Configuration for the IPv6 adaptation layer components: TUN interface
|
||||
//! and DNS responder.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Default TUN device name.
|
||||
const DEFAULT_TUN_NAME: &str = "fips0";
|
||||
|
||||
/// Default TUN MTU (IPv6 minimum).
|
||||
const DEFAULT_TUN_MTU: u16 = 1280;
|
||||
|
||||
/// Default DNS responder bind address.
|
||||
const DEFAULT_DNS_BIND_ADDR: &str = "127.0.0.1";
|
||||
|
||||
/// Default DNS responder port.
|
||||
const DEFAULT_DNS_PORT: u16 = 5354;
|
||||
|
||||
/// Default DNS record TTL in seconds (5 minutes).
|
||||
const DEFAULT_DNS_TTL: u32 = 300;
|
||||
|
||||
/// DNS responder configuration (`dns.*`).
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct DnsConfig {
|
||||
/// Enable DNS responder (`dns.enabled`).
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub enabled: bool,
|
||||
|
||||
/// Bind address (`dns.bind_addr`).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub bind_addr: Option<String>,
|
||||
|
||||
/// Port (`dns.port`).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub port: Option<u16>,
|
||||
|
||||
/// Record TTL in seconds (`dns.ttl`).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub ttl: Option<u32>,
|
||||
}
|
||||
|
||||
impl DnsConfig {
|
||||
/// Get the bind address (default: 127.0.0.1).
|
||||
pub fn bind_addr(&self) -> &str {
|
||||
self.bind_addr.as_deref().unwrap_or(DEFAULT_DNS_BIND_ADDR)
|
||||
}
|
||||
|
||||
/// Get the port (default: 5354).
|
||||
pub fn port(&self) -> u16 {
|
||||
self.port.unwrap_or(DEFAULT_DNS_PORT)
|
||||
}
|
||||
|
||||
/// Get the TTL in seconds (default: 300).
|
||||
pub fn ttl(&self) -> u32 {
|
||||
self.ttl.unwrap_or(DEFAULT_DNS_TTL)
|
||||
}
|
||||
}
|
||||
|
||||
/// TUN interface configuration (`tun.*`).
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct TunConfig {
|
||||
/// Enable TUN interface (`tun.enabled`).
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub enabled: bool,
|
||||
|
||||
/// Device name (`tun.name`).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
|
||||
/// MTU (`tun.mtu`).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub mtu: Option<u16>,
|
||||
}
|
||||
|
||||
impl TunConfig {
|
||||
/// Get the device name (default: "fips0").
|
||||
pub fn name(&self) -> &str {
|
||||
self.name.as_deref().unwrap_or(DEFAULT_TUN_NAME)
|
||||
}
|
||||
|
||||
/// Get the MTU (default: 1280).
|
||||
pub fn mtu(&self) -> u16 {
|
||||
self.mtu.unwrap_or(DEFAULT_TUN_MTU)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
//! responder (.fips domain resolution), and ICMPv6 handling (error
|
||||
//! signaling and neighbor discovery).
|
||||
|
||||
pub mod config;
|
||||
pub mod dns;
|
||||
pub mod icmp;
|
||||
pub mod tun;
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user