Move nostr peer rendezvous and mDNS into dedicated module homes

Relocate the overlay peer-rendezvous subsystem out of the overloaded
src/discovery/ tree into two focused, independent homes: src/nostr/
(relay-mediated overlay endpoint advertise/resolve/auto-mesh plus NAT
traversal) and src/mdns/ (link-local DNS-SD rendezvous). The two
subsystems are independent, so they get separate homes rather than
sharing one.

Drop the ambiguous "Discovery" stem from their identifiers in favor of
"Rendezvous": NostrDiscovery -> NostrRendezvous, LanDiscovery ->
LanRendezvous, and the matching config, policy, field, and method names.
The former src/discovery.rs handoff types (EstablishedTraversal,
BootstrapHandoffResult, the punch-packet helpers) fold into
src/nostr/handoff and stay reachable via the crate-root re-exports.

Pure relocation and rename: no logic, wire-format, config-key, metric,
or tracing-target changes. The operator-facing node.rendezvous.nostr.*
and node.rendezvous.lan.* config keys and the fips-overlay-v1 advert
namespace are byte-identical. cargo fmt/build/clippy clean; lib test
suite 1547 passing (baseline unchanged).
This commit is contained in:
Johnathan Corgan
2026-07-09 21:51:54 +00:00
parent 3b401a0cbd
commit 3f80530cc5
23 changed files with 193 additions and 189 deletions
+2 -2
View File
@@ -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!(
+41 -41
View File
@@ -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<u64>,
pub backoff_max_secs: Option<u64>,
pub forward_min_interval_secs: Option<u64>,
pub nostr: Option<NostrDiscoveryConfig>,
pub lan: Option<crate::discovery::lan::LanDiscoveryConfig>,
pub nostr: Option<NostrRendezvousConfig>,
pub lan: Option<crate::mdns::LanRendezvousConfig>,
}
/// 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<String>,
/// Relay URLs used for encrypted signaling events.
#[serde(default = "NostrDiscoveryConfig::default_dm_relays")]
#[serde(default = "NostrRendezvousConfig::default_dm_relays")]
pub dm_relays: Vec<String>,
/// 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<String>,
/// 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);
}
+1 -1
View File
@@ -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<String, _> = node
.nostr_discovery_handle()
.nostr_rendezvous_handle()
.map(|d| {
d.failure_state_snapshot()
.into_iter()
+4 -3
View File
@@ -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::{
+22 -18
View File
@@ -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<String>,
}
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<String>,
advertised_port: u16,
config: LanDiscoveryConfig,
) -> Result<Arc<Self>, LanDiscoveryError> {
config: LanRendezvousConfig,
) -> Result<Arc<Self>, 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();
@@ -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<super::LanDiscoveredPeer> {
@@ -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,
+4 -4
View File
@@ -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();
+36 -36
View File
@@ -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<String> {
pub(super) fn lan_rendezvous_scope(&self) -> Option<String> {
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<NostrDiscovery>) {
async fn queue_open_discovery_retries(&mut self, bootstrap: &std::sync::Arc<NostrRendezvous>) {
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<NostrDiscovery>,
bootstrap: &std::sync::Arc<NostrRendezvous>,
max_age_secs: Option<u64>,
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<NostrDiscovery>,
bootstrap: &std::sync::Arc<NostrRendezvous>,
) {
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<NostrDiscovery>,
bootstrap: &std::sync::Arc<NostrRendezvous>,
) -> Option<OverlayAdvert> {
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<NostrDiscovery>,
) -> Result<(), crate::discovery::nostr::BootstrapError> {
bootstrap: &std::sync::Arc<NostrRendezvous>,
) -> Result<(), crate::nostr::BootstrapError> {
let advert = self.build_overlay_advert(bootstrap).await;
bootstrap.update_local_advert(advert).await
}
+12 -12
View File
@@ -445,17 +445,17 @@ pub struct Node {
retry_pending: HashMap<NodeAddr, retry::RetryState>,
/// Optional Nostr/STUN overlay discovery coordinator for `udp:nat` peers.
nostr_discovery: Option<Arc<crate::discovery::nostr::NostrDiscovery>>,
nostr_rendezvous: Option<Arc<crate::nostr::NostrRendezvous>>,
/// 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<Arc<crate::discovery::lan::LanDiscovery>>,
lan_rendezvous: Option<Arc<crate::mdns::LanRendezvous>>,
/// 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<u64>,
nostr_rendezvous_started_at_ms: Option<u64>,
/// 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<String, _> = 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.
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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 {
+8 -8
View File
@@ -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;
}
+17 -17
View File
@@ -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)"
+3 -6
View File
@@ -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
@@ -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,
@@ -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<HashMap<String, CachedOverlayAdvert>>,
local_advert: RwLock<Option<OverlayAdvert>>,
current_advert_event_id: RwLock<Option<EventId>>,
@@ -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<Arc<Self>, 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(
@@ -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);
}
@@ -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<i64>,
}
/// 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,
+2 -2
View File
@@ -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)