mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 08:14:42 +00:00
Merge the nostr rendezvous reorg into the next line
Forward-merge the src/nostr / src/mdns rendezvous reorganization (relocation out of src/discovery/, the Discovery->Rendezvous rename, the RendezvousDriver consolidation, and the trace-target reference updates) onto the next branch. The sole conflict was in the LAN poll method's doc-comment and signature: kept next's XX-handshake wording and applied the lan_rendezvous rename. Merged tree builds clean; next lib suite 1583 passing, fmt/clippy clean.
This commit is contained in:
@@ -178,7 +178,7 @@ The resolution itself happens at debug-log level, so you will
|
||||
not see it in the default-level journal. The user-facing way to
|
||||
confirm everything worked is `fipsctl show peers` in the next
|
||||
step. (To watch the resolution in the journal, run the daemon
|
||||
manually with `RUST_LOG=fips::discovery::nostr=debug`; not
|
||||
manually with `RUST_LOG=fips::nostr=debug`; not
|
||||
necessary for this tutorial.)
|
||||
|
||||
## Step 5: Verify the resolved endpoint
|
||||
|
||||
+2
-2
@@ -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};
|
||||
@@ -1359,7 +1359,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
@@ -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
|
||||
}
|
||||
@@ -1241,27 +1241,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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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::{
|
||||
|
||||
@@ -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,
|
||||
@@ -259,13 +259,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_fmp_rekey_msg3(now_ms).await;
|
||||
@@ -322,12 +322,14 @@ impl Node {
|
||||
// though no msg1/msg2 exchange can ever succeed. Bump the
|
||||
// discovery-layer cooldown to the long protocol-mismatch
|
||||
// window and emit a single WARN per fresh observation.
|
||||
if self.bootstrap_transports.contains(&packet.transport_id)
|
||||
if self
|
||||
.nostr_rendezvous
|
||||
.is_bootstrap_transport(&packet.transport_id)
|
||||
&& let Some(npub) = self
|
||||
.bootstrap_transport_npubs
|
||||
.get(&packet.transport_id)
|
||||
.nostr_rendezvous
|
||||
.bootstrap_transport_npub(&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();
|
||||
|
||||
+85
-257
@@ -2,13 +2,10 @@
|
||||
|
||||
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::{BootstrapEvent, NostrRendezvous};
|
||||
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 +263,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.engine_arc()
|
||||
{
|
||||
let npub = peer_config.npub.clone();
|
||||
tokio::spawn(async move {
|
||||
@@ -360,7 +357,7 @@ impl Node {
|
||||
.filter(|(id, handle)| {
|
||||
handle.transport_type().name == "udp"
|
||||
&& handle.is_operational()
|
||||
&& !self.bootstrap_transports.contains(id)
|
||||
&& !self.nostr_rendezvous.is_bootstrap_transport(id)
|
||||
})
|
||||
.filter_map(|(id, handle)| {
|
||||
let local_addr = handle.local_addr()?;
|
||||
@@ -736,8 +733,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.engine_arc() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -883,19 +880,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)"
|
||||
),
|
||||
@@ -934,7 +931,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() {
|
||||
@@ -961,8 +958,8 @@ impl Node {
|
||||
/// Drain mDNS-discovered peers and initiate Noise XX handshakes.
|
||||
/// The handshake itself is the authentication — a spoofed mDNS advert
|
||||
/// with someone else's npub fails the XX 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;
|
||||
@@ -970,7 +967,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 {
|
||||
@@ -1198,7 +1195,7 @@ impl Node {
|
||||
}
|
||||
|
||||
if self.config().node.rendezvous.nostr.enabled {
|
||||
match NostrDiscovery::start(
|
||||
match NostrRendezvous::start(
|
||||
self.identity(),
|
||||
self.config().node.rendezvous.nostr.clone(),
|
||||
)
|
||||
@@ -1208,8 +1205,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.set_engine(runtime);
|
||||
self.nostr_rendezvous.set_started_at_ms(Self::now_ms());
|
||||
info!("Nostr overlay discovery enabled");
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -1234,14 +1231,14 @@ impl Node {
|
||||
.filter(|(id, h)| {
|
||||
h.transport_type().name == "udp"
|
||||
&& h.is_operational()
|
||||
&& !self.bootstrap_transports.contains(id)
|
||||
&& !self.nostr_rendezvous.is_bootstrap_transport(id)
|
||||
})
|
||||
.filter_map(|(id, h)| h.local_addr().map(|addr| (*id, addr.port())))
|
||||
.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,
|
||||
@@ -1250,7 +1247,7 @@ impl Node {
|
||||
.await
|
||||
{
|
||||
Ok(runtime) => {
|
||||
self.lan_discovery = Some(runtime);
|
||||
self.lan_rendezvous = Some(runtime);
|
||||
info!("LAN mDNS discovery enabled");
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -1538,7 +1535,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_engine()
|
||||
&& let Err(e) = bootstrap.shutdown().await
|
||||
{
|
||||
warn!(error = %e, "Failed to shutdown Nostr overlay discovery");
|
||||
@@ -1547,7 +1544,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;
|
||||
}
|
||||
|
||||
@@ -1680,89 +1677,6 @@ impl Node {
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn nostr_peer_fallback_addresses(
|
||||
&self,
|
||||
peer_config: &PeerConfig,
|
||||
existing: &[PeerAddress],
|
||||
) -> Vec<PeerAddress> {
|
||||
if !self.config().node.rendezvous.nostr.enabled
|
||||
|| !peer_config.via_nostr
|
||||
|| self.config().node.rendezvous.nostr.policy
|
||||
== crate::config::NostrDiscoveryPolicy::Disabled
|
||||
{
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let Some(bootstrap) = self.nostr_discovery.clone() else {
|
||||
return Vec::new();
|
||||
};
|
||||
let endpoints = match bootstrap.advert_endpoints_for_peer(&peer_config.npub).await {
|
||||
Ok(endpoints) => endpoints,
|
||||
Err(err) => {
|
||||
debug!(
|
||||
npub = %peer_config.npub,
|
||||
error = %err,
|
||||
"Failed to resolve Nostr advert endpoints for configured peer"
|
||||
);
|
||||
return Vec::new();
|
||||
}
|
||||
};
|
||||
|
||||
let mut fallback = Vec::new();
|
||||
let mut next_priority = existing
|
||||
.iter()
|
||||
.map(|addr| addr.priority)
|
||||
.max()
|
||||
.unwrap_or(100)
|
||||
.saturating_add(1);
|
||||
let seen_at_ms = Self::now_ms();
|
||||
for endpoint in endpoints {
|
||||
let Some(candidate) =
|
||||
Self::overlay_endpoint_to_peer_address(&endpoint, next_priority, seen_at_ms)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if existing
|
||||
.iter()
|
||||
.any(|addr| addr.transport == candidate.transport && addr.addr == candidate.addr)
|
||||
|| fallback.iter().any(|addr: &PeerAddress| {
|
||||
addr.transport == candidate.transport && addr.addr == candidate.addr
|
||||
})
|
||||
{
|
||||
continue;
|
||||
}
|
||||
fallback.push(candidate);
|
||||
next_priority = next_priority.saturating_add(1);
|
||||
}
|
||||
fallback
|
||||
}
|
||||
|
||||
fn overlay_endpoint_to_peer_address(
|
||||
endpoint: &OverlayEndpointAdvert,
|
||||
priority: u8,
|
||||
seen_at_ms: u64,
|
||||
) -> Option<PeerAddress> {
|
||||
let transport = match endpoint.transport {
|
||||
OverlayTransportKind::Udp => "udp",
|
||||
OverlayTransportKind::Tcp => "tcp",
|
||||
OverlayTransportKind::Tor => "tor",
|
||||
};
|
||||
Some(
|
||||
PeerAddress::with_priority(transport, endpoint.addr.clone(), priority)
|
||||
.with_seen_at_ms(seen_at_ms),
|
||||
)
|
||||
}
|
||||
|
||||
async fn request_nostr_bootstrap(&self, peer_config: &PeerConfig) -> bool {
|
||||
let Some(bootstrap) = self.nostr_discovery.clone() else {
|
||||
debug!(npub = %peer_config.npub, "No Nostr overlay runtime for udp:nat address");
|
||||
return false;
|
||||
};
|
||||
bootstrap.request_connect(peer_config.clone()).await;
|
||||
info!(npub = %peer_config.npub, "Started Nostr UDP NAT traversal attempt");
|
||||
true
|
||||
}
|
||||
|
||||
async fn attempt_peer_address_list(
|
||||
&mut self,
|
||||
peer_config: &PeerConfig,
|
||||
@@ -1788,7 +1702,11 @@ impl Node {
|
||||
if !allow_bootstrap_nat {
|
||||
continue;
|
||||
}
|
||||
if self.request_nostr_bootstrap(peer_config).await {
|
||||
if self
|
||||
.nostr_rendezvous
|
||||
.request_nostr_bootstrap(peer_config)
|
||||
.await
|
||||
{
|
||||
attempted = attempted.saturating_add(1);
|
||||
}
|
||||
continue;
|
||||
@@ -1890,7 +1808,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;
|
||||
}
|
||||
@@ -1907,13 +1825,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;
|
||||
}
|
||||
@@ -2019,7 +1937,9 @@ impl Node {
|
||||
let seen_at_ms = Self::now_ms();
|
||||
for endpoint in endpoints {
|
||||
let Some(candidate) =
|
||||
Self::overlay_endpoint_to_peer_address(&endpoint, priority, seen_at_ms)
|
||||
crate::nostr::RendezvousDriver::overlay_endpoint_to_peer_address(
|
||||
&endpoint, priority, seen_at_ms,
|
||||
)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
@@ -2105,20 +2025,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 {
|
||||
if self.nostr_rendezvous.startup_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;
|
||||
self.nostr_rendezvous.set_startup_sweep_done();
|
||||
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();
|
||||
@@ -2141,7 +2061,7 @@ impl Node {
|
||||
.startup_sweep_max_age_secs;
|
||||
self.run_open_discovery_sweep(bootstrap, Some(max_age_secs), "startup")
|
||||
.await;
|
||||
self.startup_open_discovery_sweep_done = true;
|
||||
self.nostr_rendezvous.set_startup_sweep_done();
|
||||
}
|
||||
|
||||
fn available_outbound_slots(&self) -> usize {
|
||||
@@ -2254,163 +2174,66 @@ impl Node {
|
||||
)
|
||||
}
|
||||
|
||||
async fn build_overlay_advert(
|
||||
&self,
|
||||
bootstrap: &std::sync::Arc<NostrDiscovery>,
|
||||
) -> Option<OverlayAdvert> {
|
||||
if !self.config().node.rendezvous.nostr.enabled {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut endpoints = Vec::new();
|
||||
let mut has_udp_nat = false;
|
||||
|
||||
/// Capture the advertisable-endpoint inputs of every operational
|
||||
/// transport into a snapshot the rendezvous driver can turn into an
|
||||
/// `OverlayAdvert` without borrowing the transport table across the
|
||||
/// STUN await. Iteration order matches `self.transports.values()`, and
|
||||
/// only transports whose type matched a configured listener are
|
||||
/// included, mirroring the original per-transport branch structure.
|
||||
fn advert_transport_snapshot(&self) -> Vec<crate::nostr::AdvertTransportSnapshot> {
|
||||
use crate::nostr::AdvertTransportSnapshot;
|
||||
let mut snapshot = Vec::new();
|
||||
for handle in self.transports.values() {
|
||||
if !handle.is_operational() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match handle.transport_type().name {
|
||||
"udp" => {
|
||||
let Some(cfg) = self.lookup_udp_config(handle.name()) else {
|
||||
continue;
|
||||
};
|
||||
if !cfg.advertise_on_nostr() {
|
||||
continue;
|
||||
}
|
||||
if cfg.is_public() {
|
||||
// Precedence:
|
||||
// 1. operator-supplied `external_addr` (skips STUN)
|
||||
// 2. non-wildcard `local_addr` (operator bound to
|
||||
// a specific public IP directly)
|
||||
// 3. STUN auto-discovery against ephemeral socket
|
||||
// 4. loud warn + omit endpoint
|
||||
if let Some(explicit) = cfg.external_advert_addr() {
|
||||
endpoints.push(OverlayEndpointAdvert {
|
||||
transport: OverlayTransportKind::Udp,
|
||||
addr: explicit.to_string(),
|
||||
});
|
||||
} else {
|
||||
match handle.local_addr() {
|
||||
Some(addr) if !addr.ip().is_unspecified() => {
|
||||
endpoints.push(OverlayEndpointAdvert {
|
||||
transport: OverlayTransportKind::Udp,
|
||||
addr: addr.to_string(),
|
||||
});
|
||||
}
|
||||
Some(addr) => {
|
||||
let key = handle.transport_id().as_u32();
|
||||
let port = addr.port();
|
||||
if let Some(public) =
|
||||
bootstrap.learn_public_udp_addr(key, port).await
|
||||
{
|
||||
endpoints.push(OverlayEndpointAdvert {
|
||||
transport: OverlayTransportKind::Udp,
|
||||
addr: public.to_string(),
|
||||
});
|
||||
} else {
|
||||
warn!(
|
||||
transport_id = key,
|
||||
bind_addr = %addr,
|
||||
"advert: udp public=true bound to wildcard but \
|
||||
STUN observation failed; advertising no UDP \
|
||||
endpoint. Either set transports.udp.external_addr, \
|
||||
bind to a specific public IP, or ensure \
|
||||
node.rendezvous.nostr.stun_servers is reachable"
|
||||
);
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
endpoints.push(OverlayEndpointAdvert {
|
||||
transport: OverlayTransportKind::Udp,
|
||||
addr: "nat".to_string(),
|
||||
});
|
||||
has_udp_nat = true;
|
||||
}
|
||||
snapshot.push(AdvertTransportSnapshot::Udp {
|
||||
advertise: cfg.advertise_on_nostr(),
|
||||
is_public: cfg.is_public(),
|
||||
external_addr: cfg.external_advert_addr(),
|
||||
local_addr: handle.local_addr(),
|
||||
transport_key: handle.transport_id().as_u32(),
|
||||
});
|
||||
}
|
||||
"tcp" => {
|
||||
let Some(cfg) = self.lookup_tcp_config(handle.name()) else {
|
||||
continue;
|
||||
};
|
||||
if !cfg.advertise_on_nostr() {
|
||||
continue;
|
||||
}
|
||||
// Precedence:
|
||||
// 1. operator-supplied `external_addr` (only path that
|
||||
// works on cloud-NAT setups where the public IP is
|
||||
// not on a host interface).
|
||||
// 2. non-wildcard `local_addr` (operator bound to a
|
||||
// specific public IP directly).
|
||||
// 3. loud warn + omit endpoint (no TCP STUN equivalent).
|
||||
if let Some(explicit) = cfg.external_advert_addr() {
|
||||
endpoints.push(OverlayEndpointAdvert {
|
||||
transport: OverlayTransportKind::Tcp,
|
||||
addr: explicit.to_string(),
|
||||
});
|
||||
} else {
|
||||
match handle.local_addr() {
|
||||
Some(addr) if !addr.ip().is_unspecified() => {
|
||||
endpoints.push(OverlayEndpointAdvert {
|
||||
transport: OverlayTransportKind::Tcp,
|
||||
addr: addr.to_string(),
|
||||
});
|
||||
}
|
||||
Some(addr) => {
|
||||
warn!(
|
||||
bind_addr = %addr,
|
||||
"advert: tcp advertise_on_nostr=true bound to wildcard \
|
||||
and no transports.tcp.external_addr set; advertising no \
|
||||
TCP endpoint. Either set external_addr to the public \
|
||||
IP (recommended for cloud 1:1-NAT setups) or bind \
|
||||
explicitly to the public IP"
|
||||
);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
snapshot.push(AdvertTransportSnapshot::Tcp {
|
||||
advertise: cfg.advertise_on_nostr(),
|
||||
external_addr: cfg.external_advert_addr(),
|
||||
local_addr: handle.local_addr(),
|
||||
});
|
||||
}
|
||||
"tor" => {
|
||||
let Some(cfg) = self.lookup_tor_config(handle.name()) else {
|
||||
continue;
|
||||
};
|
||||
if !cfg.advertise_on_nostr() {
|
||||
continue;
|
||||
}
|
||||
if let Some(addr) = handle.onion_address() {
|
||||
endpoints.push(OverlayEndpointAdvert {
|
||||
transport: OverlayTransportKind::Tor,
|
||||
addr: format!("{}:{}", addr, cfg.advertised_port()),
|
||||
});
|
||||
}
|
||||
snapshot.push(AdvertTransportSnapshot::Tor {
|
||||
advertise: cfg.advertise_on_nostr(),
|
||||
onion_addr: handle.onion_address().map(|s| s.to_string()),
|
||||
advertised_port: cfg.advertised_port(),
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if endpoints.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(OverlayAdvert {
|
||||
identifier: ADVERT_IDENTIFIER.to_string(),
|
||||
version: ADVERT_VERSION,
|
||||
endpoints,
|
||||
signal_relays: has_udp_nat
|
||||
.then(|| self.config().node.rendezvous.nostr.dm_relays.clone()),
|
||||
stun_servers: has_udp_nat
|
||||
.then(|| self.config().node.rendezvous.nostr.stun_servers.clone()),
|
||||
})
|
||||
snapshot
|
||||
}
|
||||
|
||||
async fn refresh_overlay_advert(
|
||||
&self,
|
||||
bootstrap: &std::sync::Arc<NostrDiscovery>,
|
||||
) -> Result<(), crate::discovery::nostr::BootstrapError> {
|
||||
let advert = self.build_overlay_advert(bootstrap).await;
|
||||
bootstrap.update_local_advert(advert).await
|
||||
bootstrap: &std::sync::Arc<NostrRendezvous>,
|
||||
) -> Result<(), crate::nostr::BootstrapError> {
|
||||
let snapshot = self.advert_transport_snapshot();
|
||||
self.nostr_rendezvous
|
||||
.refresh_overlay_advert(bootstrap, snapshot, &self.config().node.rendezvous.nostr)
|
||||
.await
|
||||
}
|
||||
|
||||
fn lookup_udp_config(&self, transport_name: Option<&str>) -> Option<&crate::config::UdpConfig> {
|
||||
@@ -2527,7 +2350,13 @@ impl Node {
|
||||
async fn peer_address_candidates(&self, peer_config: &PeerConfig) -> Vec<PeerAddress> {
|
||||
let static_addresses = self.static_peer_addresses(peer_config);
|
||||
let overlay_addresses = self
|
||||
.nostr_peer_fallback_addresses(peer_config, &static_addresses)
|
||||
.nostr_rendezvous
|
||||
.nostr_peer_fallback_addresses(
|
||||
peer_config,
|
||||
&static_addresses,
|
||||
&self.config().node.rendezvous.nostr,
|
||||
Self::now_ms(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut candidates = Vec::with_capacity(overlay_addresses.len() + static_addresses.len());
|
||||
@@ -2595,7 +2424,7 @@ impl Node {
|
||||
};
|
||||
if peer
|
||||
.transport_id()
|
||||
.map(|id| self.bootstrap_transports.contains(&id))
|
||||
.map(|id| self.nostr_rendezvous.is_bootstrap_transport(&id))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return false;
|
||||
@@ -2791,17 +2620,16 @@ impl Node {
|
||||
transport_id,
|
||||
crate::transport::TransportHandle::Udp(transport),
|
||||
);
|
||||
self.bootstrap_transports.insert(transport_id);
|
||||
self.bootstrap_transport_npubs
|
||||
.insert(transport_id, traversal.peer_npub.clone());
|
||||
self.nostr_rendezvous
|
||||
.insert_bootstrap_transport(transport_id, traversal.peer_npub.clone());
|
||||
|
||||
let remote_addr = TransportAddr::from_string(&traversal.remote_addr.to_string());
|
||||
if let Err(err) = self
|
||||
.initiate_connection(transport_id, remote_addr.clone(), Some(peer_identity))
|
||||
.await
|
||||
{
|
||||
self.bootstrap_transports.remove(&transport_id);
|
||||
self.bootstrap_transport_npubs.remove(&transport_id);
|
||||
self.nostr_rendezvous
|
||||
.remove_bootstrap_transport(&transport_id);
|
||||
if let Some(mut handle) = self.transports.remove(&transport_id) {
|
||||
let _ = handle.stop().await;
|
||||
}
|
||||
|
||||
+17
-40
@@ -79,7 +79,7 @@ use crate::upper::tun::{TunError, TunOutboundRx, TunState, TunTx};
|
||||
use crate::utils::index::IndexAllocator;
|
||||
use crate::{Config, ConfigError, Identity, IdentityError, NodeAddr, PeerIdentity, TreeCoordinate};
|
||||
use rand::Rng;
|
||||
use std::collections::{BTreeSet, HashMap, HashSet, VecDeque};
|
||||
use std::collections::{BTreeSet, HashMap, VecDeque};
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
use std::thread::JoinHandle;
|
||||
@@ -464,31 +464,16 @@ pub struct Node {
|
||||
/// are exhausted.
|
||||
retry_pending: HashMap<NodeAddr, retry::RetryState>,
|
||||
|
||||
/// Optional Nostr/STUN overlay discovery coordinator for `udp:nat` peers.
|
||||
nostr_discovery: Option<Arc<crate::discovery::nostr::NostrDiscovery>>,
|
||||
/// Node-side driver state for the Nostr overlay peer-rendezvous
|
||||
/// subsystem: the engine handle, its startup timestamp, the one-shot
|
||||
/// startup-sweep latch, and the per-peer bootstrap-transport bookkeeping
|
||||
/// adopted from NAT-traversal handoffs.
|
||||
nostr_rendezvous: crate::nostr::RendezvousDriver,
|
||||
/// 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>>,
|
||||
/// 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>,
|
||||
/// 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.
|
||||
startup_open_discovery_sweep_done: bool,
|
||||
/// Per-peer UDP transports adopted from NAT traversal handoff.
|
||||
bootstrap_transports: HashSet<TransportId>,
|
||||
/// Originating peer npub (bech32) for each adopted bootstrap
|
||||
/// transport, captured at `adopt_established_traversal` time.
|
||||
/// Populated alongside `bootstrap_transports`; cleared in
|
||||
/// `cleanup_bootstrap_transport_if_unused`. Used by the rx loop to
|
||||
/// route fatal-protocol-mismatch observations back to the
|
||||
/// Nostr-discovery `failure_state` for long cooldown application.
|
||||
bootstrap_transport_npubs: HashMap<TransportId, String>,
|
||||
lan_rendezvous: Option<Arc<crate::mdns::LanRendezvous>>,
|
||||
|
||||
// === Periodic Parent Re-evaluation ===
|
||||
/// Timestamp of last periodic parent re-evaluation (for pacing).
|
||||
@@ -707,12 +692,8 @@ impl Node {
|
||||
),
|
||||
pending_connects: Vec::new(),
|
||||
retry_pending: HashMap::new(),
|
||||
nostr_discovery: None,
|
||||
nostr_discovery_started_at_ms: None,
|
||||
lan_discovery: None,
|
||||
startup_open_discovery_sweep_done: false,
|
||||
bootstrap_transports: HashSet::new(),
|
||||
bootstrap_transport_npubs: HashMap::new(),
|
||||
nostr_rendezvous: crate::nostr::RendezvousDriver::default(),
|
||||
lan_rendezvous: None,
|
||||
last_parent_reeval: None,
|
||||
last_congestion_log: None,
|
||||
estimated_mesh_size: None,
|
||||
@@ -871,12 +852,8 @@ 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,
|
||||
startup_open_discovery_sweep_done: false,
|
||||
bootstrap_transports: HashSet::new(),
|
||||
bootstrap_transport_npubs: HashMap::new(),
|
||||
nostr_rendezvous: crate::nostr::RendezvousDriver::default(),
|
||||
lan_rendezvous: None,
|
||||
last_parent_reeval: None,
|
||||
last_congestion_log: None,
|
||||
estimated_mesh_size: None,
|
||||
@@ -1874,7 +1851,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()
|
||||
@@ -2331,7 +2308,7 @@ impl Node {
|
||||
}
|
||||
|
||||
pub(crate) fn cleanup_bootstrap_transport_if_unused(&mut self, transport_id: TransportId) {
|
||||
if !self.bootstrap_transports.contains(&transport_id) {
|
||||
if !self.nostr_rendezvous.is_bootstrap_transport(&transport_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2361,8 +2338,8 @@ impl Node {
|
||||
"bootstrap transport has no remaining references; dropping"
|
||||
);
|
||||
|
||||
self.bootstrap_transports.remove(&transport_id);
|
||||
self.bootstrap_transport_npubs.remove(&transport_id);
|
||||
self.nostr_rendezvous
|
||||
.remove_bootstrap_transport(&transport_id);
|
||||
self.transport_drops.remove(&transport_id);
|
||||
self.transports.remove(&transport_id);
|
||||
}
|
||||
@@ -2437,8 +2414,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.engine()
|
||||
}
|
||||
|
||||
/// Iterate over all peer node IDs.
|
||||
|
||||
@@ -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
@@ -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.engine_arc() {
|
||||
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.engine_arc()
|
||||
{
|
||||
let npub = peer_config.npub.clone();
|
||||
tokio::spawn(async move {
|
||||
|
||||
@@ -1043,20 +1043,20 @@ async fn test_response_path_mtu_four_node_chain() {
|
||||
/// 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;
|
||||
@@ -1064,7 +1064,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
|
||||
@@ -1087,8 +1087,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(),
|
||||
@@ -1099,7 +1099,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
@@ -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();
|
||||
}
|
||||
@@ -1128,14 +1128,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.set_engine(bootstrap.clone());
|
||||
|
||||
node.poll_nostr_discovery().await;
|
||||
node.poll_nostr_rendezvous().await;
|
||||
|
||||
assert!(
|
||||
bootstrap.failure_state_snapshot().is_empty(),
|
||||
@@ -1149,7 +1149,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();
|
||||
@@ -1162,7 +1162,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 {
|
||||
@@ -1173,9 +1173,9 @@ async fn test_nostr_traversal_established_skips_connected_peer() {
|
||||
socket,
|
||||
),
|
||||
});
|
||||
node.nostr_discovery = Some(bootstrap.clone());
|
||||
node.nostr_rendezvous.set_engine(bootstrap.clone());
|
||||
|
||||
node.poll_nostr_discovery().await;
|
||||
node.poll_nostr_rendezvous().await;
|
||||
|
||||
assert_eq!(
|
||||
node.link_count(),
|
||||
@@ -1711,14 +1711,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();
|
||||
@@ -1730,13 +1730,13 @@ async fn poll_nostr_discovery_established_gated_at_capacity() {
|
||||
socket,
|
||||
),
|
||||
});
|
||||
node.nostr_discovery = Some(bootstrap.clone());
|
||||
node.nostr_rendezvous.set_engine(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(),
|
||||
@@ -1756,11 +1756,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)"
|
||||
|
||||
@@ -0,0 +1,399 @@
|
||||
//! Node-side driver state for the Nostr overlay peer-rendezvous subsystem.
|
||||
//!
|
||||
//! [`RendezvousDriver`] consolidates the rendezvous-subsystem state that
|
||||
//! previously lived as loose fields on the `Node` struct: the engine handle,
|
||||
//! its startup timestamp, the one-shot startup-sweep latch, and the
|
||||
//! per-peer bootstrap-transport bookkeeping adopted from NAT-traversal
|
||||
//! handoffs. Keeping it in the `nostr` module gives the subsystem a single
|
||||
//! home while leaving the transport/connection-table mutations that consume
|
||||
//! this state on `Node`.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::config::{NostrRendezvousConfig, NostrRendezvousPolicy, PeerAddress, PeerConfig};
|
||||
use crate::transport::TransportId;
|
||||
|
||||
use super::{
|
||||
ADVERT_IDENTIFIER, ADVERT_VERSION, BootstrapError, NostrRendezvous, OverlayAdvert,
|
||||
OverlayEndpointAdvert, OverlayTransportKind,
|
||||
};
|
||||
|
||||
/// Snapshot of a single operational transport's advertisable endpoint
|
||||
/// inputs, captured on `Node` at advert-build time so the driver can
|
||||
/// assemble the overlay advert without borrowing the transport table.
|
||||
/// Only transports whose type matched a configured listener are included;
|
||||
/// the `advertise` gate is carried verbatim so the driver reproduces the
|
||||
/// original per-transport branch logic exactly.
|
||||
pub enum AdvertTransportSnapshot {
|
||||
Udp {
|
||||
advertise: bool,
|
||||
is_public: bool,
|
||||
external_addr: Option<SocketAddr>,
|
||||
local_addr: Option<SocketAddr>,
|
||||
transport_key: u32,
|
||||
},
|
||||
Tcp {
|
||||
advertise: bool,
|
||||
external_addr: Option<SocketAddr>,
|
||||
local_addr: Option<SocketAddr>,
|
||||
},
|
||||
Tor {
|
||||
advertise: bool,
|
||||
onion_addr: Option<String>,
|
||||
advertised_port: u16,
|
||||
},
|
||||
}
|
||||
|
||||
/// Node-side rendezvous-subsystem state and bootstrap-transport bookkeeping.
|
||||
#[derive(Default)]
|
||||
pub struct RendezvousDriver {
|
||||
/// Optional Nostr/STUN overlay discovery coordinator for `udp:nat` peers.
|
||||
engine: Option<Arc<NostrRendezvous>>,
|
||||
/// 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.
|
||||
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.
|
||||
startup_sweep_done: bool,
|
||||
/// Per-peer UDP transports adopted from NAT traversal handoff.
|
||||
bootstrap_transports: HashSet<TransportId>,
|
||||
/// Originating peer npub (bech32) for each adopted bootstrap
|
||||
/// transport, captured at `adopt_established_traversal` time.
|
||||
/// Populated alongside `bootstrap_transports`; cleared in
|
||||
/// `cleanup_bootstrap_transport_if_unused`. Used by the rx loop to
|
||||
/// route fatal-protocol-mismatch observations back to the
|
||||
/// Nostr-discovery `failure_state` for long cooldown application.
|
||||
bootstrap_transport_npubs: HashMap<TransportId, String>,
|
||||
}
|
||||
|
||||
impl RendezvousDriver {
|
||||
/// Borrow the engine handle if discovery is running.
|
||||
pub fn engine(&self) -> Option<&NostrRendezvous> {
|
||||
self.engine.as_deref()
|
||||
}
|
||||
|
||||
/// Clone the engine `Arc` handle if discovery is running.
|
||||
pub fn engine_arc(&self) -> Option<Arc<NostrRendezvous>> {
|
||||
self.engine.clone()
|
||||
}
|
||||
|
||||
/// Install the engine handle once discovery starts.
|
||||
pub fn set_engine(&mut self, engine: Arc<NostrRendezvous>) {
|
||||
self.engine = Some(engine);
|
||||
}
|
||||
|
||||
/// Take the engine handle for shutdown, clearing it.
|
||||
pub fn take_engine(&mut self) -> Option<Arc<NostrRendezvous>> {
|
||||
self.engine.take()
|
||||
}
|
||||
|
||||
/// Record the wall-clock ms at which discovery successfully started.
|
||||
pub fn set_started_at_ms(&mut self, now_ms: u64) {
|
||||
self.started_at_ms = Some(now_ms);
|
||||
}
|
||||
|
||||
/// Wall-clock ms when discovery started, if it has.
|
||||
pub fn started_at_ms(&self) -> Option<u64> {
|
||||
self.started_at_ms
|
||||
}
|
||||
|
||||
/// Whether the one-shot startup sweep has already run.
|
||||
pub fn startup_sweep_done(&self) -> bool {
|
||||
self.startup_sweep_done
|
||||
}
|
||||
|
||||
/// Latch the one-shot startup sweep as done.
|
||||
pub fn set_startup_sweep_done(&mut self) {
|
||||
self.startup_sweep_done = true;
|
||||
}
|
||||
|
||||
/// Whether `transport_id` is an adopted bootstrap transport.
|
||||
pub fn is_bootstrap_transport(&self, transport_id: &TransportId) -> bool {
|
||||
self.bootstrap_transports.contains(transport_id)
|
||||
}
|
||||
|
||||
/// Originating peer npub for an adopted bootstrap transport, if any.
|
||||
pub fn bootstrap_transport_npub(&self, transport_id: &TransportId) -> Option<&String> {
|
||||
self.bootstrap_transport_npubs.get(transport_id)
|
||||
}
|
||||
|
||||
/// Register an adopted bootstrap transport and its originating npub.
|
||||
pub fn insert_bootstrap_transport(&mut self, transport_id: TransportId, npub: String) {
|
||||
self.bootstrap_transports.insert(transport_id);
|
||||
self.bootstrap_transport_npubs.insert(transport_id, npub);
|
||||
}
|
||||
|
||||
/// Drop an adopted bootstrap transport from both bookkeeping maps.
|
||||
pub fn remove_bootstrap_transport(&mut self, transport_id: &TransportId) {
|
||||
self.bootstrap_transports.remove(transport_id);
|
||||
self.bootstrap_transport_npubs.remove(transport_id);
|
||||
}
|
||||
|
||||
/// Convert an advertised overlay endpoint into a `PeerAddress` candidate.
|
||||
/// Pure mapping; `seen_at_ms` is supplied by the caller.
|
||||
pub fn overlay_endpoint_to_peer_address(
|
||||
endpoint: &OverlayEndpointAdvert,
|
||||
priority: u8,
|
||||
seen_at_ms: u64,
|
||||
) -> Option<PeerAddress> {
|
||||
let transport = match endpoint.transport {
|
||||
OverlayTransportKind::Udp => "udp",
|
||||
OverlayTransportKind::Tcp => "tcp",
|
||||
OverlayTransportKind::Tor => "tor",
|
||||
};
|
||||
Some(
|
||||
PeerAddress::with_priority(transport, endpoint.addr.clone(), priority)
|
||||
.with_seen_at_ms(seen_at_ms),
|
||||
)
|
||||
}
|
||||
|
||||
/// Kick off a Nostr-mediated UDP NAT-traversal attempt for `peer_config`.
|
||||
/// Returns whether an attempt was started (false if discovery is down).
|
||||
pub async fn request_nostr_bootstrap(&self, peer_config: &PeerConfig) -> bool {
|
||||
let Some(bootstrap) = self.engine_arc() else {
|
||||
debug!(npub = %peer_config.npub, "No Nostr overlay runtime for udp:nat address");
|
||||
return false;
|
||||
};
|
||||
bootstrap.request_connect(peer_config.clone()).await;
|
||||
info!(npub = %peer_config.npub, "Started Nostr UDP NAT traversal attempt");
|
||||
true
|
||||
}
|
||||
|
||||
/// Resolve additional overlay `PeerAddress` candidates for a `via_nostr`
|
||||
/// configured peer by fetching its published advert endpoints. `existing`
|
||||
/// is the already-known static address list (used for priority and dedup);
|
||||
/// `now_ms` stamps the returned candidates' `seen_at`.
|
||||
pub async fn nostr_peer_fallback_addresses(
|
||||
&self,
|
||||
peer_config: &PeerConfig,
|
||||
existing: &[PeerAddress],
|
||||
nostr_cfg: &NostrRendezvousConfig,
|
||||
now_ms: u64,
|
||||
) -> Vec<PeerAddress> {
|
||||
if !nostr_cfg.enabled
|
||||
|| !peer_config.via_nostr
|
||||
|| nostr_cfg.policy == NostrRendezvousPolicy::Disabled
|
||||
{
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let Some(bootstrap) = self.engine_arc() else {
|
||||
return Vec::new();
|
||||
};
|
||||
let endpoints = match bootstrap.advert_endpoints_for_peer(&peer_config.npub).await {
|
||||
Ok(endpoints) => endpoints,
|
||||
Err(err) => {
|
||||
debug!(
|
||||
npub = %peer_config.npub,
|
||||
error = %err,
|
||||
"Failed to resolve Nostr advert endpoints for configured peer"
|
||||
);
|
||||
return Vec::new();
|
||||
}
|
||||
};
|
||||
|
||||
let mut fallback = Vec::new();
|
||||
let mut next_priority = existing
|
||||
.iter()
|
||||
.map(|addr| addr.priority)
|
||||
.max()
|
||||
.unwrap_or(100)
|
||||
.saturating_add(1);
|
||||
let seen_at_ms = now_ms;
|
||||
for endpoint in endpoints {
|
||||
let Some(candidate) =
|
||||
Self::overlay_endpoint_to_peer_address(&endpoint, next_priority, seen_at_ms)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if existing
|
||||
.iter()
|
||||
.any(|addr| addr.transport == candidate.transport && addr.addr == candidate.addr)
|
||||
|| fallback.iter().any(|addr: &PeerAddress| {
|
||||
addr.transport == candidate.transport && addr.addr == candidate.addr
|
||||
})
|
||||
{
|
||||
continue;
|
||||
}
|
||||
fallback.push(candidate);
|
||||
next_priority = next_priority.saturating_add(1);
|
||||
}
|
||||
fallback
|
||||
}
|
||||
|
||||
/// Publish (or withdraw) the local overlay advert built from `snapshot`.
|
||||
/// `bootstrap` is passed explicitly because the startup path refreshes the
|
||||
/// advert before the engine handle is installed on the driver.
|
||||
pub async fn refresh_overlay_advert(
|
||||
&self,
|
||||
bootstrap: &Arc<NostrRendezvous>,
|
||||
snapshot: Vec<AdvertTransportSnapshot>,
|
||||
nostr_cfg: &NostrRendezvousConfig,
|
||||
) -> Result<(), BootstrapError> {
|
||||
let advert = self
|
||||
.build_overlay_advert(bootstrap, snapshot, nostr_cfg)
|
||||
.await;
|
||||
bootstrap.update_local_advert(advert).await
|
||||
}
|
||||
|
||||
/// Assemble the local `OverlayAdvert` from the per-transport `snapshot`.
|
||||
/// The STUN `learn_public_udp_addr` await for wildcard-bound public UDP
|
||||
/// sockets is reached through the `bootstrap` handle.
|
||||
async fn build_overlay_advert(
|
||||
&self,
|
||||
bootstrap: &Arc<NostrRendezvous>,
|
||||
snapshot: Vec<AdvertTransportSnapshot>,
|
||||
nostr_cfg: &NostrRendezvousConfig,
|
||||
) -> Option<OverlayAdvert> {
|
||||
if !nostr_cfg.enabled {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut endpoints = Vec::new();
|
||||
let mut has_udp_nat = false;
|
||||
|
||||
for entry in snapshot {
|
||||
match entry {
|
||||
AdvertTransportSnapshot::Udp {
|
||||
advertise,
|
||||
is_public,
|
||||
external_addr,
|
||||
local_addr,
|
||||
transport_key,
|
||||
} => {
|
||||
if !advertise {
|
||||
continue;
|
||||
}
|
||||
if is_public {
|
||||
// Precedence:
|
||||
// 1. operator-supplied `external_addr` (skips STUN)
|
||||
// 2. non-wildcard `local_addr` (operator bound to
|
||||
// a specific public IP directly)
|
||||
// 3. STUN auto-discovery against ephemeral socket
|
||||
// 4. loud warn + omit endpoint
|
||||
if let Some(explicit) = external_addr {
|
||||
endpoints.push(OverlayEndpointAdvert {
|
||||
transport: OverlayTransportKind::Udp,
|
||||
addr: explicit.to_string(),
|
||||
});
|
||||
} else {
|
||||
match local_addr {
|
||||
Some(addr) if !addr.ip().is_unspecified() => {
|
||||
endpoints.push(OverlayEndpointAdvert {
|
||||
transport: OverlayTransportKind::Udp,
|
||||
addr: addr.to_string(),
|
||||
});
|
||||
}
|
||||
Some(addr) => {
|
||||
let key = transport_key;
|
||||
let port = addr.port();
|
||||
if let Some(public) =
|
||||
bootstrap.learn_public_udp_addr(key, port).await
|
||||
{
|
||||
endpoints.push(OverlayEndpointAdvert {
|
||||
transport: OverlayTransportKind::Udp,
|
||||
addr: public.to_string(),
|
||||
});
|
||||
} else {
|
||||
warn!(
|
||||
transport_id = key,
|
||||
bind_addr = %addr,
|
||||
"advert: udp public=true bound to wildcard but \
|
||||
STUN observation failed; advertising no UDP \
|
||||
endpoint. Either set transports.udp.external_addr, \
|
||||
bind to a specific public IP, or ensure \
|
||||
node.rendezvous.nostr.stun_servers is reachable"
|
||||
);
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
endpoints.push(OverlayEndpointAdvert {
|
||||
transport: OverlayTransportKind::Udp,
|
||||
addr: "nat".to_string(),
|
||||
});
|
||||
has_udp_nat = true;
|
||||
}
|
||||
}
|
||||
AdvertTransportSnapshot::Tcp {
|
||||
advertise,
|
||||
external_addr,
|
||||
local_addr,
|
||||
} => {
|
||||
if !advertise {
|
||||
continue;
|
||||
}
|
||||
// Precedence:
|
||||
// 1. operator-supplied `external_addr` (only path that
|
||||
// works on cloud-NAT setups where the public IP is
|
||||
// not on a host interface).
|
||||
// 2. non-wildcard `local_addr` (operator bound to a
|
||||
// specific public IP directly).
|
||||
// 3. loud warn + omit endpoint (no TCP STUN equivalent).
|
||||
if let Some(explicit) = external_addr {
|
||||
endpoints.push(OverlayEndpointAdvert {
|
||||
transport: OverlayTransportKind::Tcp,
|
||||
addr: explicit.to_string(),
|
||||
});
|
||||
} else {
|
||||
match local_addr {
|
||||
Some(addr) if !addr.ip().is_unspecified() => {
|
||||
endpoints.push(OverlayEndpointAdvert {
|
||||
transport: OverlayTransportKind::Tcp,
|
||||
addr: addr.to_string(),
|
||||
});
|
||||
}
|
||||
Some(addr) => {
|
||||
warn!(
|
||||
bind_addr = %addr,
|
||||
"advert: tcp advertise_on_nostr=true bound to wildcard \
|
||||
and no transports.tcp.external_addr set; advertising no \
|
||||
TCP endpoint. Either set external_addr to the public \
|
||||
IP (recommended for cloud 1:1-NAT setups) or bind \
|
||||
explicitly to the public IP"
|
||||
);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
AdvertTransportSnapshot::Tor {
|
||||
advertise,
|
||||
onion_addr,
|
||||
advertised_port,
|
||||
} => {
|
||||
if !advertise {
|
||||
continue;
|
||||
}
|
||||
if let Some(addr) = onion_addr {
|
||||
endpoints.push(OverlayEndpointAdvert {
|
||||
transport: OverlayTransportKind::Tor,
|
||||
addr: format!("{}:{}", addr, advertised_port),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if endpoints.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(OverlayAdvert {
|
||||
identifier: ADVERT_IDENTIFIER.to_string(),
|
||||
version: ADVERT_VERSION,
|
||||
endpoints,
|
||||
signal_relays: has_udp_nat.then(|| nostr_cfg.dm_relays.clone()),
|
||||
stun_servers: has_udp_nat.then(|| nostr_cfg.stun_servers.clone()),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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,6 @@
|
||||
mod driver;
|
||||
mod failure_state;
|
||||
mod handoff;
|
||||
mod runtime;
|
||||
mod signal;
|
||||
mod stun;
|
||||
@@ -8,7 +10,9 @@ mod types;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub use runtime::NostrDiscovery;
|
||||
pub use driver::{AdvertTransportSnapshot, RendezvousDriver};
|
||||
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,5 +1,5 @@
|
||||
use super::handoff::EstablishedTraversal;
|
||||
use crate::config::PeerConfig;
|
||||
use crate::discovery::EstablishedTraversal;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const ADVERT_KIND: u16 = 37195;
|
||||
@@ -13,9 +13,9 @@ pub const ADVERT_KIND: u16 = 37195;
|
||||
pub const ADVERT_IDENTIFIER: &str = "fips-overlay-v1-next";
|
||||
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)]
|
||||
@@ -196,7 +196,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,
|
||||
@@ -219,7 +219,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,
|
||||
@@ -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)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# relevant to NAT-traversal handshake-completion flake evidence
|
||||
# collection (ISSUE-2026-0027):
|
||||
#
|
||||
# - fips::discovery::nostr — overlay advert publish/consume
|
||||
# - fips::nostr — overlay advert publish/consume
|
||||
# (where the cross-init race begins)
|
||||
# - fips::transport::udp — UDP socket bind/send/recv
|
||||
# (where the punch packets flow)
|
||||
@@ -33,7 +33,7 @@
|
||||
# is set and the suite is nat-lan.
|
||||
|
||||
x-trace-rust-log: &trace-rust-log
|
||||
RUST_LOG: "info,fips::discovery::nostr=trace,fips::transport::udp=trace,fips::node::lifecycle=trace,fips::node::handlers::handshake=trace,fips::node::handlers::forwarding=trace"
|
||||
RUST_LOG: "info,fips::nostr=trace,fips::transport::udp=trace,fips::node::lifecycle=trace,fips::node::handlers::handshake=trace,fips::node::handlers::forwarding=trace"
|
||||
|
||||
services:
|
||||
lan-a:
|
||||
|
||||
@@ -27,7 +27,7 @@ x-fips-common: &fips-common
|
||||
- net.ipv6.conf.all.disable_ipv6=0
|
||||
restart: "no"
|
||||
environment:
|
||||
- RUST_LOG=info,fips::discovery::nostr=debug,fips::node::lifecycle=debug
|
||||
- RUST_LOG=info,fips::nostr=debug,fips::node::lifecycle=debug
|
||||
|
||||
services:
|
||||
relay:
|
||||
@@ -115,7 +115,7 @@ services:
|
||||
entrypoint:
|
||||
- /usr/local/bin/nat-node-entrypoint.sh
|
||||
environment:
|
||||
- RUST_LOG=info,fips::discovery::nostr=debug,fips::node::lifecycle=debug
|
||||
- RUST_LOG=info,fips::nostr=debug,fips::node::lifecycle=debug
|
||||
- DATA_IF=eth0
|
||||
- ROUTE_SUBNET=172.31.254.0/24
|
||||
- ROUTE_VIA=172.31.1.254
|
||||
@@ -141,7 +141,7 @@ services:
|
||||
entrypoint:
|
||||
- /usr/local/bin/nat-node-entrypoint.sh
|
||||
environment:
|
||||
- RUST_LOG=info,fips::discovery::nostr=debug,fips::node::lifecycle=debug
|
||||
- RUST_LOG=info,fips::nostr=debug,fips::node::lifecycle=debug
|
||||
- DATA_IF=eth0
|
||||
- ROUTE_SUBNET=172.31.254.0/24
|
||||
- ROUTE_VIA=172.31.2.254
|
||||
@@ -167,7 +167,7 @@ services:
|
||||
entrypoint:
|
||||
- /usr/local/bin/nat-node-entrypoint.sh
|
||||
environment:
|
||||
- RUST_LOG=info,fips::discovery::nostr=debug,fips::node::lifecycle=debug
|
||||
- RUST_LOG=info,fips::nostr=debug,fips::node::lifecycle=debug
|
||||
- DATA_IF=eth0
|
||||
- ROUTE_SUBNET=172.31.254.0/24
|
||||
- ROUTE_VIA=172.31.1.254
|
||||
@@ -193,7 +193,7 @@ services:
|
||||
entrypoint:
|
||||
- /usr/local/bin/nat-node-entrypoint.sh
|
||||
environment:
|
||||
- RUST_LOG=info,fips::discovery::nostr=debug,fips::node::lifecycle=debug
|
||||
- RUST_LOG=info,fips::nostr=debug,fips::node::lifecycle=debug
|
||||
- DATA_IF=eth0
|
||||
- ROUTE_SUBNET=172.31.254.0/24
|
||||
- ROUTE_VIA=172.31.2.254
|
||||
|
||||
Reference in New Issue
Block a user