Skip to main content

fips_core/config/node/
discovery.rs

1use super::*;
2
3/// Discovery protocol (`node.discovery.*`).
4#[derive(Debug, Clone, Serialize, Deserialize)]
5pub struct DiscoveryConfig {
6    /// Hop limit for LookupRequest flood (`node.discovery.ttl`).
7    #[serde(default = "DiscoveryConfig::default_ttl")]
8    pub ttl: u8,
9    /// Per-attempt timeouts in seconds (`node.discovery.attempt_timeouts_secs`).
10    /// Each entry is the time to wait for a response before sending the next
11    /// LookupRequest (with a fresh request_id). Sequence length determines the
12    /// total number of attempts before declaring the destination unreachable.
13    /// Default `[1, 2, 4, 8]` gives 4 attempts and a 15s total budget.
14    #[serde(default = "DiscoveryConfig::default_attempt_timeouts_secs")]
15    pub attempt_timeouts_secs: Vec<u64>,
16    /// Dedup cache expiry in seconds (`node.discovery.recent_expiry_secs`).
17    #[serde(default = "DiscoveryConfig::default_recent_expiry_secs")]
18    pub recent_expiry_secs: u64,
19    /// Base backoff after lookup failure in seconds (`node.discovery.backoff_base_secs`).
20    /// Doubles per consecutive failure up to `backoff_max_secs`. Set both to
21    /// 0 to disable post-failure suppression.
22    #[serde(default = "DiscoveryConfig::default_backoff_base_secs")]
23    pub backoff_base_secs: u64,
24    /// Maximum backoff cap in seconds (`node.discovery.backoff_max_secs`).
25    #[serde(default = "DiscoveryConfig::default_backoff_max_secs")]
26    pub backoff_max_secs: u64,
27    /// Minimum interval between forwarded lookups for the same target in seconds
28    /// (`node.discovery.forward_min_interval_secs`).
29    /// Defense-in-depth against misbehaving nodes.
30    #[serde(default = "DiscoveryConfig::default_forward_min_interval_secs")]
31    pub forward_min_interval_secs: u64,
32    /// Nostr-mediated overlay endpoint discovery.
33    #[serde(default = "DiscoveryConfig::default_nostr")]
34    pub nostr: NostrDiscoveryConfig,
35    /// mDNS / DNS-SD peer discovery on the local link. Identity surface
36    /// is a strict subset of what `nostr.advertise` already publishes
37    /// publicly, so there's no marginal privacy cost; the latency win
38    /// for same-LAN peers is large (sub-second pairing, no relay).
39    #[serde(default = "DiscoveryConfig::default_lan")]
40    pub lan: crate::discovery::lan::LanDiscoveryConfig,
41    /// Same-host process discovery through one fixed loopback UDP rendezvous.
42    /// The socket is only a bootstrap hint; the normal Noise handshake still
43    /// authenticates every local peer. It never changes application-owned
44    /// outbound connectivity.
45    #[serde(default = "DiscoveryConfig::default_local")]
46    pub local: crate::discovery::local::LocalInstanceDiscoveryConfig,
47}
48
49impl Default for DiscoveryConfig {
50    fn default() -> Self {
51        Self {
52            ttl: 64,
53            attempt_timeouts_secs: vec![1, 2, 4, 8],
54            recent_expiry_secs: 10,
55            backoff_base_secs: 30,
56            backoff_max_secs: 300,
57            forward_min_interval_secs: 2,
58            nostr: NostrDiscoveryConfig::default(),
59            lan: crate::discovery::lan::LanDiscoveryConfig::default(),
60            local: crate::discovery::local::LocalInstanceDiscoveryConfig::default(),
61        }
62    }
63}
64
65impl DiscoveryConfig {
66    fn default_ttl() -> u8 {
67        64
68    }
69    fn default_attempt_timeouts_secs() -> Vec<u64> {
70        vec![1, 2, 4, 8]
71    }
72    fn default_recent_expiry_secs() -> u64 {
73        10
74    }
75    fn default_backoff_base_secs() -> u64 {
76        30
77    }
78    fn default_backoff_max_secs() -> u64 {
79        300
80    }
81    fn default_forward_min_interval_secs() -> u64 {
82        2
83    }
84    fn default_nostr() -> NostrDiscoveryConfig {
85        NostrDiscoveryConfig::default()
86    }
87    fn default_lan() -> crate::discovery::lan::LanDiscoveryConfig {
88        crate::discovery::lan::LanDiscoveryConfig::default()
89    }
90    fn default_local() -> crate::discovery::local::LocalInstanceDiscoveryConfig {
91        crate::discovery::local::LocalInstanceDiscoveryConfig::default()
92    }
93}
94
95/// Nostr advert discovery policy.
96///
97/// Controls how overlay endpoint adverts are consumed:
98/// - `disabled`: ignore advert-derived endpoints for all peers
99/// - `configured_only`: allow advert fallback for configured peers
100/// - `open`: also consider adverts for non-configured peers
101#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
102#[serde(rename_all = "snake_case")]
103pub enum NostrDiscoveryPolicy {
104    Disabled,
105    #[default]
106    ConfiguredOnly,
107    Open,
108}
109
110/// Source used to publish and receive Nostr peer adverts.
111///
112/// `relays` keeps the built-in relay client as the peerfinding provider.
113/// `external` disables built-in advert relay publication, subscriptions, and
114/// cache-miss queries so an adapter can route ordinary signed advert events
115/// through a transport-neutral pubsub provider instead. Transport negotiation
116/// is carried by authenticated FIPS sessions, not by a second relay plane.
117#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
118#[serde(rename_all = "snake_case")]
119pub enum NostrPeerfindingSource {
120    #[default]
121    Relays,
122    External,
123}
124
125/// Nostr-mediated overlay endpoint discovery (`node.discovery.nostr.*`).
126#[derive(Debug, Clone, Serialize, Deserialize)]
127#[serde(deny_unknown_fields)]
128pub struct NostrDiscoveryConfig {
129    /// Enable signed Nostr peer adverts and FIPS-session transport upgrades.
130    #[serde(default)]
131    pub enabled: bool,
132    /// Publish service advertisements so remote peers can bootstrap inbound.
133    #[serde(default = "NostrDiscoveryConfig::default_advertise")]
134    pub advertise: bool,
135    /// Provider used for signed peer advert distribution and lookup.
136    #[serde(default)]
137    pub peerfinding_source: NostrPeerfindingSource,
138    /// Relay URLs used for service advertisements.
139    #[serde(default = "NostrDiscoveryConfig::default_advert_relays")]
140    pub advert_relays: Vec<String>,
141    /// STUN servers used for local reflexive address discovery.
142    /// Outbound observation uses only this local list; peer-advertised STUN
143    /// values are informational and are not treated as egress targets.
144    #[serde(default = "NostrDiscoveryConfig::default_stun_servers")]
145    pub stun_servers: Vec<String>,
146    /// Physical interface for STUN and UDP traversal sockets.
147    ///
148    /// This should match the UDP carrier's `bind_interface` when FIPS runs
149    /// inside a system VPN, preventing discovery packets from looping back
150    /// into the overlay default route.
151    #[serde(default, skip_serializing_if = "Option::is_none")]
152    pub bind_interface: Option<String>,
153    /// Whether to advertise local (RFC 1918 / ULA) interface addresses as
154    /// host candidates in the traversal offer.
155    ///
156    /// Off by default: in most deployments the relevant peers are not on the
157    /// same broadcast domain, and sharing private host candidates causes
158    /// misleading punch successes when an asymmetric L3 path (corporate VPN,
159    /// Tailscale subnet route, overlapping address space, etc.) makes a
160    /// peer's private IP one-way reachable from this node. Enable only when
161    /// peers are on the same physical LAN and same-LAN punching is wanted.
162    #[serde(default)]
163    pub share_local_candidates: bool,
164    /// Traversal application namespace advertised in the Nostr protocol tag.
165    #[serde(default = "NostrDiscoveryConfig::default_app")]
166    pub app: String,
167    /// Signaling TTL in seconds.
168    #[serde(default = "NostrDiscoveryConfig::default_signal_ttl_secs")]
169    pub signal_ttl_secs: u64,
170    /// Policy for advert-derived endpoint discovery.
171    #[serde(default)]
172    pub policy: NostrDiscoveryPolicy,
173    /// Max number of open-discovery peers queued for outbound retry/connection
174    /// at once. Prevents unbounded queue growth from ambient advert traffic.
175    #[serde(default = "NostrDiscoveryConfig::default_open_discovery_max_pending")]
176    pub open_discovery_max_pending: usize,
177    /// Sort open-discovery adverts by externally supplied peer trust scores.
178    ///
179    /// Off by default. When enabled, positive-rated peers are tried first,
180    /// unknown peers get reserved probe slots, and negatively rated peers are
181    /// deferred behind trusted and unknown peers.
182    #[serde(default)]
183    pub open_discovery_trust_ratings_enabled: bool,
184    /// Number of scarce open-discovery enqueue slots reserved for unknown
185    /// peers when trust sorting is enabled.
186    #[serde(default = "NostrDiscoveryConfig::default_open_discovery_newcomer_probe_slots")]
187    pub open_discovery_newcomer_probe_slots: usize,
188    /// Rating fact scope accepted by the open-discovery trust cache.
189    #[serde(default = "NostrDiscoveryConfig::default_open_discovery_rating_scope")]
190    pub open_discovery_rating_scope: String,
191    /// Historical rating fact lookback window for the relay subscription.
192    /// A value of 0 subscribes without a `since` bound.
193    #[serde(default = "NostrDiscoveryConfig::default_open_discovery_rating_lookback_secs")]
194    pub open_discovery_rating_lookback_secs: u64,
195    /// Signed rating fact authors accepted by the open-discovery trust cache.
196    /// The local node identity is always trusted; this list is for peers or
197    /// crawlers the operator explicitly trusts.
198    #[serde(default)]
199    pub open_discovery_trusted_rating_authors: Vec<String>,
200    /// Local JSON files containing signed rating fact events to load into the
201    /// open-discovery trust cache at startup. Files may be raw event arrays or
202    /// objects with an `events` array, such as `fipsctl ratings export --format
203    /// events` or `htree nostr-index query` output.
204    #[serde(default)]
205    pub open_discovery_rating_event_files: Vec<std::path::PathBuf>,
206    /// Max concurrent inbound traversal offers processed at once.
207    /// Acts as a rate limit against offer spam from relays.
208    #[serde(default = "NostrDiscoveryConfig::default_max_concurrent_incoming_offers")]
209    pub max_concurrent_incoming_offers: usize,
210    /// Max cached overlay adverts retained from relay traffic.
211    /// Bounds memory under ambient advert volume.
212    #[serde(default = "NostrDiscoveryConfig::default_advert_cache_max_entries")]
213    pub advert_cache_max_entries: usize,
214    /// Max seen-session IDs retained for replay detection.
215    /// Oldest entries are evicted when the cap is exceeded.
216    #[serde(default = "NostrDiscoveryConfig::default_seen_sessions_max_entries")]
217    pub seen_sessions_max_entries: usize,
218    /// Overall punch attempt timeout in seconds.
219    #[serde(default = "NostrDiscoveryConfig::default_attempt_timeout_secs")]
220    pub attempt_timeout_secs: u64,
221    /// Replay tracking retention window in seconds.
222    #[serde(default = "NostrDiscoveryConfig::default_replay_window_secs")]
223    pub replay_window_secs: u64,
224    /// Delay before punch traffic starts.
225    #[serde(default = "NostrDiscoveryConfig::default_punch_start_delay_ms")]
226    pub punch_start_delay_ms: u64,
227    /// Interval between punch packets.
228    #[serde(default = "NostrDiscoveryConfig::default_punch_interval_ms")]
229    pub punch_interval_ms: u64,
230    /// How long to keep punching before failure.
231    #[serde(default = "NostrDiscoveryConfig::default_punch_duration_ms")]
232    pub punch_duration_ms: u64,
233    /// Advert TTL in seconds.
234    #[serde(default = "NostrDiscoveryConfig::default_advert_ttl_secs")]
235    pub advert_ttl_secs: u64,
236    /// How often adverts are refreshed in seconds.
237    #[serde(default = "NostrDiscoveryConfig::default_advert_refresh_secs")]
238    pub advert_refresh_secs: u64,
239    /// Settle delay in seconds after Nostr discovery starts before the
240    /// one-shot startup sweep of cached adverts runs. Allows the relay
241    /// subscription backlog to populate the in-memory advert cache.
242    /// Only used under `policy: open`. Default: 5.
243    #[serde(default = "NostrDiscoveryConfig::default_startup_sweep_delay_secs")]
244    pub startup_sweep_delay_secs: u64,
245    /// Maximum age in seconds for cached adverts considered by the
246    /// one-shot startup sweep. Adverts whose `created_at` is older than
247    /// `now - startup_sweep_max_age_secs` are skipped. Only used under
248    /// `policy: open`. Default: 3600 (1 hour).
249    #[serde(default = "NostrDiscoveryConfig::default_startup_sweep_max_age_secs")]
250    pub startup_sweep_max_age_secs: u64,
251    /// Number of consecutive NAT-traversal failures against a peer before
252    /// an extended cooldown is applied to throttle further offer publishes.
253    /// At this threshold the daemon also actively re-fetches the peer's
254    /// advert in built-in `relays` peerfinding mode to evict cache entries for
255    /// peers that have gone away. External providers own refresh in
256    /// `external` mode. Default: 5.
257    #[serde(default = "NostrDiscoveryConfig::default_failure_streak_threshold")]
258    pub failure_streak_threshold: u32,
259    /// Cooldown applied to a peer once `failure_streak_threshold` is hit.
260    /// Suppresses both open-discovery sweep enqueues and per-attempt
261    /// retry firings until elapsed. Default: 1800 (30 minutes).
262    #[serde(default = "NostrDiscoveryConfig::default_extended_cooldown_secs")]
263    pub extended_cooldown_secs: u64,
264    /// Minimum interval between `NAT traversal failed` WARN log lines for
265    /// the same peer. Subsequent failures inside the window log at DEBUG.
266    /// Reduces log spam on public-test nodes with many cache-learned
267    /// peers. Default: 300 (5 minutes).
268    #[serde(default = "NostrDiscoveryConfig::default_warn_log_interval_secs")]
269    pub warn_log_interval_secs: u64,
270    /// Maximum entries retained in the per-npub failure-state map.
271    /// Bounds memory under high cache turnover. Oldest entries (by last
272    /// failure time) evicted when the cap is exceeded. Default: 4096.
273    #[serde(default = "NostrDiscoveryConfig::default_failure_state_max_entries")]
274    pub failure_state_max_entries: usize,
275    /// Cooldown applied after observing a fatal protocol mismatch on a
276    /// Nostr-adopted bootstrap transport (e.g. `Unknown FMP version`
277    /// from a peer running a different FMP-protocol version). Independent
278    /// of `extended_cooldown_secs` and much longer because the mismatch
279    /// is structural — re-traversing the peer is wasted effort until one
280    /// side upgrades. Default: 86400 (24 hours).
281    #[serde(default = "NostrDiscoveryConfig::default_protocol_mismatch_cooldown_secs")]
282    pub protocol_mismatch_cooldown_secs: u64,
283}
284
285impl Default for NostrDiscoveryConfig {
286    fn default() -> Self {
287        let open_discovery_newcomer_probe_slots =
288            Self::default_open_discovery_newcomer_probe_slots();
289        let open_discovery_rating_lookback_secs =
290            Self::default_open_discovery_rating_lookback_secs();
291        Self {
292            enabled: false,
293            advertise: Self::default_advertise(),
294            peerfinding_source: NostrPeerfindingSource::default(),
295            advert_relays: Self::default_advert_relays(),
296            stun_servers: Self::default_stun_servers(),
297            bind_interface: None,
298            share_local_candidates: false,
299            app: Self::default_app(),
300            signal_ttl_secs: Self::default_signal_ttl_secs(),
301            policy: NostrDiscoveryPolicy::default(),
302            open_discovery_max_pending: Self::default_open_discovery_max_pending(),
303            open_discovery_trust_ratings_enabled: false,
304            open_discovery_newcomer_probe_slots,
305            open_discovery_rating_scope: Self::default_open_discovery_rating_scope(),
306            open_discovery_rating_lookback_secs,
307            open_discovery_trusted_rating_authors: Vec::new(),
308            open_discovery_rating_event_files: Vec::new(),
309            max_concurrent_incoming_offers: Self::default_max_concurrent_incoming_offers(),
310            advert_cache_max_entries: Self::default_advert_cache_max_entries(),
311            seen_sessions_max_entries: Self::default_seen_sessions_max_entries(),
312            attempt_timeout_secs: Self::default_attempt_timeout_secs(),
313            replay_window_secs: Self::default_replay_window_secs(),
314            punch_start_delay_ms: Self::default_punch_start_delay_ms(),
315            punch_interval_ms: Self::default_punch_interval_ms(),
316            punch_duration_ms: Self::default_punch_duration_ms(),
317            advert_ttl_secs: Self::default_advert_ttl_secs(),
318            advert_refresh_secs: Self::default_advert_refresh_secs(),
319            startup_sweep_delay_secs: Self::default_startup_sweep_delay_secs(),
320            startup_sweep_max_age_secs: Self::default_startup_sweep_max_age_secs(),
321            failure_streak_threshold: Self::default_failure_streak_threshold(),
322            extended_cooldown_secs: Self::default_extended_cooldown_secs(),
323            warn_log_interval_secs: Self::default_warn_log_interval_secs(),
324            failure_state_max_entries: Self::default_failure_state_max_entries(),
325            protocol_mismatch_cooldown_secs: Self::default_protocol_mismatch_cooldown_secs(),
326        }
327    }
328}
329
330impl NostrDiscoveryConfig {
331    /// Whether FIPS itself should open relay paths for peer adverts.
332    #[must_use]
333    pub const fn uses_relay_peerfinding(&self) -> bool {
334        matches!(self.peerfinding_source, NostrPeerfindingSource::Relays)
335    }
336
337    fn default_advertise() -> bool {
338        true
339    }
340
341    fn default_advert_relays() -> Vec<String> {
342        vec![
343            "wss://relay.damus.io".to_string(),
344            "wss://nos.lol".to_string(),
345            "wss://offchain.pub".to_string(),
346            "wss://temp.iris.to".to_string(),
347        ]
348    }
349
350    fn default_stun_servers() -> Vec<String> {
351        vec![
352            "stun:stun.l.google.com:19302".to_string(),
353            "stun:stun.cloudflare.com:3478".to_string(),
354            "stun:global.stun.twilio.com:3478".to_string(),
355        ]
356    }
357
358    fn default_app() -> String {
359        "fips-overlay-v1".to_string()
360    }
361
362    fn default_signal_ttl_secs() -> u64 {
363        120
364    }
365
366    fn default_open_discovery_max_pending() -> usize {
367        64
368    }
369
370    fn default_open_discovery_newcomer_probe_slots() -> usize {
371        1
372    }
373
374    fn default_open_discovery_rating_scope() -> String {
375        "fips.peer".to_string()
376    }
377
378    fn default_open_discovery_rating_lookback_secs() -> u64 {
379        604_800
380    }
381
382    fn default_max_concurrent_incoming_offers() -> usize {
383        16
384    }
385
386    fn default_advert_cache_max_entries() -> usize {
387        2048
388    }
389
390    fn default_seen_sessions_max_entries() -> usize {
391        2048
392    }
393
394    fn default_attempt_timeout_secs() -> u64 {
395        10
396    }
397
398    fn default_replay_window_secs() -> u64 {
399        300
400    }
401
402    fn default_punch_start_delay_ms() -> u64 {
403        2_000
404    }
405
406    fn default_punch_interval_ms() -> u64 {
407        200
408    }
409
410    fn default_punch_duration_ms() -> u64 {
411        10_000
412    }
413
414    fn default_advert_ttl_secs() -> u64 {
415        3_600
416    }
417
418    fn default_advert_refresh_secs() -> u64 {
419        1_800
420    }
421
422    fn default_startup_sweep_delay_secs() -> u64 {
423        5
424    }
425
426    fn default_startup_sweep_max_age_secs() -> u64 {
427        3_600
428    }
429
430    fn default_failure_streak_threshold() -> u32 {
431        5
432    }
433
434    fn default_extended_cooldown_secs() -> u64 {
435        1_800
436    }
437
438    fn default_warn_log_interval_secs() -> u64 {
439        300
440    }
441
442    fn default_failure_state_max_entries() -> usize {
443        4_096
444    }
445
446    fn default_protocol_mismatch_cooldown_secs() -> u64 {
447        86_400
448    }
449}