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 `~/.fips/instances/*.json`.
42    /// Embedded endpoints with a discovery scope enable this so local VMs,
43    /// browser helpers, and native app processes can find loopback-reachable
44    /// FIPS sockets without polling relays or relying on mDNS loopback.
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. Traversal signaling
116/// remains on the separately configured signaling relays.
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 Nostr-signaled traversal bootstrap.
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    /// Relay URLs used for encrypted signaling events.
142    #[serde(default = "NostrDiscoveryConfig::default_dm_relays")]
143    pub dm_relays: Vec<String>,
144    /// STUN servers used for local reflexive address discovery.
145    /// Outbound observation uses only this local list; peer-advertised STUN
146    /// values are informational and are not treated as egress targets.
147    #[serde(default = "NostrDiscoveryConfig::default_stun_servers")]
148    pub stun_servers: Vec<String>,
149    /// Whether to advertise local (RFC 1918 / ULA) interface addresses as
150    /// host candidates in the traversal offer.
151    ///
152    /// Off by default: in most deployments the relevant peers are not on the
153    /// same broadcast domain, and sharing private host candidates causes
154    /// misleading punch successes when an asymmetric L3 path (corporate VPN,
155    /// Tailscale subnet route, overlapping address space, etc.) makes a
156    /// peer's private IP one-way reachable from this node. Enable only when
157    /// peers are on the same physical LAN and same-LAN punching is wanted.
158    #[serde(default)]
159    pub share_local_candidates: bool,
160    /// Traversal application namespace advertised in the Nostr protocol tag.
161    #[serde(default = "NostrDiscoveryConfig::default_app")]
162    pub app: String,
163    /// Signaling TTL in seconds.
164    #[serde(default = "NostrDiscoveryConfig::default_signal_ttl_secs")]
165    pub signal_ttl_secs: u64,
166    /// Policy for advert-derived endpoint discovery.
167    #[serde(default)]
168    pub policy: NostrDiscoveryPolicy,
169    /// Max number of open-discovery peers queued for outbound retry/connection
170    /// at once. Prevents unbounded queue growth from ambient advert traffic.
171    #[serde(default = "NostrDiscoveryConfig::default_open_discovery_max_pending")]
172    pub open_discovery_max_pending: usize,
173    /// Sort open-discovery adverts by externally supplied peer trust scores.
174    ///
175    /// Off by default. When enabled, positive-rated peers are tried first,
176    /// unknown peers get reserved probe slots, and negatively rated peers are
177    /// deferred behind trusted and unknown peers.
178    #[serde(default)]
179    pub open_discovery_trust_ratings_enabled: bool,
180    /// Number of scarce open-discovery enqueue slots reserved for unknown
181    /// peers when trust sorting is enabled.
182    #[serde(default = "NostrDiscoveryConfig::default_open_discovery_newcomer_probe_slots")]
183    pub open_discovery_newcomer_probe_slots: usize,
184    /// Rating fact scope accepted by the open-discovery trust cache.
185    #[serde(default = "NostrDiscoveryConfig::default_open_discovery_rating_scope")]
186    pub open_discovery_rating_scope: String,
187    /// Historical rating fact lookback window for the relay subscription.
188    /// A value of 0 subscribes without a `since` bound.
189    #[serde(default = "NostrDiscoveryConfig::default_open_discovery_rating_lookback_secs")]
190    pub open_discovery_rating_lookback_secs: u64,
191    /// Signed rating fact authors accepted by the open-discovery trust cache.
192    /// The local node identity is always trusted; this list is for peers or
193    /// crawlers the operator explicitly trusts.
194    #[serde(default)]
195    pub open_discovery_trusted_rating_authors: Vec<String>,
196    /// Local JSON files containing signed rating fact events to load into the
197    /// open-discovery trust cache at startup. Files may be raw event arrays or
198    /// objects with an `events` array, such as `fipsctl ratings export --format
199    /// events` or `htree nostr-index query` output.
200    #[serde(default)]
201    pub open_discovery_rating_event_files: Vec<std::path::PathBuf>,
202    /// Max concurrent inbound traversal offers processed at once.
203    /// Acts as a rate limit against offer spam from relays.
204    #[serde(default = "NostrDiscoveryConfig::default_max_concurrent_incoming_offers")]
205    pub max_concurrent_incoming_offers: usize,
206    /// Max cached overlay adverts retained from relay traffic.
207    /// Bounds memory under ambient advert volume.
208    #[serde(default = "NostrDiscoveryConfig::default_advert_cache_max_entries")]
209    pub advert_cache_max_entries: usize,
210    /// Max seen-session IDs retained for replay detection.
211    /// Oldest entries are evicted when the cap is exceeded.
212    #[serde(default = "NostrDiscoveryConfig::default_seen_sessions_max_entries")]
213    pub seen_sessions_max_entries: usize,
214    /// Overall punch attempt timeout in seconds.
215    #[serde(default = "NostrDiscoveryConfig::default_attempt_timeout_secs")]
216    pub attempt_timeout_secs: u64,
217    /// Replay tracking retention window in seconds.
218    #[serde(default = "NostrDiscoveryConfig::default_replay_window_secs")]
219    pub replay_window_secs: u64,
220    /// Delay before punch traffic starts.
221    #[serde(default = "NostrDiscoveryConfig::default_punch_start_delay_ms")]
222    pub punch_start_delay_ms: u64,
223    /// Interval between punch packets.
224    #[serde(default = "NostrDiscoveryConfig::default_punch_interval_ms")]
225    pub punch_interval_ms: u64,
226    /// How long to keep punching before failure.
227    #[serde(default = "NostrDiscoveryConfig::default_punch_duration_ms")]
228    pub punch_duration_ms: u64,
229    /// Advert TTL in seconds.
230    #[serde(default = "NostrDiscoveryConfig::default_advert_ttl_secs")]
231    pub advert_ttl_secs: u64,
232    /// How often adverts are refreshed in seconds.
233    #[serde(default = "NostrDiscoveryConfig::default_advert_refresh_secs")]
234    pub advert_refresh_secs: u64,
235    /// Settle delay in seconds after Nostr discovery starts before the
236    /// one-shot startup sweep of cached adverts runs. Allows the relay
237    /// subscription backlog to populate the in-memory advert cache.
238    /// Only used under `policy: open`. Default: 5.
239    #[serde(default = "NostrDiscoveryConfig::default_startup_sweep_delay_secs")]
240    pub startup_sweep_delay_secs: u64,
241    /// Maximum age in seconds for cached adverts considered by the
242    /// one-shot startup sweep. Adverts whose `created_at` is older than
243    /// `now - startup_sweep_max_age_secs` are skipped. Only used under
244    /// `policy: open`. Default: 3600 (1 hour).
245    #[serde(default = "NostrDiscoveryConfig::default_startup_sweep_max_age_secs")]
246    pub startup_sweep_max_age_secs: u64,
247    /// Number of consecutive NAT-traversal failures against a peer before
248    /// an extended cooldown is applied to throttle further offer publishes.
249    /// At this threshold the daemon also actively re-fetches the peer's
250    /// advert in built-in `relays` peerfinding mode to evict cache entries for
251    /// peers that have gone away. External providers own refresh in
252    /// `external` mode. Default: 5.
253    #[serde(default = "NostrDiscoveryConfig::default_failure_streak_threshold")]
254    pub failure_streak_threshold: u32,
255    /// Cooldown applied to a peer once `failure_streak_threshold` is hit.
256    /// Suppresses both open-discovery sweep enqueues and per-attempt
257    /// retry firings until elapsed. Default: 1800 (30 minutes).
258    #[serde(default = "NostrDiscoveryConfig::default_extended_cooldown_secs")]
259    pub extended_cooldown_secs: u64,
260    /// Minimum interval between `NAT traversal failed` WARN log lines for
261    /// the same peer. Subsequent failures inside the window log at DEBUG.
262    /// Reduces log spam on public-test nodes with many cache-learned
263    /// peers. Default: 300 (5 minutes).
264    #[serde(default = "NostrDiscoveryConfig::default_warn_log_interval_secs")]
265    pub warn_log_interval_secs: u64,
266    /// Maximum entries retained in the per-npub failure-state map.
267    /// Bounds memory under high cache turnover. Oldest entries (by last
268    /// failure time) evicted when the cap is exceeded. Default: 4096.
269    #[serde(default = "NostrDiscoveryConfig::default_failure_state_max_entries")]
270    pub failure_state_max_entries: usize,
271    /// Cooldown applied after observing a fatal protocol mismatch on a
272    /// Nostr-adopted bootstrap transport (e.g. `Unknown FMP version`
273    /// from a peer running a different FMP-protocol version). Independent
274    /// of `extended_cooldown_secs` and much longer because the mismatch
275    /// is structural — re-traversing the peer is wasted effort until one
276    /// side upgrades. Default: 86400 (24 hours).
277    #[serde(default = "NostrDiscoveryConfig::default_protocol_mismatch_cooldown_secs")]
278    pub protocol_mismatch_cooldown_secs: u64,
279}
280
281impl Default for NostrDiscoveryConfig {
282    fn default() -> Self {
283        let open_discovery_newcomer_probe_slots =
284            Self::default_open_discovery_newcomer_probe_slots();
285        let open_discovery_rating_lookback_secs =
286            Self::default_open_discovery_rating_lookback_secs();
287        Self {
288            enabled: false,
289            advertise: Self::default_advertise(),
290            peerfinding_source: NostrPeerfindingSource::default(),
291            advert_relays: Self::default_advert_relays(),
292            dm_relays: Self::default_dm_relays(),
293            stun_servers: Self::default_stun_servers(),
294            share_local_candidates: false,
295            app: Self::default_app(),
296            signal_ttl_secs: Self::default_signal_ttl_secs(),
297            policy: NostrDiscoveryPolicy::default(),
298            open_discovery_max_pending: Self::default_open_discovery_max_pending(),
299            open_discovery_trust_ratings_enabled: false,
300            open_discovery_newcomer_probe_slots,
301            open_discovery_rating_scope: Self::default_open_discovery_rating_scope(),
302            open_discovery_rating_lookback_secs,
303            open_discovery_trusted_rating_authors: Vec::new(),
304            open_discovery_rating_event_files: Vec::new(),
305            max_concurrent_incoming_offers: Self::default_max_concurrent_incoming_offers(),
306            advert_cache_max_entries: Self::default_advert_cache_max_entries(),
307            seen_sessions_max_entries: Self::default_seen_sessions_max_entries(),
308            attempt_timeout_secs: Self::default_attempt_timeout_secs(),
309            replay_window_secs: Self::default_replay_window_secs(),
310            punch_start_delay_ms: Self::default_punch_start_delay_ms(),
311            punch_interval_ms: Self::default_punch_interval_ms(),
312            punch_duration_ms: Self::default_punch_duration_ms(),
313            advert_ttl_secs: Self::default_advert_ttl_secs(),
314            advert_refresh_secs: Self::default_advert_refresh_secs(),
315            startup_sweep_delay_secs: Self::default_startup_sweep_delay_secs(),
316            startup_sweep_max_age_secs: Self::default_startup_sweep_max_age_secs(),
317            failure_streak_threshold: Self::default_failure_streak_threshold(),
318            extended_cooldown_secs: Self::default_extended_cooldown_secs(),
319            warn_log_interval_secs: Self::default_warn_log_interval_secs(),
320            failure_state_max_entries: Self::default_failure_state_max_entries(),
321            protocol_mismatch_cooldown_secs: Self::default_protocol_mismatch_cooldown_secs(),
322        }
323    }
324}
325
326impl NostrDiscoveryConfig {
327    /// Whether FIPS itself should open relay paths for peer adverts.
328    #[must_use]
329    pub const fn uses_relay_peerfinding(&self) -> bool {
330        matches!(self.peerfinding_source, NostrPeerfindingSource::Relays)
331    }
332
333    fn default_advertise() -> bool {
334        true
335    }
336
337    fn default_advert_relays() -> Vec<String> {
338        vec![
339            "wss://relay.damus.io".to_string(),
340            "wss://nos.lol".to_string(),
341            "wss://offchain.pub".to_string(),
342            "wss://temp.iris.to".to_string(),
343        ]
344    }
345
346    fn default_dm_relays() -> Vec<String> {
347        vec![
348            "wss://relay.damus.io".to_string(),
349            "wss://nos.lol".to_string(),
350            "wss://offchain.pub".to_string(),
351            "wss://temp.iris.to".to_string(),
352        ]
353    }
354
355    fn default_stun_servers() -> Vec<String> {
356        vec![
357            "stun:stun.l.google.com:19302".to_string(),
358            "stun:stun.cloudflare.com:3478".to_string(),
359            "stun:global.stun.twilio.com:3478".to_string(),
360        ]
361    }
362
363    fn default_app() -> String {
364        "fips-overlay-v1".to_string()
365    }
366
367    fn default_signal_ttl_secs() -> u64 {
368        120
369    }
370
371    fn default_open_discovery_max_pending() -> usize {
372        64
373    }
374
375    fn default_open_discovery_newcomer_probe_slots() -> usize {
376        1
377    }
378
379    fn default_open_discovery_rating_scope() -> String {
380        "fips.peer".to_string()
381    }
382
383    fn default_open_discovery_rating_lookback_secs() -> u64 {
384        604_800
385    }
386
387    fn default_max_concurrent_incoming_offers() -> usize {
388        16
389    }
390
391    fn default_advert_cache_max_entries() -> usize {
392        2048
393    }
394
395    fn default_seen_sessions_max_entries() -> usize {
396        2048
397    }
398
399    fn default_attempt_timeout_secs() -> u64 {
400        10
401    }
402
403    fn default_replay_window_secs() -> u64 {
404        300
405    }
406
407    fn default_punch_start_delay_ms() -> u64 {
408        2_000
409    }
410
411    fn default_punch_interval_ms() -> u64 {
412        200
413    }
414
415    fn default_punch_duration_ms() -> u64 {
416        10_000
417    }
418
419    fn default_advert_ttl_secs() -> u64 {
420        3_600
421    }
422
423    fn default_advert_refresh_secs() -> u64 {
424        1_800
425    }
426
427    fn default_startup_sweep_delay_secs() -> u64 {
428        5
429    }
430
431    fn default_startup_sweep_max_age_secs() -> u64 {
432        3_600
433    }
434
435    fn default_failure_streak_threshold() -> u32 {
436        5
437    }
438
439    fn default_extended_cooldown_secs() -> u64 {
440        1_800
441    }
442
443    fn default_warn_log_interval_secs() -> u64 {
444        300
445    }
446
447    fn default_failure_state_max_entries() -> usize {
448        4_096
449    }
450
451    fn default_protocol_mismatch_cooldown_secs() -> u64 {
452        86_400
453    }
454}