diff options
Diffstat (limited to 'src/advertise.rs')
| -rw-r--r-- | src/advertise.rs | 173 |
1 files changed, 173 insertions, 0 deletions
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<u32, PendingAd>, + + /// Received advertisements by peer ID. + received: HashMap<NodeId, ReceivedAd>, +} + +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); + } +} |