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