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    /// Max concurrent inbound traversal offers processed at once.
156    /// Acts as a rate limit against offer spam from relays.
157    #[serde(default = "NostrDiscoveryConfig::default_max_concurrent_incoming_offers")]
158    pub max_concurrent_incoming_offers: usize,
159    /// Max cached overlay adverts retained from relay traffic.
160    /// Bounds memory under ambient advert volume.
161    #[serde(default = "NostrDiscoveryConfig::default_advert_cache_max_entries")]
162    pub advert_cache_max_entries: usize,
163    /// Max seen-session IDs retained for replay detection.
164    /// Oldest entries are evicted when the cap is exceeded.
165    #[serde(default = "NostrDiscoveryConfig::default_seen_sessions_max_entries")]
166    pub seen_sessions_max_entries: usize,
167    /// Overall punch attempt timeout in seconds.
168    #[serde(default = "NostrDiscoveryConfig::default_attempt_timeout_secs")]
169    pub attempt_timeout_secs: u64,
170    /// Replay tracking retention window in seconds.
171    #[serde(default = "NostrDiscoveryConfig::default_replay_window_secs")]
172    pub replay_window_secs: u64,
173    /// Delay before punch traffic starts.
174    #[serde(default = "NostrDiscoveryConfig::default_punch_start_delay_ms")]
175    pub punch_start_delay_ms: u64,
176    /// Interval between punch packets.
177    #[serde(default = "NostrDiscoveryConfig::default_punch_interval_ms")]
178    pub punch_interval_ms: u64,
179    /// How long to keep punching before failure.
180    #[serde(default = "NostrDiscoveryConfig::default_punch_duration_ms")]
181    pub punch_duration_ms: u64,
182    /// Advert TTL in seconds.
183    #[serde(default = "NostrDiscoveryConfig::default_advert_ttl_secs")]
184    pub advert_ttl_secs: u64,
185    /// How often adverts are refreshed in seconds.
186    #[serde(default = "NostrDiscoveryConfig::default_advert_refresh_secs")]
187    pub advert_refresh_secs: u64,
188    /// Settle delay in seconds after Nostr discovery starts before the
189    /// one-shot startup sweep of cached adverts runs. Allows the relay
190    /// subscription backlog to populate the in-memory advert cache.
191    /// Only used under `policy: open`. Default: 5.
192    #[serde(default = "NostrDiscoveryConfig::default_startup_sweep_delay_secs")]
193    pub startup_sweep_delay_secs: u64,
194    /// Maximum age in seconds for cached adverts considered by the
195    /// one-shot startup sweep. Adverts whose `created_at` is older than
196    /// `now - startup_sweep_max_age_secs` are skipped. Only used under
197    /// `policy: open`. Default: 3600 (1 hour).
198    #[serde(default = "NostrDiscoveryConfig::default_startup_sweep_max_age_secs")]
199    pub startup_sweep_max_age_secs: u64,
200    /// Number of consecutive NAT-traversal failures against a peer before
201    /// an extended cooldown is applied to throttle further offer publishes.
202    /// At this threshold the daemon also actively re-fetches the peer's
203    /// advert from `advert_relays` to evict cache entries for peers that
204    /// have gone away. Default: 5.
205    #[serde(default = "NostrDiscoveryConfig::default_failure_streak_threshold")]
206    pub failure_streak_threshold: u32,
207    /// Cooldown applied to a peer once `failure_streak_threshold` is hit.
208    /// Suppresses both open-discovery sweep enqueues and per-attempt
209    /// retry firings until elapsed. Default: 1800 (30 minutes).
210    #[serde(default = "NostrDiscoveryConfig::default_extended_cooldown_secs")]
211    pub extended_cooldown_secs: u64,
212    /// Minimum interval between `NAT traversal failed` WARN log lines for
213    /// the same peer. Subsequent failures inside the window log at DEBUG.
214    /// Reduces log spam on public-test nodes with many cache-learned
215    /// peers. Default: 300 (5 minutes).
216    #[serde(default = "NostrDiscoveryConfig::default_warn_log_interval_secs")]
217    pub warn_log_interval_secs: u64,
218    /// Maximum entries retained in the per-npub failure-state map.
219    /// Bounds memory under high cache turnover. Oldest entries (by last
220    /// failure time) evicted when the cap is exceeded. Default: 4096.
221    #[serde(default = "NostrDiscoveryConfig::default_failure_state_max_entries")]
222    pub failure_state_max_entries: usize,
223    /// Cooldown applied after observing a fatal protocol mismatch on a
224    /// Nostr-adopted bootstrap transport (e.g. `Unknown FMP version`
225    /// from a peer running a different FMP-protocol version). Independent
226    /// of `extended_cooldown_secs` and much longer because the mismatch
227    /// is structural — re-traversing the peer is wasted effort until one
228    /// side upgrades. Default: 86400 (24 hours).
229    #[serde(default = "NostrDiscoveryConfig::default_protocol_mismatch_cooldown_secs")]
230    pub protocol_mismatch_cooldown_secs: u64,
231}
232
233impl Default for NostrDiscoveryConfig {
234    fn default() -> Self {
235        Self {
236            enabled: false,
237            advertise: Self::default_advertise(),
238            advert_relays: Self::default_advert_relays(),
239            dm_relays: Self::default_dm_relays(),
240            stun_servers: Self::default_stun_servers(),
241            share_local_candidates: false,
242            app: Self::default_app(),
243            signal_ttl_secs: Self::default_signal_ttl_secs(),
244            policy: NostrDiscoveryPolicy::default(),
245            open_discovery_max_pending: Self::default_open_discovery_max_pending(),
246            max_concurrent_incoming_offers: Self::default_max_concurrent_incoming_offers(),
247            advert_cache_max_entries: Self::default_advert_cache_max_entries(),
248            seen_sessions_max_entries: Self::default_seen_sessions_max_entries(),
249            attempt_timeout_secs: Self::default_attempt_timeout_secs(),
250            replay_window_secs: Self::default_replay_window_secs(),
251            punch_start_delay_ms: Self::default_punch_start_delay_ms(),
252            punch_interval_ms: Self::default_punch_interval_ms(),
253            punch_duration_ms: Self::default_punch_duration_ms(),
254            advert_ttl_secs: Self::default_advert_ttl_secs(),
255            advert_refresh_secs: Self::default_advert_refresh_secs(),
256            startup_sweep_delay_secs: Self::default_startup_sweep_delay_secs(),
257            startup_sweep_max_age_secs: Self::default_startup_sweep_max_age_secs(),
258            failure_streak_threshold: Self::default_failure_streak_threshold(),
259            extended_cooldown_secs: Self::default_extended_cooldown_secs(),
260            warn_log_interval_secs: Self::default_warn_log_interval_secs(),
261            failure_state_max_entries: Self::default_failure_state_max_entries(),
262            protocol_mismatch_cooldown_secs: Self::default_protocol_mismatch_cooldown_secs(),
263        }
264    }
265}
266
267impl NostrDiscoveryConfig {
268    fn default_advertise() -> bool {
269        true
270    }
271
272    fn default_advert_relays() -> Vec<String> {
273        vec![
274            "wss://relay.damus.io".to_string(),
275            "wss://nos.lol".to_string(),
276            "wss://offchain.pub".to_string(),
277            "wss://temp.iris.to".to_string(),
278        ]
279    }
280
281    fn default_dm_relays() -> Vec<String> {
282        vec![
283            "wss://relay.damus.io".to_string(),
284            "wss://nos.lol".to_string(),
285            "wss://offchain.pub".to_string(),
286            "wss://temp.iris.to".to_string(),
287        ]
288    }
289
290    fn default_stun_servers() -> Vec<String> {
291        vec![
292            "stun:stun.l.google.com:19302".to_string(),
293            "stun:stun.cloudflare.com:3478".to_string(),
294            "stun:global.stun.twilio.com:3478".to_string(),
295        ]
296    }
297
298    fn default_app() -> String {
299        "fips-overlay-v1".to_string()
300    }
301
302    fn default_signal_ttl_secs() -> u64 {
303        120
304    }
305
306    fn default_open_discovery_max_pending() -> usize {
307        64
308    }
309
310    fn default_max_concurrent_incoming_offers() -> usize {
311        16
312    }
313
314    fn default_advert_cache_max_entries() -> usize {
315        2048
316    }
317
318    fn default_seen_sessions_max_entries() -> usize {
319        2048
320    }
321
322    fn default_attempt_timeout_secs() -> u64 {
323        10
324    }
325
326    fn default_replay_window_secs() -> u64 {
327        300
328    }
329
330    fn default_punch_start_delay_ms() -> u64 {
331        2_000
332    }
333
334    fn default_punch_interval_ms() -> u64 {
335        200
336    }
337
338    fn default_punch_duration_ms() -> u64 {
339        10_000
340    }
341
342    fn default_advert_ttl_secs() -> u64 {
343        3_600
344    }
345
346    fn default_advert_refresh_secs() -> u64 {
347        1_800
348    }
349
350    fn default_startup_sweep_delay_secs() -> u64 {
351        5
352    }
353
354    fn default_startup_sweep_max_age_secs() -> u64 {
355        3_600
356    }
357
358    fn default_failure_streak_threshold() -> u32 {
359        5
360    }
361
362    fn default_extended_cooldown_secs() -> u64 {
363        1_800
364    }
365
366    fn default_warn_log_interval_secs() -> u64 {
367        300
368    }
369
370    fn default_failure_state_max_entries() -> usize {
371        4_096
372    }
373
374    fn default_protocol_mismatch_cooldown_secs() -> u64 {
375        86_400
376    }
377}