diff --git a/src/config/mod.rs b/src/config/mod.rs index e4343f5..b4b1eb7 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -34,7 +34,7 @@ use thiserror::Error; pub use gateway::{ConntrackConfig, GatewayConfig, GatewayDnsConfig, PortForward, Proto}; pub use node::{ BloomConfig, BuffersConfig, CacheConfig, ControlConfig, LimitsConfig, LookupConfig, MmpConfig, - NodeConfig, NostrDiscoveryConfig, NostrDiscoveryPolicy, RateLimitConfig, RekeyConfig, + NodeConfig, NostrRendezvousConfig, NostrRendezvousPolicy, RateLimitConfig, RekeyConfig, RendezvousConfig, RetryConfig, SessionConfig, SessionMmpConfig, TreeConfig, }; pub use peer::{ConnectPolicy, PeerAddress, PeerConfig}; @@ -1344,7 +1344,7 @@ peers: assert_eq!(config.node.rendezvous.nostr.signal_ttl_secs, 45); assert_eq!( config.node.rendezvous.nostr.policy, - NostrDiscoveryPolicy::ConfiguredOnly + NostrRendezvousPolicy::ConfiguredOnly ); assert_eq!(config.node.rendezvous.nostr.open_discovery_max_pending, 12); assert_eq!( diff --git a/src/config/node.rs b/src/config/node.rs index 9e54bfe..8f8173b 100644 --- a/src/config/node.rs +++ b/src/config/node.rs @@ -263,13 +263,13 @@ impl LookupConfig { pub struct RendezvousConfig { /// Nostr-mediated overlay endpoint rendezvous (`node.rendezvous.nostr.*`). #[serde(default)] - pub nostr: NostrDiscoveryConfig, + pub nostr: NostrRendezvousConfig, /// mDNS / DNS-SD peer rendezvous on the local link (`node.rendezvous.lan.*`). /// Identity surface is a strict subset of what `nostr.advertise` already /// publishes publicly, so there's no marginal privacy cost; the latency /// win for same-LAN peers is large (sub-second pairing, no relay). #[serde(default)] - pub lan: crate::discovery::lan::LanDiscoveryConfig, + pub lan: crate::mdns::LanRendezvousConfig, } /// COMPAT (drop at the v2 cutover): a deprecated legacy `node.discovery:` block. @@ -290,8 +290,8 @@ pub(crate) struct DiscoveryConfigCompat { pub backoff_base_secs: Option, pub backoff_max_secs: Option, pub forward_min_interval_secs: Option, - pub nostr: Option, - pub lan: Option, + pub nostr: Option, + pub lan: Option, } /// Nostr advert discovery policy. @@ -303,7 +303,7 @@ pub(crate) struct DiscoveryConfigCompat { /// - `open`: also consider adverts for non-configured peers #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] -pub enum NostrDiscoveryPolicy { +pub enum NostrRendezvousPolicy { Disabled, #[default] ConfiguredOnly, @@ -313,23 +313,23 @@ pub enum NostrDiscoveryPolicy { /// Nostr-mediated overlay endpoint discovery (`node.rendezvous.nostr.*`). #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct NostrDiscoveryConfig { +pub struct NostrRendezvousConfig { /// Enable Nostr-signaled traversal bootstrap. #[serde(default)] pub enabled: bool, /// Publish service advertisements so remote peers can bootstrap inbound. - #[serde(default = "NostrDiscoveryConfig::default_advertise")] + #[serde(default = "NostrRendezvousConfig::default_advertise")] pub advertise: bool, /// Relay URLs used for service advertisements. - #[serde(default = "NostrDiscoveryConfig::default_advert_relays")] + #[serde(default = "NostrRendezvousConfig::default_advert_relays")] pub advert_relays: Vec, /// Relay URLs used for encrypted signaling events. - #[serde(default = "NostrDiscoveryConfig::default_dm_relays")] + #[serde(default = "NostrRendezvousConfig::default_dm_relays")] pub dm_relays: Vec, /// STUN servers used for local reflexive address discovery. /// Outbound observation uses only this local list; peer-advertised STUN /// values are informational and are not treated as egress targets. - #[serde(default = "NostrDiscoveryConfig::default_stun_servers")] + #[serde(default = "NostrRendezvousConfig::default_stun_servers")] pub stun_servers: Vec, /// Whether to advertise local (RFC 1918 / ULA) interface addresses as /// host candidates in the traversal offer. @@ -343,85 +343,85 @@ pub struct NostrDiscoveryConfig { #[serde(default)] pub share_local_candidates: bool, /// Traversal application namespace and advert identifier suffix. - #[serde(default = "NostrDiscoveryConfig::default_app")] + #[serde(default = "NostrRendezvousConfig::default_app")] pub app: String, /// Signaling TTL in seconds. - #[serde(default = "NostrDiscoveryConfig::default_signal_ttl_secs")] + #[serde(default = "NostrRendezvousConfig::default_signal_ttl_secs")] pub signal_ttl_secs: u64, /// Policy for advert-derived endpoint discovery. #[serde(default)] - pub policy: NostrDiscoveryPolicy, + pub policy: NostrRendezvousPolicy, /// Max number of open-discovery peers queued for outbound retry/connection /// at once. Prevents unbounded queue growth from ambient advert traffic. - #[serde(default = "NostrDiscoveryConfig::default_open_discovery_max_pending")] + #[serde(default = "NostrRendezvousConfig::default_open_discovery_max_pending")] pub open_discovery_max_pending: usize, /// Max concurrent inbound traversal offers processed at once. /// Acts as a rate limit against offer spam from relays. - #[serde(default = "NostrDiscoveryConfig::default_max_concurrent_incoming_offers")] + #[serde(default = "NostrRendezvousConfig::default_max_concurrent_incoming_offers")] pub max_concurrent_incoming_offers: usize, /// Max cached overlay adverts retained from relay traffic. /// Bounds memory under ambient advert volume. - #[serde(default = "NostrDiscoveryConfig::default_advert_cache_max_entries")] + #[serde(default = "NostrRendezvousConfig::default_advert_cache_max_entries")] pub advert_cache_max_entries: usize, /// Max seen-session IDs retained for replay detection. /// Oldest entries are evicted when the cap is exceeded. - #[serde(default = "NostrDiscoveryConfig::default_seen_sessions_max_entries")] + #[serde(default = "NostrRendezvousConfig::default_seen_sessions_max_entries")] pub seen_sessions_max_entries: usize, /// Overall punch attempt timeout in seconds. - #[serde(default = "NostrDiscoveryConfig::default_attempt_timeout_secs")] + #[serde(default = "NostrRendezvousConfig::default_attempt_timeout_secs")] pub attempt_timeout_secs: u64, /// Replay tracking retention window in seconds. - #[serde(default = "NostrDiscoveryConfig::default_replay_window_secs")] + #[serde(default = "NostrRendezvousConfig::default_replay_window_secs")] pub replay_window_secs: u64, /// Delay before punch traffic starts. - #[serde(default = "NostrDiscoveryConfig::default_punch_start_delay_ms")] + #[serde(default = "NostrRendezvousConfig::default_punch_start_delay_ms")] pub punch_start_delay_ms: u64, /// Interval between punch packets. - #[serde(default = "NostrDiscoveryConfig::default_punch_interval_ms")] + #[serde(default = "NostrRendezvousConfig::default_punch_interval_ms")] pub punch_interval_ms: u64, /// How long to keep punching before failure. - #[serde(default = "NostrDiscoveryConfig::default_punch_duration_ms")] + #[serde(default = "NostrRendezvousConfig::default_punch_duration_ms")] pub punch_duration_ms: u64, /// Advert TTL in seconds. - #[serde(default = "NostrDiscoveryConfig::default_advert_ttl_secs")] + #[serde(default = "NostrRendezvousConfig::default_advert_ttl_secs")] pub advert_ttl_secs: u64, /// How often adverts are refreshed in seconds. - #[serde(default = "NostrDiscoveryConfig::default_advert_refresh_secs")] + #[serde(default = "NostrRendezvousConfig::default_advert_refresh_secs")] pub advert_refresh_secs: u64, /// Settle delay in seconds after Nostr discovery starts before the /// one-shot startup sweep of cached adverts runs. Allows the relay /// subscription backlog to populate the in-memory advert cache. /// Only used under `policy: open`. Default: 5. - #[serde(default = "NostrDiscoveryConfig::default_startup_sweep_delay_secs")] + #[serde(default = "NostrRendezvousConfig::default_startup_sweep_delay_secs")] pub startup_sweep_delay_secs: u64, /// Maximum age in seconds for cached adverts considered by the /// one-shot startup sweep. Adverts whose `created_at` is older than /// `now - startup_sweep_max_age_secs` are skipped. Only used under /// `policy: open`. Default: 3600 (1 hour). - #[serde(default = "NostrDiscoveryConfig::default_startup_sweep_max_age_secs")] + #[serde(default = "NostrRendezvousConfig::default_startup_sweep_max_age_secs")] pub startup_sweep_max_age_secs: u64, /// Number of consecutive NAT-traversal failures against a peer before /// an extended cooldown is applied to throttle further offer publishes. /// At this threshold the daemon also actively re-fetches the peer's /// advert from `advert_relays` to evict cache entries for peers that /// have gone away. Default: 5. - #[serde(default = "NostrDiscoveryConfig::default_failure_streak_threshold")] + #[serde(default = "NostrRendezvousConfig::default_failure_streak_threshold")] pub failure_streak_threshold: u32, /// Cooldown applied to a peer once `failure_streak_threshold` is hit. /// Suppresses both open-discovery sweep enqueues and per-attempt /// retry firings until elapsed. Default: 1800 (30 minutes). - #[serde(default = "NostrDiscoveryConfig::default_extended_cooldown_secs")] + #[serde(default = "NostrRendezvousConfig::default_extended_cooldown_secs")] pub extended_cooldown_secs: u64, /// Minimum interval between `NAT traversal failed` WARN log lines for /// the same peer. Subsequent failures inside the window log at DEBUG. /// Reduces log spam on public-test nodes with many cache-learned /// peers. Default: 300 (5 minutes). - #[serde(default = "NostrDiscoveryConfig::default_warn_log_interval_secs")] + #[serde(default = "NostrRendezvousConfig::default_warn_log_interval_secs")] pub warn_log_interval_secs: u64, /// Maximum entries retained in the per-npub failure-state map. /// Bounds memory under high cache turnover. Oldest entries (by last /// failure time) evicted when the cap is exceeded. Default: 4096. - #[serde(default = "NostrDiscoveryConfig::default_failure_state_max_entries")] + #[serde(default = "NostrRendezvousConfig::default_failure_state_max_entries")] pub failure_state_max_entries: usize, /// Cooldown applied after observing a fatal protocol mismatch on a /// Nostr-adopted bootstrap transport (e.g. `Unknown FMP version` @@ -429,11 +429,11 @@ pub struct NostrDiscoveryConfig { /// of `extended_cooldown_secs` and much longer because the mismatch /// is structural — re-traversing the peer is wasted effort until one /// side upgrades. Default: 86400 (24 hours). - #[serde(default = "NostrDiscoveryConfig::default_protocol_mismatch_cooldown_secs")] + #[serde(default = "NostrRendezvousConfig::default_protocol_mismatch_cooldown_secs")] pub protocol_mismatch_cooldown_secs: u64, } -impl Default for NostrDiscoveryConfig { +impl Default for NostrRendezvousConfig { fn default() -> Self { Self { enabled: false, @@ -444,7 +444,7 @@ impl Default for NostrDiscoveryConfig { share_local_candidates: false, app: Self::default_app(), signal_ttl_secs: Self::default_signal_ttl_secs(), - policy: NostrDiscoveryPolicy::default(), + policy: NostrRendezvousPolicy::default(), open_discovery_max_pending: Self::default_open_discovery_max_pending(), max_concurrent_incoming_offers: Self::default_max_concurrent_incoming_offers(), advert_cache_max_entries: Self::default_advert_cache_max_entries(), @@ -467,7 +467,7 @@ impl Default for NostrDiscoveryConfig { } } -impl NostrDiscoveryConfig { +impl NostrRendezvousConfig { fn default_advertise() -> bool { true } @@ -1225,27 +1225,27 @@ owd_window_size: 48 } #[test] - fn test_nostr_discovery_startup_sweep_defaults() { - let c = NostrDiscoveryConfig::default(); + fn test_nostr_rendezvous_startup_sweep_defaults() { + let c = NostrRendezvousConfig::default(); assert_eq!(c.startup_sweep_delay_secs, 5); assert_eq!(c.startup_sweep_max_age_secs, 3_600); } #[test] - fn test_nostr_discovery_startup_sweep_yaml_override() { + fn test_nostr_rendezvous_startup_sweep_yaml_override() { let yaml = "enabled: true\npolicy: open\nstartup_sweep_delay_secs: 10\nstartup_sweep_max_age_secs: 1800\n"; - let c: NostrDiscoveryConfig = serde_yaml::from_str(yaml).unwrap(); + let c: NostrRendezvousConfig = serde_yaml::from_str(yaml).unwrap(); assert!(c.enabled); - assert_eq!(c.policy, NostrDiscoveryPolicy::Open); + assert_eq!(c.policy, NostrRendezvousPolicy::Open); assert_eq!(c.startup_sweep_delay_secs, 10); assert_eq!(c.startup_sweep_max_age_secs, 1_800); } #[test] - fn test_nostr_discovery_startup_sweep_partial_yaml_uses_defaults() { + fn test_nostr_rendezvous_startup_sweep_partial_yaml_uses_defaults() { // Only override delay; max_age should fall back to default. let yaml = "enabled: true\nstartup_sweep_delay_secs: 30\n"; - let c: NostrDiscoveryConfig = serde_yaml::from_str(yaml).unwrap(); + let c: NostrRendezvousConfig = serde_yaml::from_str(yaml).unwrap(); assert_eq!(c.startup_sweep_delay_secs, 30); assert_eq!(c.startup_sweep_max_age_secs, 3_600); } diff --git a/src/control/queries.rs b/src/control/queries.rs index 435b752..1c796e2 100644 --- a/src/control/queries.rs +++ b/src/control/queries.rs @@ -236,7 +236,7 @@ pub fn show_peers(node: &Node) -> Value { // Per-npub Nostr-traversal failure-state snapshot, indexed by npub // for O(1) per-peer lookup. Empty if Nostr discovery is disabled. let nostr_state: std::collections::HashMap = node - .nostr_discovery_handle() + .nostr_rendezvous_handle() .map(|d| { d.failure_state_snapshot() .into_iter() diff --git a/src/lib.rs b/src/lib.rs index 8a62bb4..83ed616 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,12 +12,13 @@ extern crate alloc; pub mod cache; pub mod config; pub mod control; -pub mod discovery; #[cfg(target_os = "linux")] pub mod gateway; pub mod identity; +pub mod mdns; pub mod node; pub mod noise; +pub mod nostr; pub mod peer; pub mod perf_profile; pub(crate) mod proto; @@ -39,8 +40,8 @@ pub use identity::{ pub use config::{Config, ConfigError, IdentityConfig, NymConfig, TorConfig, UdpConfig}; pub use upper::config::{DnsConfig, TunConfig}; -// Re-export discovery types -pub use discovery::{BootstrapHandoffResult, EstablishedTraversal}; +// Re-export nostr rendezvous handoff types +pub use nostr::{BootstrapHandoffResult, EstablishedTraversal, is_punch_packet}; // Re-export tree types (relocated from tree:: to proto::stp) pub use proto::stp::{ diff --git a/src/discovery/lan/mod.rs b/src/mdns/mod.rs similarity index 92% rename from src/discovery/lan/mod.rs rename to src/mdns/mod.rs index e82b2a7..4da3ebb 100644 --- a/src/discovery/lan/mod.rs +++ b/src/mdns/mod.rs @@ -54,8 +54,12 @@ pub const TXT_KEY_SCOPE: &str = "scope"; /// `PROTOCOL_VERSION`). pub const TXT_KEY_VERSION: &str = "v"; +/// FIPS protocol version advertised in the mDNS TXT `v` key. Kept in sync +/// with the Nostr rendezvous `PROTOCOL_VERSION` (same value, `"1"`). +const TXT_PROTOCOL_VERSION: &str = "1"; + #[derive(Debug, Error)] -pub enum LanDiscoveryError { +pub enum LanRendezvousError { #[error("mDNS daemon init failed: {0}")] Daemon(String), #[error("mDNS register failed: {0}")] @@ -79,7 +83,7 @@ pub struct LanDiscoveredPeer { pub observed_at: Instant, } -/// Browser-side events surfaced by `LanDiscovery::drain_events`. +/// Browser-side events surfaced by `LanRendezvous::drain_events`. #[derive(Debug, Clone)] pub enum LanEvent { Discovered(LanDiscoveredPeer), @@ -87,17 +91,17 @@ pub enum LanEvent { /// Runtime configuration for the mDNS responder + browser. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct LanDiscoveryConfig { +pub struct LanRendezvousConfig { /// Master switch. Default: `false` — LAN discovery is opt-in. Operators /// who want sub-second same-LAN pairing enable it via /// `node.rendezvous.lan.enabled: true`. Default-off avoids reintroducing /// a per-LAN identity broadcast on nodes that have deliberately disabled /// other discovery channels, and avoids any multicast surprise on upgrade. - #[serde(default = "LanDiscoveryConfig::default_enabled")] + #[serde(default = "LanRendezvousConfig::default_enabled")] pub enabled: bool, /// Overridable service type, primarily so integration tests can run /// multiple isolated services on the same loopback interface. - #[serde(default = "LanDiscoveryConfig::default_service_type")] + #[serde(default = "LanRendezvousConfig::default_service_type")] pub service_type: String, /// Optional application/network scope carried in the LAN-only TXT /// record. Browsers that set a scope ignore adverts for other scopes. @@ -109,7 +113,7 @@ pub struct LanDiscoveryConfig { pub scope: Option, } -impl Default for LanDiscoveryConfig { +impl Default for LanRendezvousConfig { fn default() -> Self { Self { enabled: Self::default_enabled(), @@ -119,7 +123,7 @@ impl Default for LanDiscoveryConfig { } } -impl LanDiscoveryConfig { +impl LanRendezvousConfig { fn default_enabled() -> bool { false } @@ -129,7 +133,7 @@ impl LanDiscoveryConfig { } /// Running mDNS responder + browser bound to the node's UDP advert port. -pub struct LanDiscovery { +pub struct LanRendezvous { daemon: ServiceDaemon, own_npub: String, instance_fullname: String, @@ -137,7 +141,7 @@ pub struct LanDiscovery { event_pump: tokio::task::JoinHandle<()>, } -impl LanDiscovery { +impl LanRendezvous { /// Start the mDNS responder and browser. /// /// `advertised_port` is the UDP port the operational UDP transport @@ -148,16 +152,16 @@ impl LanDiscovery { identity: &Identity, scope: Option, advertised_port: u16, - config: LanDiscoveryConfig, - ) -> Result, LanDiscoveryError> { + config: LanRendezvousConfig, + ) -> Result, LanRendezvousError> { if !config.enabled { - return Err(LanDiscoveryError::Disabled); + return Err(LanRendezvousError::Disabled); } if advertised_port == 0 { - return Err(LanDiscoveryError::NoAdvertisedPort); + return Err(LanRendezvousError::NoAdvertisedPort); } - let daemon = ServiceDaemon::new().map_err(|e| LanDiscoveryError::Daemon(e.to_string()))?; + let daemon = ServiceDaemon::new().map_err(|e| LanRendezvousError::Daemon(e.to_string()))?; let npub = identity.npub(); // mDNS DNS labels are capped at 63 bytes. 16 bech32 chars of npub @@ -176,7 +180,7 @@ impl LanDiscovery { } props.insert( TXT_KEY_VERSION.to_string(), - super::nostr::PROTOCOL_VERSION.to_string(), + TXT_PROTOCOL_VERSION.to_string(), ); // host_ipv4 is set to "127.0.0.1" *and* enable_addr_auto() is @@ -193,18 +197,18 @@ impl LanDiscovery { advertised_port, Some(props), ) - .map_err(|e| LanDiscoveryError::Register(e.to_string()))? + .map_err(|e| LanRendezvousError::Register(e.to_string()))? .enable_addr_auto(); let instance_fullname = service_info.get_fullname().to_string(); daemon .register(service_info) - .map_err(|e| LanDiscoveryError::Register(e.to_string()))?; + .map_err(|e| LanRendezvousError::Register(e.to_string()))?; let browse_rx = daemon .browse(&config.service_type) - .map_err(|e| LanDiscoveryError::Browse(e.to_string()))?; + .map_err(|e| LanRendezvousError::Browse(e.to_string()))?; let (events_tx, events_rx) = tokio::sync::mpsc::unbounded_channel(); let own_npub = npub.clone(); diff --git a/src/discovery/lan/tests.rs b/src/mdns/tests.rs similarity index 91% rename from src/discovery/lan/tests.rs rename to src/mdns/tests.rs index 1e4867c..5983abe 100644 --- a/src/discovery/lan/tests.rs +++ b/src/mdns/tests.rs @@ -4,7 +4,7 @@ use std::time::Duration; use crate::Identity; use mdns_sd::ScopedIp; -use super::{LanDiscovery, LanDiscoveryConfig, LanEvent}; +use super::{LanEvent, LanRendezvous, LanRendezvousConfig}; /// Distinct service type per test run so concurrent cargo-test workers /// on the same machine don't cross-feed each other's adverts via the @@ -15,8 +15,8 @@ fn isolated_service_type(tag: &str) -> String { format!("_fipstest-{tag}-{rand:08x}._udp.local.") } -fn config_for(service_type: String) -> LanDiscoveryConfig { - LanDiscoveryConfig { +fn config_for(service_type: String) -> LanRendezvousConfig { + LanRendezvousConfig { enabled: true, service_type, scope: None, @@ -47,7 +47,7 @@ fn non_link_local_ipv6_advert_is_preserved() { } async fn wait_for_peer( - discovery: &LanDiscovery, + discovery: &LanRendezvous, expected_npub: &str, timeout: Duration, ) -> Option { @@ -64,7 +64,7 @@ async fn wait_for_peer( None } -/// Two LanDiscovery instances on isolated service types — `a` browses +/// Two LanRendezvous instances on isolated service types — `a` browses /// only its own type and never sees `b`, and vice versa. Sanity check /// that the scope-isolation defense works (we'd lose isolation if mdns- /// sd ever leaked across service types). @@ -76,7 +76,7 @@ async fn isolated_service_types_do_not_cross_feed() { let service_a = isolated_service_type("isolated-a"); let service_b = isolated_service_type("isolated-b"); - let lan_a = LanDiscovery::start( + let lan_a = LanRendezvous::start( &identity_a, Some("scope-x".to_string()), 61001, @@ -84,7 +84,7 @@ async fn isolated_service_types_do_not_cross_feed() { ) .await .expect("start a"); - let lan_b = LanDiscovery::start( + let lan_b = LanRendezvous::start( &identity_b, Some("scope-x".to_string()), 61002, @@ -118,7 +118,7 @@ async fn isolated_service_types_do_not_cross_feed() { assert!(!saw_a_from_b, "isolated service types must not cross-feed"); } -/// Two LanDiscovery instances on the same service type and the same +/// Two LanRendezvous instances on the same service type and the same /// scope: each should observe the other's advert within a few seconds. /// Exercises the responder + browser + TXT plumbing end-to-end. /// @@ -136,7 +136,7 @@ async fn matched_scope_peers_observe_each_other() { let service = isolated_service_type("matched"); - let lan_a = LanDiscovery::start( + let lan_a = LanRendezvous::start( &identity_a, Some("scope-shared".to_string()), 61101, @@ -144,7 +144,7 @@ async fn matched_scope_peers_observe_each_other() { ) .await .expect("start a"); - let lan_b = LanDiscovery::start( + let lan_b = LanRendezvous::start( &identity_b, Some("scope-shared".to_string()), 61102, @@ -181,7 +181,7 @@ async fn cross_scope_advert_is_filtered() { let service = isolated_service_type("cross-scope"); - let lan_a = LanDiscovery::start( + let lan_a = LanRendezvous::start( &identity_a, Some("scope-a".to_string()), 61201, @@ -189,7 +189,7 @@ async fn cross_scope_advert_is_filtered() { ) .await .expect("start a"); - let lan_b = LanDiscovery::start( + let lan_b = LanRendezvous::start( &identity_b, Some("scope-b".to_string()), 61202, diff --git a/src/node/handlers/rx_loop.rs b/src/node/handlers/rx_loop.rs index 525550f..f7b3288 100644 --- a/src/node/handlers/rx_loop.rs +++ b/src/node/handlers/rx_loop.rs @@ -257,13 +257,13 @@ impl Node { // is polled separately from `reload_peer_acl` because the // ACL's embedded alias reloader and this snapshot are // distinct resources; the `path_mtu_lookup` cache and the - // `nostr_discovery` subsystem are deliberately excluded + // `nostr_rendezvous` subsystem are deliberately excluded // from `Reloadable` since neither reloads from a backing // file (see `node::reloadable`). self.reload_host_map().await; self.poll_pending_connects().await; - self.poll_nostr_discovery().await; - self.poll_lan_discovery().await; + self.poll_nostr_rendezvous().await; + self.poll_lan_rendezvous().await; self.resend_pending_handshakes(now_ms).await; self.resend_pending_rekeys(now_ms).await; self.resend_pending_session_handshakes(now_ms).await; @@ -324,7 +324,7 @@ impl Node { .bootstrap_transport_npubs .get(&packet.transport_id) .cloned() - && let Some(handle) = self.nostr_discovery_handle() + && let Some(handle) = self.nostr_rendezvous_handle() { let now_ms = Self::now_ms(); let cooldown_secs = handle.protocol_mismatch_cooldown_secs(); diff --git a/src/node/lifecycle.rs b/src/node/lifecycle.rs index 0b2a0bd..bfd97e1 100644 --- a/src/node/lifecycle.rs +++ b/src/node/lifecycle.rs @@ -2,13 +2,13 @@ use super::{Node, NodeError, NodeState}; use crate::config::{ConnectPolicy, PeerAddress, PeerConfig}; -use crate::discovery::nostr::{ - ADVERT_IDENTIFIER, ADVERT_VERSION, BootstrapEvent, NostrDiscovery, OverlayAdvert, - OverlayEndpointAdvert, OverlayTransportKind, -}; -use crate::discovery::{BootstrapHandoffResult, EstablishedTraversal}; use crate::node::acl::PeerAclContext; use crate::node::wire::build_msg1; +use crate::nostr::{ + ADVERT_IDENTIFIER, ADVERT_VERSION, BootstrapEvent, NostrRendezvous, OverlayAdvert, + OverlayEndpointAdvert, OverlayTransportKind, +}; +use crate::nostr::{BootstrapHandoffResult, EstablishedTraversal}; use crate::peer::PeerConnection; use crate::proto::fmp::{Disconnect, DisconnectReason}; use crate::transport::{Link, LinkDirection, LinkId, TransportAddr, TransportId, packet_channel}; @@ -266,7 +266,7 @@ impl Node { // would loop on the same dead address until expiry. Force a // re-fetch so the next retry tick picks up fresh endpoints. if matches!(e, crate::node::NodeError::NoTransportForType(_)) - && let Some(bootstrap) = self.nostr_discovery.clone() + && let Some(bootstrap) = self.nostr_rendezvous.clone() { let npub = peer_config.npub.clone(); tokio::spawn(async move { @@ -679,8 +679,8 @@ impl Node { } } - pub(super) async fn poll_nostr_discovery(&mut self) { - let Some(bootstrap) = self.nostr_discovery.clone() else { + pub(super) async fn poll_nostr_rendezvous(&mut self) { + let Some(bootstrap) = self.nostr_rendezvous.clone() else { return; }; @@ -826,19 +826,19 @@ impl Node { tokio::spawn(async move { let outcome = bootstrap.refetch_advert_for_stale_check(&npub).await; match outcome { - crate::discovery::nostr::NostrRefetchOutcome::Evicted => info!( + crate::nostr::NostrRefetchOutcome::Evicted => info!( npub = %npub, "stale-advert sweep: peer evicted from advert cache" ), - crate::discovery::nostr::NostrRefetchOutcome::Refreshed => info!( + crate::nostr::NostrRefetchOutcome::Refreshed => info!( npub = %npub, "stale-advert sweep: peer republished, cache refreshed and streak reset" ), - crate::discovery::nostr::NostrRefetchOutcome::SameAdvert => debug!( + crate::nostr::NostrRefetchOutcome::SameAdvert => debug!( npub = %npub, "stale-advert sweep: advert unchanged, cooldown stands" ), - crate::discovery::nostr::NostrRefetchOutcome::Skipped => debug!( + crate::nostr::NostrRefetchOutcome::Skipped => debug!( npub = %npub, "stale-advert sweep: skipped (relay error or no advert_relays)" ), @@ -877,7 +877,7 @@ impl Node { /// changing the public Nostr discovery `app` tag. The older fallback /// extracts a scope from the Nostr app tag used by default scoped /// discovery. - pub(super) fn lan_discovery_scope(&self) -> Option { + pub(super) fn lan_rendezvous_scope(&self) -> Option { if let Some(scope) = self.config().node.rendezvous.lan.scope.as_deref() { let scope = scope.trim(); if !scope.is_empty() { @@ -904,8 +904,8 @@ impl Node { /// Drain mDNS-discovered peers and initiate Noise IK handshakes. /// The handshake itself is the authentication — a spoofed mDNS advert /// with someone else's npub fails the IK exchange and is dropped. - pub(super) async fn poll_lan_discovery(&mut self) { - let Some(runtime) = self.lan_discovery.clone() else { + pub(super) async fn poll_lan_rendezvous(&mut self) { + let Some(runtime) = self.lan_rendezvous.clone() else { return; }; let events = runtime.drain_events().await; @@ -913,7 +913,7 @@ impl Node { return; } for event in events { - let crate::discovery::lan::LanEvent::Discovered(peer) = event; + let crate::mdns::LanEvent::Discovered(peer) = event; let Some((transport_id, local_addr)) = self.find_udp_transport_for_remote_addr(peer.addr) else { @@ -1139,7 +1139,7 @@ impl Node { } if self.config().node.rendezvous.nostr.enabled { - match NostrDiscovery::start( + match NostrRendezvous::start( self.identity(), self.config().node.rendezvous.nostr.clone(), ) @@ -1149,8 +1149,8 @@ impl Node { if let Err(err) = self.refresh_overlay_advert(&runtime).await { warn!(error = %err, "Failed to publish initial Nostr overlay advert"); } - self.nostr_discovery = Some(runtime); - self.nostr_discovery_started_at_ms = Some(Self::now_ms()); + self.nostr_rendezvous = Some(runtime); + self.nostr_rendezvous_started_at_ms = Some(Self::now_ms()); info!("Nostr overlay discovery enabled"); } Err(err) => { @@ -1181,8 +1181,8 @@ impl Node { .min_by_key(|(id, _)| id.as_u32()) .map(|(_, port)| port) .unwrap_or(0); - let scope = self.lan_discovery_scope(); - match crate::discovery::lan::LanDiscovery::start( + let scope = self.lan_rendezvous_scope(); + match crate::mdns::LanRendezvous::start( self.identity(), scope, advertised_udp_port, @@ -1191,7 +1191,7 @@ impl Node { .await { Ok(runtime) => { - self.lan_discovery = Some(runtime); + self.lan_rendezvous = Some(runtime); info!("LAN mDNS discovery enabled"); } Err(err) => { @@ -1479,7 +1479,7 @@ impl Node { .await; // Stop Nostr overlay discovery background work and withdraw any advert. - if let Some(bootstrap) = self.nostr_discovery.take() + if let Some(bootstrap) = self.nostr_rendezvous.take() && let Err(e) = bootstrap.shutdown().await { warn!(error = %e, "Failed to shutdown Nostr overlay discovery"); @@ -1488,7 +1488,7 @@ impl Node { // Tear down LAN mDNS responder + browser. Best-effort: the // OS will eventually time the advert out via its TTL even if // we don't get a clean unregister out before the daemon exits. - if let Some(lan) = self.lan_discovery.take() { + if let Some(lan) = self.lan_rendezvous.take() { lan.shutdown().await; } @@ -1629,12 +1629,12 @@ impl Node { if !self.config().node.rendezvous.nostr.enabled || !peer_config.via_nostr || self.config().node.rendezvous.nostr.policy - == crate::config::NostrDiscoveryPolicy::Disabled + == crate::config::NostrRendezvousPolicy::Disabled { return Vec::new(); } - let Some(bootstrap) = self.nostr_discovery.clone() else { + let Some(bootstrap) = self.nostr_rendezvous.clone() else { return Vec::new(); }; let endpoints = match bootstrap.advert_endpoints_for_peer(&peer_config.npub).await { @@ -1695,7 +1695,7 @@ impl Node { } async fn request_nostr_bootstrap(&self, peer_config: &PeerConfig) -> bool { - let Some(bootstrap) = self.nostr_discovery.clone() else { + let Some(bootstrap) = self.nostr_rendezvous.clone() else { debug!(npub = %peer_config.npub, "No Nostr overlay runtime for udp:nat address"); return false; }; @@ -1831,7 +1831,7 @@ impl Node { ))) } - async fn queue_open_discovery_retries(&mut self, bootstrap: &std::sync::Arc) { + async fn queue_open_discovery_retries(&mut self, bootstrap: &std::sync::Arc) { self.run_open_discovery_sweep(bootstrap, None, "per-tick") .await; } @@ -1848,13 +1848,13 @@ impl Node { /// startup sweeps are distinguishable in operator-facing logs. pub(in crate::node) async fn run_open_discovery_sweep( &mut self, - bootstrap: &std::sync::Arc, + bootstrap: &std::sync::Arc, max_age_secs: Option, caller: &'static str, ) { if !self.config().node.rendezvous.nostr.enabled || self.config().node.rendezvous.nostr.policy - != crate::config::NostrDiscoveryPolicy::Open + != crate::config::NostrRendezvousPolicy::Open { return; } @@ -2046,20 +2046,20 @@ impl Node { /// `node.rendezvous.nostr.enabled` and `policy == open`. async fn maybe_run_startup_open_discovery_sweep( &mut self, - bootstrap: &std::sync::Arc, + bootstrap: &std::sync::Arc, ) { if self.startup_open_discovery_sweep_done { return; } if !self.config().node.rendezvous.nostr.enabled || self.config().node.rendezvous.nostr.policy - != crate::config::NostrDiscoveryPolicy::Open + != crate::config::NostrRendezvousPolicy::Open { // Mark done so we don't keep re-checking on every tick. self.startup_open_discovery_sweep_done = true; return; } - let Some(started_at_ms) = self.nostr_discovery_started_at_ms else { + let Some(started_at_ms) = self.nostr_rendezvous_started_at_ms else { return; }; let now_ms = Self::now_ms(); @@ -2192,7 +2192,7 @@ impl Node { async fn build_overlay_advert( &self, - bootstrap: &std::sync::Arc, + bootstrap: &std::sync::Arc, ) -> Option { if !self.config().node.rendezvous.nostr.enabled { return None; @@ -2343,8 +2343,8 @@ impl Node { async fn refresh_overlay_advert( &self, - bootstrap: &std::sync::Arc, - ) -> Result<(), crate::discovery::nostr::BootstrapError> { + bootstrap: &std::sync::Arc, + ) -> Result<(), crate::nostr::BootstrapError> { let advert = self.build_overlay_advert(bootstrap).await; bootstrap.update_local_advert(advert).await } diff --git a/src/node/mod.rs b/src/node/mod.rs index cdaa1d3..14c7fc0 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -445,17 +445,17 @@ pub struct Node { retry_pending: HashMap, /// Optional Nostr/STUN overlay discovery coordinator for `udp:nat` peers. - nostr_discovery: Option>, + nostr_rendezvous: Option>, /// mDNS / DNS-SD responder + browser for local-link peer discovery. /// Identity is unverified at this layer — the Noise XX handshake /// initiated against an mDNS-observed endpoint is what proves the /// peer holds the matching private key. - lan_discovery: Option>, + lan_rendezvous: Option>, /// Wall-clock ms when Nostr discovery successfully started, used to /// schedule the one-shot startup advert sweep after a settle delay. /// `None` until discovery comes up; remains `None` if discovery is /// disabled or failed to start. - nostr_discovery_started_at_ms: Option, + nostr_rendezvous_started_at_ms: Option, /// Whether the one-shot startup advert sweep has run. Set to true /// after the first sweep fires (under `policy: open`); thereafter /// only the per-tick `queue_open_discovery_retries` continues. @@ -684,9 +684,9 @@ impl Node { ), pending_connects: Vec::new(), retry_pending: HashMap::new(), - nostr_discovery: None, - nostr_discovery_started_at_ms: None, - lan_discovery: None, + nostr_rendezvous: None, + nostr_rendezvous_started_at_ms: None, + lan_rendezvous: None, startup_open_discovery_sweep_done: false, bootstrap_transports: HashSet::new(), bootstrap_transport_npubs: HashMap::new(), @@ -846,9 +846,9 @@ impl Node { lookup: Lookup::new(LookupBackoff::new(), LookupForwardRateLimiter::new()), pending_connects: Vec::new(), retry_pending: HashMap::new(), - nostr_discovery: None, - nostr_discovery_started_at_ms: None, - lan_discovery: None, + nostr_rendezvous: None, + nostr_rendezvous_started_at_ms: None, + lan_rendezvous: None, startup_open_discovery_sweep_done: false, bootstrap_transports: HashSet::new(), bootstrap_transport_npubs: HashMap::new(), @@ -1834,7 +1834,7 @@ impl Node { // Per-npub Nostr-traversal failure-state, indexed by npub for O(1) // per-peer lookup (empty when Nostr discovery is disabled). let nostr_state: std::collections::HashMap = self - .nostr_discovery_handle() + .nostr_rendezvous_handle() .map(|d| { d.failure_state_snapshot() .into_iter() @@ -2398,8 +2398,8 @@ impl Node { /// Reference to the Nostr discovery handle if discovery is enabled. /// Used by control queries (`show_peers` per-peer Nostr-traversal /// state) to read failure-state without taking shared ownership. - pub fn nostr_discovery_handle(&self) -> Option<&crate::discovery::nostr::NostrDiscovery> { - self.nostr_discovery.as_deref() + pub fn nostr_rendezvous_handle(&self) -> Option<&crate::nostr::NostrRendezvous> { + self.nostr_rendezvous.as_deref() } /// Iterate over all peer node IDs. diff --git a/src/node/reloadable.rs b/src/node/reloadable.rs index 6013110..1bd90cb 100644 --- a/src/node/reloadable.rs +++ b/src/node/reloadable.rs @@ -41,7 +41,7 @@ //! file. There is nothing to poll. (Its read side could adopt the same //! lock-free `ArcSwap` shape in the future, but that is an optimization, not //! a reload.) -//! - `nostr_discovery` is an async spawned subsystem, not a snapshot of disk +//! - `nostr_rendezvous` is an async spawned subsystem, not a snapshot of disk //! state. //! //! Both [`HostMapReloadable`] and the peer ACL reloader currently stat diff --git a/src/node/retry.rs b/src/node/retry.rs index cfafe2d..9fffdb5 100644 --- a/src/node/retry.rs +++ b/src/node/retry.rs @@ -282,7 +282,7 @@ impl Node { // evicts if the relay has nothing, otherwise leaves it. Cheap // (one Filter fetch with 2s timeout) and bounded by the retry // backoff cadence. - if let Some(bootstrap) = self.nostr_discovery.clone() { + if let Some(bootstrap) = self.nostr_rendezvous.clone() { let _ = bootstrap .refetch_advert_for_stale_check(&peer_config.npub) .await; @@ -318,7 +318,7 @@ impl Node { // entry expires. Force a re-fetch so the next retry tick // picks up fresh endpoints. if matches!(e, NodeError::NoTransportForType(_)) - && let Some(bootstrap) = self.nostr_discovery.clone() + && let Some(bootstrap) = self.nostr_rendezvous.clone() { let npub = peer_config.npub.clone(); tokio::spawn(async move { diff --git a/src/node/tests/discovery.rs b/src/node/tests/discovery.rs index 0bb55e6..8ad42ba 100644 --- a/src/node/tests/discovery.rs +++ b/src/node/tests/discovery.rs @@ -921,20 +921,20 @@ async fn test_originator_stores_path_mtu_in_cache() { /// Pin the iterate-filter-queue contract of `run_open_discovery_sweep`. /// /// Builds a `Node` with `nostr.policy = Open` and an empty peer list, -/// then injects three cached adverts into a test `NostrDiscovery` and +/// then injects three cached adverts into a test `NostrRendezvous` and /// asserts the sweep: /// - queues a retry for an eligible (unknown, not-self) advert, /// - skips the advert whose author is our own node identity, and /// - skips the advert whose author is an already-connected peer. /// -/// Uses `NostrDiscovery::new_for_test()` and `insert_advert_for_test()` +/// Uses `NostrRendezvous::new_for_test()` and `insert_advert_for_test()` /// (both `#[cfg(test)]`-gated test escape hatches in /// `src/discovery/nostr/runtime.rs`) to populate the cache without /// requiring live relay subscriptions. #[tokio::test] async fn test_open_discovery_sweep_queues_eligible_skips_filtered() { - use crate::config::NostrDiscoveryPolicy; - use crate::discovery::nostr::{NostrDiscovery, OverlayEndpointAdvert, OverlayTransportKind}; + use crate::config::NostrRendezvousPolicy; + use crate::nostr::{NostrRendezvous, OverlayEndpointAdvert, OverlayTransportKind}; use crate::peer::ActivePeer; use crate::transport::LinkId; use std::sync::Arc; @@ -942,7 +942,7 @@ async fn test_open_discovery_sweep_queues_eligible_skips_filtered() { // Build node with open-discovery enabled. let mut config = crate::Config::new(); config.node.rendezvous.nostr.enabled = true; - config.node.rendezvous.nostr.policy = NostrDiscoveryPolicy::Open; + config.node.rendezvous.nostr.policy = NostrRendezvousPolicy::Open; let mut node = crate::Node::new(config).unwrap(); // Identity of an already-connected peer; insert into node.peers @@ -965,8 +965,8 @@ async fn test_open_discovery_sweep_queues_eligible_skips_filtered() { let self_npub = crate::encode_npub(&node.identity().pubkey()); let self_node_addr = *node.identity().node_addr(); - // Build a NostrDiscovery test instance and inject the three adverts. - let bootstrap = Arc::new(NostrDiscovery::new_for_test()); + // Build a NostrRendezvous test instance and inject the three adverts. + let bootstrap = Arc::new(NostrRendezvous::new_for_test()); let endpoint = OverlayEndpointAdvert { transport: OverlayTransportKind::Udp, addr: "203.0.113.7:2121".to_string(), @@ -977,7 +977,7 @@ async fn test_open_discovery_sweep_queues_eligible_skips_filtered() { .unwrap_or(0); for npub in [&eligible_npub, &connected_npub, &self_npub] { let advert = - NostrDiscovery::cached_advert_for_test(npub.clone(), endpoint.clone(), now_secs); + NostrRendezvous::cached_advert_for_test(npub.clone(), endpoint.clone(), now_secs); bootstrap.insert_advert_for_test(npub.clone(), advert).await; } diff --git a/src/node/tests/unit.rs b/src/node/tests/unit.rs index d447f33..6408dab 100644 --- a/src/node/tests/unit.rs +++ b/src/node/tests/unit.rs @@ -1,5 +1,5 @@ use super::*; -use crate::discovery::nostr::{BootstrapEvent, NostrDiscovery}; +use crate::nostr::{BootstrapEvent, NostrRendezvous}; use crate::peer::PromotionResult; use crate::transport::udp::UdpTransport; use crate::transport::{TransportHandle, packet_channel}; @@ -171,7 +171,7 @@ async fn test_node_start_does_not_wait_for_nostr_relay_startup() { config.node.control.enabled = false; config.node.rendezvous.nostr.enabled = true; config.node.rendezvous.nostr.advertise = true; - config.node.rendezvous.nostr.policy = crate::config::NostrDiscoveryPolicy::Open; + config.node.rendezvous.nostr.policy = crate::config::NostrRendezvousPolicy::Open; config.node.rendezvous.nostr.advert_relays = vec!["wss://127.0.0.1:9".to_string()]; config.node.rendezvous.nostr.dm_relays = vec!["wss://127.0.0.1:9".to_string()]; config.transports.udp = crate::config::TransportInstances::Single(crate::config::UdpConfig { @@ -189,7 +189,7 @@ async fn test_node_start_does_not_wait_for_nostr_relay_startup() { .unwrap(); assert!(node.is_running()); - assert!(node.nostr_discovery_handle().is_some()); + assert!(node.nostr_rendezvous_handle().is_some()); node.stop().await.unwrap(); } @@ -1125,14 +1125,14 @@ async fn test_nostr_traversal_failure_skips_connected_peer() { node.promote_connection(link_id, peer_identity, 2000) .unwrap(); - let bootstrap = Arc::new(NostrDiscovery::new_for_test()); + let bootstrap = Arc::new(NostrRendezvous::new_for_test()); bootstrap.push_event_for_test(BootstrapEvent::Failed { peer_config: crate::config::PeerConfig::new(peer_identity.npub(), "udp", "127.0.0.1:9"), reason: "stale traversal failure".to_string(), }); - node.nostr_discovery = Some(bootstrap.clone()); + node.nostr_rendezvous = Some(bootstrap.clone()); - node.poll_nostr_discovery().await; + node.poll_nostr_rendezvous().await; assert!( bootstrap.failure_state_snapshot().is_empty(), @@ -1146,7 +1146,7 @@ async fn test_nostr_traversal_failure_skips_connected_peer() { #[tokio::test] async fn test_nostr_traversal_established_skips_connected_peer() { - use crate::discovery::EstablishedTraversal; + use crate::nostr::EstablishedTraversal; use std::net::UdpSocket; let mut node = make_node(); @@ -1159,7 +1159,7 @@ async fn test_nostr_traversal_established_skips_connected_peer() { let link_count = node.link_count(); let connection_count = node.connection_count(); - let bootstrap = Arc::new(NostrDiscovery::new_for_test()); + let bootstrap = Arc::new(NostrRendezvous::new_for_test()); let socket = UdpSocket::bind("127.0.0.1:0").expect("bind local UDP socket"); let remote_addr = "127.0.0.1:9999".parse().expect("parse remote addr"); bootstrap.push_event_for_test(BootstrapEvent::Established { @@ -1170,9 +1170,9 @@ async fn test_nostr_traversal_established_skips_connected_peer() { socket, ), }); - node.nostr_discovery = Some(bootstrap.clone()); + node.nostr_rendezvous = Some(bootstrap.clone()); - node.poll_nostr_discovery().await; + node.poll_nostr_rendezvous().await; assert_eq!( node.link_count(), @@ -1708,14 +1708,14 @@ async fn process_pending_retries_gated_at_capacity() { } #[tokio::test] -async fn poll_nostr_discovery_established_gated_at_capacity() { - use crate::discovery::EstablishedTraversal; +async fn poll_nostr_rendezvous_established_gated_at_capacity() { + use crate::nostr::EstablishedTraversal; use std::net::UdpSocket; let mut node = make_node_with_max_peers(2); inject_dummy_peers(&mut node, 2); - let bootstrap = Arc::new(NostrDiscovery::new_for_test()); + let bootstrap = Arc::new(NostrRendezvous::new_for_test()); let socket = UdpSocket::bind("127.0.0.1:0").expect("bind local UDP socket"); let remote_addr = "127.0.0.1:9999".parse().expect("parse remote addr"); let peer_identity = Identity::generate(); @@ -1727,13 +1727,13 @@ async fn poll_nostr_discovery_established_gated_at_capacity() { socket, ), }); - node.nostr_discovery = Some(bootstrap.clone()); + node.nostr_rendezvous = Some(bootstrap.clone()); let before_peers = node.peer_count(); let before_links = node.link_count(); let before_connections = node.connection_count(); - node.poll_nostr_discovery().await; + node.poll_nostr_rendezvous().await; assert_eq!( node.peer_count(), @@ -1753,11 +1753,11 @@ async fn poll_nostr_discovery_established_gated_at_capacity() { } #[test] -fn nostr_discovery_outbound_admission_atomic_roundtrip() { +fn nostr_rendezvous_outbound_admission_atomic_roundtrip() { // Verifies the runtime-side plumbing for the two NAT-traversal gate // points: the setter mutates the atomic and the (super-visible) // reader observes the value the Node-side wiring would publish. - let bootstrap = NostrDiscovery::new_for_test(); + let bootstrap = NostrRendezvous::new_for_test(); assert!( bootstrap.outbound_admission_allowed(), "default must allow (start unsaturated)" diff --git a/src/discovery/nostr/failure_state.rs b/src/nostr/failure_state.rs similarity index 100% rename from src/discovery/nostr/failure_state.rs rename to src/nostr/failure_state.rs diff --git a/src/discovery.rs b/src/nostr/handoff.rs similarity index 94% rename from src/discovery.rs rename to src/nostr/handoff.rs index 3612fb5..2ee4028 100644 --- a/src/discovery.rs +++ b/src/nostr/handoff.rs @@ -6,9 +6,6 @@ //! it hands the live socket and selected remote endpoint to FIPS so the //! existing Noise/FMP transport path can take over. -pub mod lan; -pub mod nostr; - use crate::config::UdpConfig; use crate::{NodeAddr, TransportId}; use std::net::{SocketAddr, UdpSocket}; @@ -16,9 +13,9 @@ use std::net::{SocketAddr, UdpSocket}; /// Punch-probe magic ("NPTC", network byte order). First byte `0x4E` /// collides with FMP's prefix-version high-nibble check, so the UDP /// transport silently filters packets carrying this magic to keep -/// post-adoption handshake logs clean. Defined at the top-level -/// `discovery` module so the UDP filter and the nostr submodule's -/// punch sender share the same constant. +/// post-adoption handshake logs clean. Defined in the nostr rendezvous +/// home so the UDP filter and the nostr submodule's punch sender share +/// the same constant. pub const PUNCH_MAGIC: u32 = 0x4E505443; /// Punch-probe-ack magic ("NPTA", network byte order). Same filter as diff --git a/src/discovery/nostr/mod.rs b/src/nostr/mod.rs similarity index 80% rename from src/discovery/nostr/mod.rs rename to src/nostr/mod.rs index 881fb2e..dcece0d 100644 --- a/src/discovery/nostr/mod.rs +++ b/src/nostr/mod.rs @@ -1,4 +1,5 @@ mod failure_state; +mod handoff; mod runtime; mod signal; mod stun; @@ -8,7 +9,8 @@ mod types; #[cfg(test)] mod tests; -pub use runtime::NostrDiscovery; +pub use handoff::{BootstrapHandoffResult, EstablishedTraversal, is_punch_packet}; +pub use runtime::NostrRendezvous; pub use types::{ ADVERT_IDENTIFIER, ADVERT_KIND, ADVERT_VERSION, BootstrapError, BootstrapEvent, CachedOverlayAdvert, NostrFailureDecision, NostrPeerFailureView, NostrRefetchOutcome, diff --git a/src/discovery/nostr/runtime.rs b/src/nostr/runtime.rs similarity index 99% rename from src/discovery/nostr/runtime.rs rename to src/nostr/runtime.rs index f1bb485..e2e1a16 100644 --- a/src/discovery/nostr/runtime.rs +++ b/src/nostr/runtime.rs @@ -17,6 +17,7 @@ use tokio::task::JoinHandle; use tracing::{debug, info, trace, warn}; use super::failure_state::FailureState; +use super::handoff::EstablishedTraversal; use super::signal::{ FreshnessOutcome, SignalEnvelope, build_signal_event, create_traversal_answer, create_traversal_offer, estimate_clock_skew, unwrap_signal_event, validate_offer_freshness, @@ -30,8 +31,7 @@ use super::types::{ OverlayAdvert, OverlayEndpointAdvert, PROTOCOL_VERSION, PunchHint, SIGNAL_KIND, TraversalAnswer, TraversalOffer, }; -use crate::config::{NostrDiscoveryConfig, PeerConfig}; -use crate::discovery::EstablishedTraversal; +use crate::config::{NostrRendezvousConfig, PeerConfig}; use crate::{NodeAddr, PeerIdentity}; const ADVERT_CACHE_STALE_GRACE_MULTIPLIER: u64 = 2; @@ -157,7 +157,7 @@ fn endpoint_advert_is_publicly_usable(endpoint: &OverlayEndpointAdvert) -> bool } /// Cached STUN-derived public address for an advert-eligible UDP transport -/// bound to a wildcard. Lives on `NostrDiscovery` so the freshness window +/// bound to a wildcard. Lives on `NostrRendezvous` so the freshness window /// survives advert refresh cycles. struct CachedPublicUdpAddr { /// Most recent STUN observation. `None` means the last attempt failed @@ -177,12 +177,12 @@ const PUBLIC_UDP_ADDR_FAILURE_TTL: Duration = Duration::from_secs(60); const RELAY_STARTUP_OP_TIMEOUT: Duration = Duration::from_secs(5); const ADVERT_PUBLISH_TIMEOUT: Duration = Duration::from_secs(10); -pub struct NostrDiscovery { +pub struct NostrRendezvous { client: Client, keys: nostr::Keys, pubkey: PublicKey, npub: String, - config: NostrDiscoveryConfig, + config: NostrRendezvousConfig, advert_cache: RwLock>, local_advert: RwLock>, current_advert_event_id: RwLock>, @@ -212,10 +212,10 @@ pub struct NostrDiscovery { outbound_admission: AtomicBool, } -impl NostrDiscovery { +impl NostrRendezvous { pub async fn start( identity: &crate::Identity, - config: NostrDiscoveryConfig, + config: NostrRendezvousConfig, ) -> Result, BootstrapError> { if !config.enabled { return Err(BootstrapError::Disabled); @@ -1733,8 +1733,8 @@ impl NostrDiscovery { } #[cfg(test)] -impl NostrDiscovery { - /// Build a minimal `NostrDiscovery` for unit tests. No relay client is +impl NostrRendezvous { + /// Build a minimal `NostrRendezvous` for unit tests. No relay client is /// connected and no background tasks are spawned; only the in-memory /// `advert_cache` and `npub` are usable. Intended for cache-injection /// tests of consumers (e.g. `Node::run_open_discovery_sweep`). @@ -1746,7 +1746,7 @@ impl NostrDiscovery { .signer(keys.clone()) .opts(ClientOptions::new().autoconnect(false)) .build(); - let config = NostrDiscoveryConfig::default(); + let config = NostrRendezvousConfig::default(); let offer_slots = Arc::new(Semaphore::new(config.max_concurrent_incoming_offers)); let (event_tx, event_rx) = mpsc::unbounded_channel(); let failure_state = FailureState::new( diff --git a/src/discovery/nostr/signal.rs b/src/nostr/signal.rs similarity index 100% rename from src/discovery/nostr/signal.rs rename to src/nostr/signal.rs diff --git a/src/discovery/nostr/stun.rs b/src/nostr/stun.rs similarity index 100% rename from src/discovery/nostr/stun.rs rename to src/nostr/stun.rs diff --git a/src/discovery/nostr/tests.rs b/src/nostr/tests.rs similarity index 96% rename from src/discovery/nostr/tests.rs rename to src/nostr/tests.rs index 355666f..4bda840 100644 --- a/src/discovery/nostr/tests.rs +++ b/src/nostr/tests.rs @@ -1,6 +1,6 @@ use nostr::prelude::{EventBuilder, Kind, Tag, Timestamp}; -use super::runtime::{NostrDiscovery, suppress_responder_for_own_initiator}; +use super::runtime::{NostrRendezvous, suppress_responder_for_own_initiator}; use super::signal::{ FreshnessOutcome, build_signal_event, create_traversal_answer, create_traversal_offer, estimate_clock_skew, validate_offer_freshness, validate_traversal_answer_for_offer, @@ -104,7 +104,7 @@ fn rejects_invalid_overlay_adverts() { signal_relays: None, stun_servers: None, }; - assert!(NostrDiscovery::validate_overlay_advert(missing_nat_metadata).is_err()); + assert!(NostrRendezvous::validate_overlay_advert(missing_nat_metadata).is_err()); let wrong_identifier = OverlayAdvert { identifier: "not-fips-overlay".to_string(), @@ -116,7 +116,7 @@ fn rejects_invalid_overlay_adverts() { signal_relays: None, stun_servers: None, }; - assert!(NostrDiscovery::validate_overlay_advert(wrong_identifier).is_err()); + assert!(NostrRendezvous::validate_overlay_advert(wrong_identifier).is_err()); } #[test] @@ -142,7 +142,7 @@ fn validate_overlay_advert_filters_unroutable_direct_endpoints() { stun_servers: None, }; - let validated = NostrDiscovery::validate_overlay_advert(advert).unwrap(); + let validated = NostrRendezvous::validate_overlay_advert(advert).unwrap(); assert_eq!(validated.endpoints.len(), 1); assert_eq!(validated.endpoints[0].addr, "8.8.8.8:443"); } @@ -166,7 +166,7 @@ fn validate_overlay_advert_rejects_only_unroutable_direct_endpoints() { stun_servers: None, }; - let err = NostrDiscovery::validate_overlay_advert(advert).unwrap_err(); + let err = NostrRendezvous::validate_overlay_advert(advert).unwrap_err(); assert!(err.to_string().contains("missing publicly routable")); } @@ -175,7 +175,7 @@ fn advert_freshness_rejects_expired_events() { let now_secs = Timestamp::now().as_secs(); let event = signed_overlay_advert_event(now_secs, Some(now_secs.saturating_sub(1))); let valid_until = - NostrDiscovery::compute_advert_valid_until_ms(&event, 600_000, now_secs * 1000); + NostrRendezvous::compute_advert_valid_until_ms(&event, 600_000, now_secs * 1000); assert!(valid_until.is_none()); } @@ -185,7 +185,7 @@ fn advert_freshness_rejects_stale_created_at_without_expiration() { let stale_created = now_secs.saturating_sub(10_000); let event = signed_overlay_advert_event(stale_created, None); let valid_until = - NostrDiscovery::compute_advert_valid_until_ms(&event, 600_000, now_secs * 1000); + NostrRendezvous::compute_advert_valid_until_ms(&event, 600_000, now_secs * 1000); assert!(valid_until.is_none()); } @@ -194,7 +194,7 @@ fn advert_freshness_uses_earliest_expiration_bound() { let now_secs = Timestamp::now().as_secs(); let event = signed_overlay_advert_event(now_secs.saturating_sub(10), Some(now_secs + 30)); let valid_until = - NostrDiscovery::compute_advert_valid_until_ms(&event, 3_600_000, now_secs * 1000) + NostrRendezvous::compute_advert_valid_until_ms(&event, 3_600_000, now_secs * 1000) .expect("event should be fresh"); assert_eq!(valid_until, (now_secs + 30) * 1000); } diff --git a/src/discovery/nostr/traversal.rs b/src/nostr/traversal.rs similarity index 100% rename from src/discovery/nostr/traversal.rs rename to src/nostr/traversal.rs diff --git a/src/discovery/nostr/types.rs b/src/nostr/types.rs similarity index 95% rename from src/discovery/nostr/types.rs rename to src/nostr/types.rs index 3d76419..00b6a39 100644 --- a/src/discovery/nostr/types.rs +++ b/src/nostr/types.rs @@ -1,14 +1,14 @@ +use super::handoff::EstablishedTraversal; use crate::config::PeerConfig; -use crate::discovery::EstablishedTraversal; use serde::{Deserialize, Serialize}; pub const ADVERT_KIND: u16 = 37195; pub const ADVERT_IDENTIFIER: &str = "fips-overlay-v1"; pub const ADVERT_VERSION: u32 = 1; pub const SIGNAL_KIND: u16 = 21059; -// Defined at the top-level `discovery` module; re-exported here so the +// Defined in the nostr `handoff` submodule; re-exported here so the // existing punch sender / receiver imports remain unchanged. -pub use crate::discovery::{PUNCH_ACK_MAGIC, PUNCH_MAGIC}; +pub use super::handoff::{PUNCH_ACK_MAGIC, PUNCH_MAGIC}; pub const PROTOCOL_VERSION: &str = "1"; #[derive(Debug, thiserror::Error)] @@ -189,7 +189,7 @@ pub struct PunchPacket { pub session_hash: [u8; 16], } -/// Outcome of `NostrDiscovery::record_traversal_failure`. +/// Outcome of `NostrRendezvous::record_traversal_failure`. #[derive(Debug, Clone, Copy)] pub struct NostrFailureDecision { pub consecutive_failures: u32, @@ -212,7 +212,7 @@ pub struct NostrPeerFailureView { pub last_observed_skew_ms: Option, } -/// Outcome of `NostrDiscovery::refetch_advert_for_stale_check` (B6). +/// Outcome of `NostrRendezvous::refetch_advert_for_stale_check` (B6). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum NostrRefetchOutcome { Evicted, diff --git a/src/transport/udp/mod.rs b/src/transport/udp/mod.rs index 3728fb9..780af5a 100644 --- a/src/transport/udp/mod.rs +++ b/src/transport/udp/mod.rs @@ -16,7 +16,7 @@ pub(crate) mod socket; mod stats; use super::resolve_socket_addr; use crate::config::UdpConfig; -use crate::discovery::is_punch_packet; +use crate::nostr::is_punch_packet; use socket::{AsyncUdpSocket, UdpRawSocket}; use stats::UdpStats; use std::collections::HashMap; @@ -960,7 +960,7 @@ mod tests { #[test] fn test_is_punch_packet_helper() { - use crate::discovery::is_punch_packet; + use crate::nostr::is_punch_packet; // PUNCH_MAGIC ("NPTC", be) assert!(is_punch_packet(&[0x4E, 0x50, 0x54, 0x43, 0xAA, 0xBB])); // PUNCH_ACK_MAGIC ("NPTA", be)