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 concurrent inbound traversal offers from one sender npub.
211    #[serde(default = "NostrDiscoveryConfig::default_max_concurrent_offers_per_npub")]
212    pub max_concurrent_offers_per_npub: usize,
213    /// Max cached overlay adverts retained from relay traffic.
214    /// Bounds memory under ambient advert volume.
215    #[serde(default = "NostrDiscoveryConfig::default_advert_cache_max_entries")]
216    pub advert_cache_max_entries: usize,
217    /// Max seen-session IDs retained for replay detection.
218    /// Oldest entries are evicted when the cap is exceeded.
219    #[serde(default = "NostrDiscoveryConfig::default_seen_sessions_max_entries")]
220    pub seen_sessions_max_entries: usize,
221    /// Overall punch attempt timeout in seconds.
222    #[serde(default = "NostrDiscoveryConfig::default_attempt_timeout_secs")]
223    pub attempt_timeout_secs: u64,
224    /// Replay tracking retention window in seconds.
225    #[serde(default = "NostrDiscoveryConfig::default_replay_window_secs")]
226    pub replay_window_secs: u64,
227    /// Delay before punch traffic starts.
228    #[serde(default = "NostrDiscoveryConfig::default_punch_start_delay_ms")]
229    pub punch_start_delay_ms: u64,
230    /// Interval between punch packets.
231    #[serde(default = "NostrDiscoveryConfig::default_punch_interval_ms")]
232    pub punch_interval_ms: u64,
233    /// How long to keep punching before failure.
234    #[serde(default = "NostrDiscoveryConfig::default_punch_duration_ms")]
235    pub punch_duration_ms: u64,
236    /// Advert TTL in seconds.
237    #[serde(default = "NostrDiscoveryConfig::default_advert_ttl_secs")]
238    pub advert_ttl_secs: u64,
239    /// How often adverts are refreshed in seconds.
240    #[serde(default = "NostrDiscoveryConfig::default_advert_refresh_secs")]
241    pub advert_refresh_secs: u64,
242    /// Settle delay in seconds after Nostr discovery starts before the
243    /// one-shot startup sweep of cached adverts runs. Allows the relay
244    /// subscription backlog to populate the in-memory advert cache.
245    /// Only used under `policy: open`. Default: 5.
246    #[serde(default = "NostrDiscoveryConfig::default_startup_sweep_delay_secs")]
247    pub startup_sweep_delay_secs: u64,
248    /// Maximum age in seconds for cached adverts considered by the
249    /// one-shot startup sweep. Adverts whose `created_at` is older than
250    /// `now - startup_sweep_max_age_secs` are skipped. Only used under
251    /// `policy: open`. Default: 3600 (1 hour).
252    #[serde(default = "NostrDiscoveryConfig::default_startup_sweep_max_age_secs")]
253    pub startup_sweep_max_age_secs: u64,
254    /// Number of consecutive NAT-traversal failures against a peer before
255    /// an extended cooldown is applied to throttle further offer publishes.
256    /// At this threshold the daemon also actively re-fetches the peer's
257    /// advert in built-in `relays` peerfinding mode to evict cache entries for
258    /// peers that have gone away. External providers own refresh in
259    /// `external` mode. Default: 5.
260    #[serde(default = "NostrDiscoveryConfig::default_failure_streak_threshold")]
261    pub failure_streak_threshold: u32,
262    /// Cooldown applied to a peer once `failure_streak_threshold` is hit.
263    /// Suppresses both open-discovery sweep enqueues and per-attempt
264    /// retry firings until elapsed. Default: 1800 (30 minutes).
265    #[serde(default = "NostrDiscoveryConfig::default_extended_cooldown_secs")]
266    pub extended_cooldown_secs: u64,
267    /// Minimum interval between `NAT traversal failed` WARN log lines for
268    /// the same peer. Subsequent failures inside the window log at DEBUG.
269    /// Reduces log spam on public-test nodes with many cache-learned
270    /// peers. Default: 300 (5 minutes).
271    #[serde(default = "NostrDiscoveryConfig::default_warn_log_interval_secs")]
272    pub warn_log_interval_secs: u64,
273    /// Maximum entries retained in the per-npub failure-state map.
274    /// Bounds memory under high cache turnover. Oldest entries (by last
275    /// failure time) evicted when the cap is exceeded. Default: 4096.
276    #[serde(default = "NostrDiscoveryConfig::default_failure_state_max_entries")]
277    pub failure_state_max_entries: usize,
278    /// Cooldown applied after observing a fatal protocol mismatch on a
279    /// Nostr-adopted bootstrap transport (e.g. `Unknown FMP version`
280    /// from a peer running a different FMP-protocol version). Independent
281    /// of `extended_cooldown_secs` and much longer because the mismatch
282    /// is structural — re-traversing the peer is wasted effort until one
283    /// side upgrades. Default: 86400 (24 hours).
284    #[serde(default = "NostrDiscoveryConfig::default_protocol_mismatch_cooldown_secs")]
285    pub protocol_mismatch_cooldown_secs: u64,
286}
287
288impl Default for NostrDiscoveryConfig {
289    fn default() -> Self {
290        let open_discovery_newcomer_probe_slots =
291            Self::default_open_discovery_newcomer_probe_slots();
292        let open_discovery_rating_lookback_secs =
293            Self::default_open_discovery_rating_lookback_secs();
294        Self {
295            enabled: false,
296            advertise: Self::default_advertise(),
297            peerfinding_source: NostrPeerfindingSource::default(),
298            advert_relays: Self::default_advert_relays(),
299            stun_servers: Self::default_stun_servers(),
300            bind_interface: None,
301            share_local_candidates: false,
302            app: Self::default_app(),
303            signal_ttl_secs: Self::default_signal_ttl_secs(),
304            policy: NostrDiscoveryPolicy::default(),
305            open_discovery_max_pending: Self::default_open_discovery_max_pending(),
306            open_discovery_trust_ratings_enabled: false,
307            open_discovery_newcomer_probe_slots,
308            open_discovery_rating_scope: Self::default_open_discovery_rating_scope(),
309            open_discovery_rating_lookback_secs,
310            open_discovery_trusted_rating_authors: Vec::new(),
311            open_discovery_rating_event_files: Vec::new(),
312            max_concurrent_incoming_offers: Self::default_max_concurrent_incoming_offers(),
313            max_concurrent_offers_per_npub: Self::default_max_concurrent_offers_per_npub(),
314            advert_cache_max_entries: Self::default_advert_cache_max_entries(),
315            seen_sessions_max_entries: Self::default_seen_sessions_max_entries(),
316            attempt_timeout_secs: Self::default_attempt_timeout_secs(),
317            replay_window_secs: Self::default_replay_window_secs(),
318            punch_start_delay_ms: Self::default_punch_start_delay_ms(),
319            punch_interval_ms: Self::default_punch_interval_ms(),
320            punch_duration_ms: Self::default_punch_duration_ms(),
321            advert_ttl_secs: Self::default_advert_ttl_secs(),
322            advert_refresh_secs: Self::default_advert_refresh_secs(),
323            startup_sweep_delay_secs: Self::default_startup_sweep_delay_secs(),
324            startup_sweep_max_age_secs: Self::default_startup_sweep_max_age_secs(),
325            failure_streak_threshold: Self::default_failure_streak_threshold(),
326            extended_cooldown_secs: Self::default_extended_cooldown_secs(),
327            warn_log_interval_secs: Self::default_warn_log_interval_secs(),
328            failure_state_max_entries: Self::default_failure_state_max_entries(),
329            protocol_mismatch_cooldown_secs: Self::default_protocol_mismatch_cooldown_secs(),
330        }
331    }
332}
333
334impl NostrDiscoveryConfig {
335    /// Whether FIPS itself should open relay paths for peer adverts.
336    #[must_use]
337    pub const fn uses_relay_peerfinding(&self) -> bool {
338        matches!(self.peerfinding_source, NostrPeerfindingSource::Relays)
339    }
340
341    fn default_advertise() -> bool {
342        true
343    }
344
345    fn default_advert_relays() -> Vec<String> {
346        vec![
347            "wss://relay.damus.io".to_string(),
348            "wss://nos.lol".to_string(),
349            "wss://offchain.pub".to_string(),
350            "wss://temp.iris.to".to_string(),
351        ]
352    }
353
354    fn default_stun_servers() -> Vec<String> {
355        vec![
356            "stun:stun.l.google.com:19302".to_string(),
357            "stun:stun.cloudflare.com:3478".to_string(),
358            "stun:global.stun.twilio.com:3478".to_string(),
359        ]
360    }
361
362    fn default_app() -> String {
363        "fips-overlay-v1".to_string()
364    }
365
366    fn default_signal_ttl_secs() -> u64 {
367        120
368    }
369
370    fn default_open_discovery_max_pending() -> usize {
371        64
372    }
373
374    fn default_open_discovery_newcomer_probe_slots() -> usize {
375        1
376    }
377
378    fn default_open_discovery_rating_scope() -> String {
379        "fips.peer".to_string()
380    }
381
382    fn default_open_discovery_rating_lookback_secs() -> u64 {
383        604_800
384    }
385
386    fn default_max_concurrent_incoming_offers() -> usize {
387        16
388    }
389
390    fn default_max_concurrent_offers_per_npub() -> usize {
391        4
392    }
393
394    fn default_advert_cache_max_entries() -> usize {
395        2048
396    }
397
398    fn default_seen_sessions_max_entries() -> usize {
399        2048
400    }
401
402    fn default_attempt_timeout_secs() -> u64 {
403        10
404    }
405
406    fn default_replay_window_secs() -> u64 {
407        300
408    }
409
410    fn default_punch_start_delay_ms() -> u64 {
411        2_000
412    }
413
414    fn default_punch_interval_ms() -> u64 {
415        200
416    }
417
418    fn default_punch_duration_ms() -> u64 {
419        10_000
420    }
421
422    fn default_advert_ttl_secs() -> u64 {
423        3_600
424    }
425
426    fn default_advert_refresh_secs() -> u64 {
427        1_800
428    }
429
430    fn default_startup_sweep_delay_secs() -> u64 {
431        5
432    }
433
434    fn default_startup_sweep_max_age_secs() -> u64 {
435        3_600
436    }
437
438    fn default_failure_streak_threshold() -> u32 {
439        5
440    }
441
442    fn default_extended_cooldown_secs() -> u64 {
443        1_800
444    }
445
446    fn default_warn_log_interval_secs() -> u64 {
447        300
448    }
449
450    fn default_failure_state_max_entries() -> usize {
451        4_096
452    }
453
454    fn default_protocol_mismatch_cooldown_secs() -> u64 {
455        86_400
456    }
457}