From 9821aabf0b50d2487b07502d3d2cd89e7d62bdbe Mon Sep 17 00:00:00 2001 From: murilo ijanc Date: Tue, 24 Mar 2026 15:04:03 -0300 Subject: Initial commit NAT-aware Kademlia DHT library for peer-to-peer networks. Features: - Distributed key-value storage (iterative FIND_NODE, FIND_VALUE, STORE) - NAT traversal via DTUN hole-punching and proxy relay - Reliable Datagram Protocol (RDP) with 7-state connection machine - Datagram transport with automatic fragmentation/reassembly - Ed25519 packet authentication - 256-bit node IDs (Ed25519 public keys) - Rate limiting, ban list, and eclipse attack mitigation - Persistence and metrics - OpenBSD and Linux support --- src/advertise.rs | 173 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 src/advertise.rs (limited to 'src/advertise.rs') diff --git a/src/advertise.rs b/src/advertise.rs new file mode 100644 index 0000000..b415b5b --- /dev/null +++ b/src/advertise.rs @@ -0,0 +1,173 @@ +//! Local address advertisement. +//! +//! Allows a node to announce its address to peers so +//! they can update their routing tables with the correct +//! endpoint. +//! +//! Used after NAT detection to inform peers of the +//! node's externally-visible address. + +use std::collections::HashMap; +use std::time::{Duration, Instant}; + +use crate::id::NodeId; + +// ── Constants ──────────────────────────────────────── + +/// TTL for advertisements. +pub const ADVERTISE_TTL: Duration = Duration::from_secs(300); + +/// Timeout for a single advertisement attempt. +pub const ADVERTISE_TIMEOUT: Duration = Duration::from_secs(2); + +/// Interval between refresh cycles. +pub const ADVERTISE_REFRESH_INTERVAL: Duration = Duration::from_secs(100); + +/// A pending outgoing advertisement. +#[derive(Debug)] +struct PendingAd { + sent_at: Instant, +} + +/// A received advertisement from a peer. +#[derive(Debug)] +struct ReceivedAd { + received_at: Instant, +} + +/// Address advertisement manager. +pub struct Advertise { + /// Pending outgoing advertisements by nonce. + pending: HashMap, + + /// Received advertisements by peer ID. + received: HashMap, +} + +impl Advertise { + pub fn new(_local_id: NodeId) -> Self { + Self { + pending: HashMap::new(), + received: HashMap::new(), + } + } + + /// Start an advertisement to a peer. + /// + /// Returns the nonce to include in the message. + pub fn start_advertise(&mut self, nonce: u32) -> u32 { + self.pending.insert( + nonce, + PendingAd { + sent_at: Instant::now(), + }, + ); + nonce + } + + /// Handle an advertisement reply (our ad was accepted). + /// + /// Returns `true` if the nonce matched a pending ad. + pub fn recv_reply(&mut self, nonce: u32) -> bool { + self.pending.remove(&nonce).is_some() + } + + /// Handle an incoming advertisement from a peer. + /// + /// Records that this peer has advertised to us. + pub fn recv_advertise(&mut self, peer_id: NodeId) { + self.received.insert( + peer_id, + ReceivedAd { + received_at: Instant::now(), + }, + ); + } + + /// Check if a peer has advertised to us recently. + pub fn has_advertised(&self, peer_id: &NodeId) -> bool { + self.received + .get(peer_id) + .map(|ad| ad.received_at.elapsed() < ADVERTISE_TTL) + .unwrap_or(false) + } + + /// Remove expired pending ads and stale received ads. + pub fn refresh(&mut self) { + self.pending + .retain(|_, ad| ad.sent_at.elapsed() < ADVERTISE_TIMEOUT); + self.received + .retain(|_, ad| ad.received_at.elapsed() < ADVERTISE_TTL); + } + + /// Number of pending outgoing advertisements. + pub fn pending_count(&self) -> usize { + self.pending.len() + } + + /// Number of active received advertisements. + pub fn received_count(&self) -> usize { + self.received + .values() + .filter(|ad| ad.received_at.elapsed() < ADVERTISE_TTL) + .count() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn advertise_and_reply() { + let mut adv = Advertise::new(NodeId::from_bytes([0x01; 32])); + adv.start_advertise(42); + assert_eq!(adv.pending_count(), 1); + + assert!(adv.recv_reply(42)); + assert_eq!(adv.pending_count(), 0); + } + + #[test] + fn unknown_reply_ignored() { + let mut adv = Advertise::new(NodeId::from_bytes([0x01; 32])); + assert!(!adv.recv_reply(999)); + } + + #[test] + fn recv_advertisement() { + let mut adv = Advertise::new(NodeId::from_bytes([0x01; 32])); + let peer = NodeId::from_bytes([0x02; 32]); + + assert!(!adv.has_advertised(&peer)); + adv.recv_advertise(peer); + assert!(adv.has_advertised(&peer)); + assert_eq!(adv.received_count(), 1); + } + + #[test] + fn refresh_clears_stale() { + let mut adv = Advertise::new(NodeId::from_bytes([0x01; 32])); + + // Insert already-expired pending ad + adv.pending.insert( + 1, + PendingAd { + sent_at: Instant::now() - Duration::from_secs(10), + }, + ); + + // Insert already-expired received ad + let peer = NodeId::from_bytes([0x02; 32]); + adv.received.insert( + peer, + ReceivedAd { + received_at: Instant::now() - Duration::from_secs(600), + }, + ); + + adv.refresh(); + assert_eq!(adv.pending_count(), 0); + assert_eq!(adv.received_count(), 0); + } +} -- cgit v1.2.3