aboutsummaryrefslogtreecommitdiffstats
path: root/src/advertise.rs
diff options
context:
space:
mode:
authormurilo ijanc2026-03-24 15:04:03 -0300
committermurilo ijanc2026-03-24 15:04:03 -0300
commit9821aabf0b50d2487b07502d3d2cd89e7d62bdbe (patch)
tree53da095ff90cc755bac3d4bf699172b5e8cd07d6 /src/advertise.rs
downloadtesseras-dht-9821aabf0b50d2487b07502d3d2cd89e7d62bdbe.tar.gz
Initial commitv0.1.0
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
Diffstat (limited to 'src/advertise.rs')
-rw-r--r--src/advertise.rs173
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);
+ }
+}