Skip to main content

fips_core/config/
transport.rs

1//! Transport configuration types.
2//!
3//! Generic transport instance handling (single vs. named) and
4//! transport-specific configuration structs.
5
6use std::collections::HashMap;
7use std::net::{IpAddr, SocketAddr};
8
9use serde::{Deserialize, Serialize};
10
11/// Parse an `external_addr` config string against a known bind port,
12/// producing the absolute `SocketAddr` to advertise on Nostr.
13///
14/// Accepts either a bare IP (`"198.51.100.1"` or `"[::1]"`) — in which
15/// case the bind port is appended — or a full `host:port` form
16/// (`"198.51.100.1:443"` or `"[::1]:443"`). Returns `None` on any parse
17/// error. IPv6 must use bracket notation when supplying a port.
18fn parse_external_advert_addr(raw: &str, bind_port: u16) -> Option<SocketAddr> {
19    if let Ok(sa) = raw.parse::<SocketAddr>() {
20        return Some(sa);
21    }
22    let ip: IpAddr = raw.parse().ok()?;
23    Some(SocketAddr::new(ip, bind_port))
24}
25
26/// Extract the port from a `bind_addr` string. Returns `None` if the
27/// string can't be parsed (e.g. a bare hostname without port).
28fn parse_bind_port(raw: &str) -> Option<u16> {
29    raw.parse::<SocketAddr>().ok().map(|sa| sa.port())
30}
31
32/// Default UDP bind address.
33const DEFAULT_UDP_BIND_ADDR: &str = "0.0.0.0:2121";
34
35/// Default UDP MTU (IPv6 minimum).
36const DEFAULT_UDP_MTU: u16 = 1280;
37
38/// Default UDP receive buffer size (16 MiB).
39///
40/// At sustained multi-Gbps single-stream the kernel UDP queue
41/// drained ~113 kpps × ~1.5 KiB ≈ 170 MiB/s, so a few-hundred-ms
42/// userspace stall would fill a 2 MiB buffer in <20 ms — small
43/// enough that ordinary jitter (GC, allocator-coalesce, scheduler
44/// context-switch on a busy host) trips RcvbufErrors and tanks TCP
45/// throughput via cwnd-halving. 16 MiB gives ~100 ms of headroom.
46///
47/// On platforms whose `net.core.rmem_max` is smaller than this, the
48/// UDP socket layer falls back to SO_RCVBUFFORCE (CAP_NET_ADMIN
49/// required) before honouring the kernel ceiling. See
50/// `transport/udp/socket.rs::UdpRawSocket::open`.
51const DEFAULT_UDP_RECV_BUF: usize = 16 * 1024 * 1024;
52
53/// Default UDP send buffer size (8 MiB). Mirrors the receive-side
54/// reasoning at half the size — outbound burst absorption matters
55/// less because we control the producer rate via the rx_loop's
56/// per-drain sendmmsg flush.
57const DEFAULT_UDP_SEND_BUF: usize = 8 * 1024 * 1024;
58
59/// UDP transport instance configuration.
60#[derive(Debug, Clone, Default, Serialize, Deserialize)]
61#[serde(deny_unknown_fields)]
62pub struct UdpConfig {
63    /// Bind address (`bind_addr`). Defaults to "0.0.0.0:2121".
64    ///
65    /// When `outbound_only = true`, this field is ignored and the transport
66    /// binds to `0.0.0.0:0` (kernel-assigned ephemeral port) regardless.
67    #[serde(default, skip_serializing_if = "Option::is_none")]
68    pub bind_addr: Option<String>,
69
70    /// UDP MTU (`mtu`). Defaults to 1280 (IPv6 minimum).
71    #[serde(default, skip_serializing_if = "Option::is_none")]
72    pub mtu: Option<u16>,
73
74    /// UDP receive buffer size in bytes (`recv_buf_size`). Defaults to 16 MiB.
75    #[serde(default, skip_serializing_if = "Option::is_none")]
76    pub recv_buf_size: Option<usize>,
77
78    /// UDP send buffer size in bytes (`send_buf_size`). Defaults to 8 MiB.
79    #[serde(default, skip_serializing_if = "Option::is_none")]
80    pub send_buf_size: Option<usize>,
81
82    /// Whether this transport should be advertised on Nostr overlay discovery.
83    /// Default: false. Implicitly forced false when `outbound_only = true`.
84    #[serde(default, skip_serializing_if = "Option::is_none")]
85    pub advertise_on_nostr: Option<bool>,
86
87    /// Whether UDP should be advertised as directly reachable (`host:port`) on
88    /// Nostr. When false and advertised, UDP is emitted as `addr: "nat"` to
89    /// trigger rendezvous traversal.
90    ///
91    /// Default: false.
92    #[serde(default, skip_serializing_if = "Option::is_none")]
93    pub public: Option<bool>,
94    /// Optional explicit public address to advertise when `public: true`
95    /// is set. Takes precedence over both the bound address and any
96    /// STUN-derived autodiscovery. Accepts either a bare IP
97    /// (`"198.51.100.1"` — the configured `bind_addr` port is appended)
98    /// or a full `host:port` (`"198.51.100.1:443"`). Useful when the
99    /// public IP isn't on a local interface (e.g. AWS EIP / cloud 1:1
100    /// NAT) and the operator wants to skip STUN autodiscovery for a
101    /// deterministic value.
102    #[serde(default, skip_serializing_if = "Option::is_none")]
103    pub external_addr: Option<String>,
104    /// Outbound-only mode. When true, the transport binds to a kernel-
105    /// assigned ephemeral port (`0.0.0.0:0`) instead of the configured
106    /// `bind_addr`, refuses inbound handshake msg1, and is never
107    /// advertised on Nostr regardless of `advertise_on_nostr`. Use this
108    /// to participate in the mesh as a pure client — initiate outbound
109    /// links without exposing an inbound listener on a known port.
110    /// Default: false.
111    #[serde(default, skip_serializing_if = "Option::is_none")]
112    pub outbound_only: Option<bool>,
113
114    /// Accept inbound handshake msg1 from new peers. Default: true.
115    /// Setting this to false combined with `auto_connect: true` on
116    /// peer-side configurations gives a "client" posture: this node
117    /// initiates outbound links but refuses inbound handshakes from
118    /// unfamiliar addresses. The Node-level gate at
119    /// `src/node/handlers/handshake.rs` carves out msg1 from peers
120    /// already established on this transport (so rekey continues to
121    /// work) — see ISSUE-2026-0004.
122    #[serde(default, skip_serializing_if = "Option::is_none")]
123    pub accept_connections: Option<bool>,
124}
125
126impl UdpConfig {
127    /// Get the bind address, using default if not configured.
128    ///
129    /// When `outbound_only = true`, returns `0.0.0.0:0` so the kernel picks
130    /// an ephemeral source port and no listener is exposed on a known port.
131    pub fn bind_addr(&self) -> &str {
132        if self.outbound_only() {
133            "0.0.0.0:0"
134        } else {
135            self.bind_addr.as_deref().unwrap_or(DEFAULT_UDP_BIND_ADDR)
136        }
137    }
138
139    /// Get the UDP MTU, using default if not configured.
140    pub fn mtu(&self) -> u16 {
141        self.mtu.unwrap_or(DEFAULT_UDP_MTU)
142    }
143
144    /// Get the receive buffer size, using default if not configured.
145    pub fn recv_buf_size(&self) -> usize {
146        self.recv_buf_size.unwrap_or(DEFAULT_UDP_RECV_BUF)
147    }
148
149    /// Get the send buffer size, using default if not configured.
150    pub fn send_buf_size(&self) -> usize {
151        self.send_buf_size.unwrap_or(DEFAULT_UDP_SEND_BUF)
152    }
153
154    /// Whether this UDP transport should be advertised on Nostr discovery.
155    /// Always false when `outbound_only = true`.
156    pub fn advertise_on_nostr(&self) -> bool {
157        if self.outbound_only() {
158            false
159        } else {
160            self.advertise_on_nostr.unwrap_or(false)
161        }
162    }
163
164    /// Whether this UDP transport should be advertised as directly reachable.
165    pub fn is_public(&self) -> bool {
166        self.public.unwrap_or(false)
167    }
168
169    /// Parse `external_addr` against the configured `bind_addr` port,
170    /// returning the absolute `SocketAddr` to advertise on Nostr.
171    /// Returns `None` if `external_addr` is unset or malformed, or if
172    /// the port cannot be inferred.
173    pub fn external_advert_addr(&self) -> Option<SocketAddr> {
174        let raw = self.external_addr.as_deref()?;
175        let bind_port = parse_bind_port(self.bind_addr())?;
176        parse_external_advert_addr(raw, bind_port)
177    }
178
179    /// Whether this transport runs in outbound-only mode. Default: false.
180    pub fn outbound_only(&self) -> bool {
181        self.outbound_only.unwrap_or(false)
182    }
183
184    /// Whether this transport accepts inbound handshakes. Default: true.
185    pub fn accept_connections(&self) -> bool {
186        self.accept_connections.unwrap_or(true)
187    }
188}
189
190/// Default simulated transport MTU (IPv6 minimum).
191#[cfg(feature = "sim-transport")]
192const DEFAULT_SIM_MTU: u16 = 1280;
193
194/// Default simulated network registry name.
195#[cfg(feature = "sim-transport")]
196const DEFAULT_SIM_NETWORK: &str = "default";
197
198/// In-memory simulated transport instance configuration.
199///
200/// This transport is intended for production-backed simulations. It uses the
201/// normal node/session/routing stack, but delivers transport packets through a
202/// registered in-process network that can model latency, throughput, loss, and
203/// churn without binding real sockets.
204#[cfg(feature = "sim-transport")]
205#[derive(Debug, Clone, Default, Serialize, Deserialize)]
206#[serde(deny_unknown_fields)]
207pub struct SimTransportConfig {
208    /// Registry name of the in-process simulated network.
209    #[serde(default, skip_serializing_if = "Option::is_none")]
210    pub network: Option<String>,
211
212    /// Address of this simulated endpoint within the network.
213    #[serde(default, skip_serializing_if = "Option::is_none")]
214    pub addr: Option<String>,
215
216    /// Transport MTU. Defaults to 1280.
217    #[serde(default, skip_serializing_if = "Option::is_none")]
218    pub mtu: Option<u16>,
219
220    /// Whether discovery should auto-connect to discovered peers.
221    #[serde(default, skip_serializing_if = "Option::is_none")]
222    pub auto_connect: Option<bool>,
223
224    /// Accept inbound handshake msg1 from new peers. Default: true.
225    #[serde(default, skip_serializing_if = "Option::is_none")]
226    pub accept_connections: Option<bool>,
227}
228
229#[cfg(feature = "sim-transport")]
230impl SimTransportConfig {
231    /// Registry name of the in-process simulated network.
232    pub fn network(&self) -> &str {
233        self.network.as_deref().unwrap_or(DEFAULT_SIM_NETWORK)
234    }
235
236    /// Get the simulated MTU.
237    pub fn mtu(&self) -> u16 {
238        self.mtu.unwrap_or(DEFAULT_SIM_MTU)
239    }
240
241    /// Whether this transport auto-connects to discovered peers.
242    pub fn auto_connect(&self) -> bool {
243        self.auto_connect.unwrap_or(false)
244    }
245
246    /// Whether this transport accepts inbound handshakes.
247    pub fn accept_connections(&self) -> bool {
248        self.accept_connections.unwrap_or(true)
249    }
250}
251
252/// Transport instances - either a single config or named instances.
253///
254/// Allows both simple single-instance config:
255/// ```yaml
256/// transports:
257///   udp:
258///     bind_addr: "0.0.0.0:2121"
259/// ```
260///
261/// And multiple named instances:
262/// ```yaml
263/// transports:
264///   udp:
265///     main:
266///       bind_addr: "0.0.0.0:2121"
267///     backup:
268///       bind_addr: "192.168.1.100:2122"
269/// ```
270#[derive(Debug, Clone, Serialize, Deserialize)]
271#[serde(untagged)]
272pub enum TransportInstances<T> {
273    /// Single unnamed instance (config fields directly under transport type).
274    Single(T),
275    /// Multiple named instances.
276    Named(HashMap<String, T>),
277}
278
279impl<T> TransportInstances<T> {
280    /// Get the number of instances.
281    pub fn len(&self) -> usize {
282        match self {
283            TransportInstances::Single(_) => 1,
284            TransportInstances::Named(map) => map.len(),
285        }
286    }
287
288    /// Check if there are no instances.
289    pub fn is_empty(&self) -> bool {
290        match self {
291            TransportInstances::Single(_) => false,
292            TransportInstances::Named(map) => map.is_empty(),
293        }
294    }
295
296    /// Iterate over all instances as (name, config) pairs.
297    ///
298    /// Single instances have `None` as the name.
299    /// Named instances have `Some(name)`.
300    pub fn iter(&self) -> impl Iterator<Item = (Option<&str>, &T)> {
301        match self {
302            TransportInstances::Single(config) => vec![(None, config)].into_iter(),
303            TransportInstances::Named(map) => map
304                .iter()
305                .map(|(k, v)| (Some(k.as_str()), v))
306                .collect::<Vec<_>>()
307                .into_iter(),
308        }
309    }
310}
311
312impl<T> Default for TransportInstances<T> {
313    fn default() -> Self {
314        TransportInstances::Named(HashMap::new())
315    }
316}
317
318/// Default Ethernet EtherType (FIPS default).
319const DEFAULT_ETHERNET_ETHERTYPE: u16 = 0x2121;
320
321/// Default Ethernet receive buffer size (2 MB).
322const DEFAULT_ETHERNET_RECV_BUF: usize = 2 * 1024 * 1024;
323
324/// Default Ethernet send buffer size (2 MB).
325const DEFAULT_ETHERNET_SEND_BUF: usize = 2 * 1024 * 1024;
326
327/// Default beacon announcement interval in seconds.
328const DEFAULT_BEACON_INTERVAL_SECS: u64 = 30;
329
330/// Minimum beacon announcement interval in seconds.
331const MIN_BEACON_INTERVAL_SECS: u64 = 10;
332
333/// Ethernet transport instance configuration.
334///
335/// EthernetConfig is always compiled (for config parsing on any platform),
336/// but the transport runtime currently requires Linux or macOS raw sockets.
337#[derive(Debug, Clone, Default, Serialize, Deserialize)]
338#[serde(deny_unknown_fields)]
339pub struct EthernetConfig {
340    /// Network interface name (e.g., "eth0", "enp3s0"). Required.
341    pub interface: String,
342
343    /// Custom EtherType (default: 0x2121).
344    #[serde(default, skip_serializing_if = "Option::is_none")]
345    pub ethertype: Option<u16>,
346
347    /// MTU override. Defaults to the interface's MTU minus 1 (for frame type prefix).
348    /// Cannot exceed the interface's actual MTU.
349    #[serde(default, skip_serializing_if = "Option::is_none")]
350    pub mtu: Option<u16>,
351
352    /// Receive buffer size in bytes. Default: 2 MB.
353    #[serde(default, skip_serializing_if = "Option::is_none")]
354    pub recv_buf_size: Option<usize>,
355
356    /// Send buffer size in bytes. Default: 2 MB.
357    #[serde(default, skip_serializing_if = "Option::is_none")]
358    pub send_buf_size: Option<usize>,
359
360    /// Listen for discovery beacons from other nodes. Default: true.
361    #[serde(default, skip_serializing_if = "Option::is_none")]
362    pub discovery: Option<bool>,
363
364    /// Broadcast announcement beacons on the LAN. Default: false.
365    #[serde(default, skip_serializing_if = "Option::is_none")]
366    pub announce: Option<bool>,
367
368    /// Auto-connect to discovered peers. Default: false.
369    #[serde(default, skip_serializing_if = "Option::is_none")]
370    pub auto_connect: Option<bool>,
371
372    /// Accept incoming connection attempts. Default: false.
373    #[serde(default, skip_serializing_if = "Option::is_none")]
374    pub accept_connections: Option<bool>,
375
376    /// Optional discovery scope carried in Ethernet beacons.
377    ///
378    /// When set, this transport ignores Ethernet beacons from other scopes.
379    /// This is a discovery/noise filter, not an access-control mechanism. If
380    /// unset, the node-level LAN discovery scope is used when available.
381    #[serde(default, skip_serializing_if = "Option::is_none")]
382    pub discovery_scope: Option<String>,
383
384    /// Announcement beacon interval in seconds. Default: 30.
385    #[serde(default, skip_serializing_if = "Option::is_none")]
386    pub beacon_interval_secs: Option<u64>,
387}
388
389impl EthernetConfig {
390    /// Get the EtherType, using default if not configured.
391    pub fn ethertype(&self) -> u16 {
392        self.ethertype.unwrap_or(DEFAULT_ETHERNET_ETHERTYPE)
393    }
394
395    /// Get the receive buffer size, using default if not configured.
396    pub fn recv_buf_size(&self) -> usize {
397        self.recv_buf_size.unwrap_or(DEFAULT_ETHERNET_RECV_BUF)
398    }
399
400    /// Get the send buffer size, using default if not configured.
401    pub fn send_buf_size(&self) -> usize {
402        self.send_buf_size.unwrap_or(DEFAULT_ETHERNET_SEND_BUF)
403    }
404
405    /// Whether to listen for discovery beacons. Default: true.
406    pub fn discovery(&self) -> bool {
407        self.discovery.unwrap_or(true)
408    }
409
410    /// Whether to broadcast announcement beacons. Default: false.
411    pub fn announce(&self) -> bool {
412        self.announce.unwrap_or(false)
413    }
414
415    /// Whether to auto-connect to discovered peers. Default: false.
416    pub fn auto_connect(&self) -> bool {
417        self.auto_connect.unwrap_or(false)
418    }
419
420    /// Whether to accept incoming connections. Default: false.
421    pub fn accept_connections(&self) -> bool {
422        self.accept_connections.unwrap_or(false)
423    }
424
425    /// Optional discovery scope for Ethernet beacons.
426    pub fn discovery_scope(&self) -> Option<&str> {
427        self.discovery_scope.as_deref().filter(|s| !s.is_empty())
428    }
429
430    /// Get the beacon interval, clamped to minimum. Default: 30s.
431    pub fn beacon_interval_secs(&self) -> u64 {
432        self.beacon_interval_secs
433            .unwrap_or(DEFAULT_BEACON_INTERVAL_SECS)
434            .max(MIN_BEACON_INTERVAL_SECS)
435    }
436}
437
438// ============================================================================
439// TCP Transport Configuration
440// ============================================================================
441
442/// Default TCP dataplane/path budget.
443const DEFAULT_TCP_MTU: u16 = 1400;
444
445/// Default TCP connect timeout in milliseconds.
446const DEFAULT_TCP_CONNECT_TIMEOUT_MS: u64 = 5000;
447
448/// Default timeout for an accepted inbound TCP connection to deliver its
449/// first complete FMP frame.
450const DEFAULT_TCP_FIRST_FRAME_TIMEOUT_MS: u64 = 3000;
451
452/// Default TCP keepalive interval in seconds.
453const DEFAULT_TCP_KEEPALIVE_SECS: u64 = 30;
454
455/// Default TCP receive buffer size (2 MB).
456const DEFAULT_TCP_RECV_BUF: usize = 2 * 1024 * 1024;
457
458/// Default TCP send buffer size (2 MB).
459const DEFAULT_TCP_SEND_BUF: usize = 2 * 1024 * 1024;
460
461/// Default maximum inbound TCP connections.
462const DEFAULT_TCP_MAX_INBOUND: usize = 256;
463
464/// Default WebSocket path accepted by the native plain-WS listener.
465const DEFAULT_WEBSOCKET_PATH: &str = "/fips";
466
467/// Default WebSocket FIPS path MTU.
468const DEFAULT_WEBSOCKET_MTU: u16 = 1400;
469
470/// Largest legal FIPS record plus conservative header room.
471const DEFAULT_WEBSOCKET_MAX_FRAME_BYTES: usize = 66 * 1024;
472
473const DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS: u64 = 5_000;
474const DEFAULT_WEBSOCKET_KEY_HINT_TIMEOUT_MS: u64 = 3_000;
475const DEFAULT_WEBSOCKET_RECONNECT_INITIAL_MS: u64 = 1_000;
476const DEFAULT_WEBSOCKET_RECONNECT_MAX_MS: u64 = 30_000;
477const DEFAULT_WEBSOCKET_MAX_CONNECTIONS: usize = 256;
478const DEFAULT_WEBSOCKET_MAX_INBOUND: usize = 128;
479const DEFAULT_WEBSOCKET_MAX_SEND_QUEUE: usize = 256;
480const DEFAULT_WEBSOCKET_PING_INTERVAL_SECS: u64 = 20;
481const DEFAULT_WEBSOCKET_IDLE_TIMEOUT_SECS: u64 = 90;
482
483/// TCP transport instance configuration.
484#[derive(Debug, Clone, Default, Serialize, Deserialize)]
485#[serde(deny_unknown_fields)]
486pub struct TcpConfig {
487    /// Listen address (e.g., "0.0.0.0:443"). If not set, outbound-only.
488    #[serde(default, skip_serializing_if = "Option::is_none")]
489    pub bind_addr: Option<String>,
490
491    /// Dataplane/path budget advertised for TCP routes. Defaults to 1400.
492    /// TCP byte-stream framing is independent of TCP_MAXSEG and is bounded by
493    /// the FMP/FSP wire record's u16 payload length.
494    #[serde(default, skip_serializing_if = "Option::is_none")]
495    pub mtu: Option<u16>,
496
497    /// Outbound connect timeout in milliseconds. Defaults to 5000.
498    #[serde(default, skip_serializing_if = "Option::is_none")]
499    pub connect_timeout_ms: Option<u64>,
500
501    /// Inbound first-frame timeout in milliseconds. Accepted connections
502    /// must deliver one complete FMP frame within this window or they are
503    /// closed. Set to 0 to disable. Defaults to 3000.
504    #[serde(default, skip_serializing_if = "Option::is_none")]
505    pub first_frame_timeout_ms: Option<u64>,
506
507    /// Enable TCP_NODELAY (disable Nagle). Defaults to true.
508    #[serde(default, skip_serializing_if = "Option::is_none")]
509    pub nodelay: Option<bool>,
510
511    /// TCP keepalive interval in seconds. 0 = disabled. Defaults to 30.
512    #[serde(default, skip_serializing_if = "Option::is_none")]
513    pub keepalive_secs: Option<u64>,
514
515    /// TCP receive buffer size in bytes. Defaults to 2 MB.
516    #[serde(default, skip_serializing_if = "Option::is_none")]
517    pub recv_buf_size: Option<usize>,
518
519    /// TCP send buffer size in bytes. Defaults to 2 MB.
520    #[serde(default, skip_serializing_if = "Option::is_none")]
521    pub send_buf_size: Option<usize>,
522
523    /// Maximum simultaneous inbound connections. Defaults to 256.
524    #[serde(default, skip_serializing_if = "Option::is_none")]
525    pub max_inbound_connections: Option<usize>,
526
527    /// Whether this transport should be advertised on Nostr overlay discovery.
528    /// Default: false.
529    #[serde(default, skip_serializing_if = "Option::is_none")]
530    pub advertise_on_nostr: Option<bool>,
531
532    /// Optional explicit public address to advertise. Required when
533    /// `bind_addr` is wildcard (e.g. `"0.0.0.0:443"`) and
534    /// `advertise_on_nostr: true`, since TCP has no STUN equivalent
535    /// for autodiscovery. Accepts either a bare IP (`"198.51.100.1"`
536    /// — the configured `bind_addr` port is appended) or a full
537    /// `host:port`. Common pattern on AWS EIP / cloud 1:1 NAT setups
538    /// where the public IP isn't bindable on the host.
539    #[serde(default, skip_serializing_if = "Option::is_none")]
540    pub external_addr: Option<String>,
541}
542
543impl TcpConfig {
544    /// Get the default MTU.
545    pub fn mtu(&self) -> u16 {
546        self.mtu.unwrap_or(DEFAULT_TCP_MTU)
547    }
548
549    /// Get the connect timeout in milliseconds.
550    pub fn connect_timeout_ms(&self) -> u64 {
551        self.connect_timeout_ms
552            .unwrap_or(DEFAULT_TCP_CONNECT_TIMEOUT_MS)
553    }
554
555    /// Get the inbound first-frame timeout in milliseconds. 0 disables it.
556    pub fn first_frame_timeout_ms(&self) -> u64 {
557        self.first_frame_timeout_ms
558            .unwrap_or(DEFAULT_TCP_FIRST_FRAME_TIMEOUT_MS)
559    }
560
561    /// Whether TCP_NODELAY is enabled. Default: true.
562    pub fn nodelay(&self) -> bool {
563        self.nodelay.unwrap_or(true)
564    }
565
566    /// Get the keepalive interval in seconds. 0 = disabled. Default: 30.
567    pub fn keepalive_secs(&self) -> u64 {
568        self.keepalive_secs.unwrap_or(DEFAULT_TCP_KEEPALIVE_SECS)
569    }
570
571    /// Get the receive buffer size. Default: 2 MB.
572    pub fn recv_buf_size(&self) -> usize {
573        self.recv_buf_size.unwrap_or(DEFAULT_TCP_RECV_BUF)
574    }
575
576    /// Get the send buffer size. Default: 2 MB.
577    pub fn send_buf_size(&self) -> usize {
578        self.send_buf_size.unwrap_or(DEFAULT_TCP_SEND_BUF)
579    }
580
581    /// Get the maximum number of inbound connections. Default: 256.
582    pub fn max_inbound_connections(&self) -> usize {
583        self.max_inbound_connections
584            .unwrap_or(DEFAULT_TCP_MAX_INBOUND)
585    }
586
587    /// Whether this TCP transport should be advertised on Nostr discovery.
588    pub fn advertise_on_nostr(&self) -> bool {
589        self.advertise_on_nostr.unwrap_or(false)
590    }
591
592    /// Parse `external_addr` against the configured `bind_addr` port,
593    /// returning the absolute `SocketAddr` to advertise on Nostr.
594    /// Returns `None` if `external_addr` is unset or malformed, or if
595    /// `bind_addr` is unset / unparseable so no port can be inferred.
596    pub fn external_advert_addr(&self) -> Option<SocketAddr> {
597        let raw = self.external_addr.as_deref()?;
598        let bind_port = parse_bind_port(self.bind_addr.as_deref()?)?;
599        parse_external_advert_addr(raw, bind_port)
600    }
601}
602
603/// WebSocket physical transport configuration.
604///
605/// The native listener intentionally speaks plain WebSocket so deployments can
606/// bind it to localhost or a private interface and terminate TLS in a reverse
607/// proxy. Clients use explicit `wss://` seed URLs; plaintext `ws://` seeds are
608/// accepted only for loopback development and tests. A bounded nonce/key-hint
609/// exchange identifies a URL-only seed before Noise IK; after that exchange,
610/// every binary WebSocket message carries exactly one bounded FIPS physical
611/// record.
612#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
613#[serde(deny_unknown_fields)]
614pub struct WebSocketConfig {
615    /// Optional native plain-WS listener address. Unset means client-only.
616    #[serde(default, skip_serializing_if = "Option::is_none")]
617    pub bind_addr: Option<String>,
618
619    /// Public `wss://` URL advertised for this listener, separate from bind.
620    #[serde(default, skip_serializing_if = "Option::is_none")]
621    pub public_url: Option<String>,
622
623    /// One or more explicit first-adjacency seed URLs.
624    #[serde(default, skip_serializing_if = "Vec::is_empty")]
625    pub seed_urls: Vec<String>,
626
627    /// HTTP path accepted by the native listener. Defaults to `/fips`.
628    #[serde(default, skip_serializing_if = "Option::is_none")]
629    pub path: Option<String>,
630
631    /// Dataplane/path budget. Defaults to 1400 bytes.
632    #[serde(default, skip_serializing_if = "Option::is_none")]
633    pub mtu: Option<u16>,
634
635    /// Maximum binary WebSocket message size.
636    #[serde(default, skip_serializing_if = "Option::is_none")]
637    pub max_frame_bytes: Option<usize>,
638
639    /// Maximum queued outbound records per connection.
640    #[serde(default, skip_serializing_if = "Option::is_none")]
641    pub max_send_queue: Option<usize>,
642
643    /// Maximum total WebSocket connections for this transport instance.
644    #[serde(default, skip_serializing_if = "Option::is_none")]
645    pub max_connections: Option<usize>,
646
647    /// Maximum simultaneous inbound WebSocket connections.
648    #[serde(default, skip_serializing_if = "Option::is_none")]
649    pub max_inbound_connections: Option<usize>,
650
651    /// Outbound TCP/TLS/WebSocket connect timeout.
652    #[serde(default, skip_serializing_if = "Option::is_none")]
653    pub connect_timeout_ms: Option<u64>,
654
655    /// Time allowed for the untrusted seed-key hint exchange.
656    #[serde(default, skip_serializing_if = "Option::is_none")]
657    pub key_hint_timeout_ms: Option<u64>,
658
659    /// Initial reconnect delay for configured seeds.
660    #[serde(default, skip_serializing_if = "Option::is_none")]
661    pub reconnect_initial_ms: Option<u64>,
662
663    /// Maximum reconnect delay for configured seeds.
664    #[serde(default, skip_serializing_if = "Option::is_none")]
665    pub reconnect_max_ms: Option<u64>,
666
667    /// WebSocket ping interval. Zero disables transport pings.
668    #[serde(default, skip_serializing_if = "Option::is_none")]
669    pub ping_interval_secs: Option<u64>,
670
671    /// Close connections with no received frame for this long. Zero disables.
672    #[serde(default, skip_serializing_if = "Option::is_none")]
673    pub idle_timeout_secs: Option<u64>,
674
675    /// Accept fresh inbound Noise IK handshakes. Defaults to true whenever the
676    /// transport has a listener or seed URL. WebSocket dial direction does not
677    /// constrain FIPS session direction on an established routed adjacency.
678    #[serde(default, skip_serializing_if = "Option::is_none")]
679    pub accept_connections: Option<bool>,
680}
681
682impl WebSocketConfig {
683    pub fn path(&self) -> &str {
684        self.path.as_deref().unwrap_or(DEFAULT_WEBSOCKET_PATH)
685    }
686
687    pub fn mtu(&self) -> u16 {
688        self.mtu.unwrap_or(DEFAULT_WEBSOCKET_MTU)
689    }
690
691    pub fn max_frame_bytes(&self) -> usize {
692        self.max_frame_bytes
693            .unwrap_or(DEFAULT_WEBSOCKET_MAX_FRAME_BYTES)
694    }
695
696    pub fn max_send_queue(&self) -> usize {
697        self.max_send_queue
698            .unwrap_or(DEFAULT_WEBSOCKET_MAX_SEND_QUEUE)
699            .max(1)
700    }
701
702    pub fn max_connections(&self) -> usize {
703        self.max_connections
704            .unwrap_or(DEFAULT_WEBSOCKET_MAX_CONNECTIONS)
705            .max(1)
706    }
707
708    pub fn max_inbound_connections(&self) -> usize {
709        self.max_inbound_connections
710            .unwrap_or(DEFAULT_WEBSOCKET_MAX_INBOUND)
711            .max(1)
712            .min(self.max_connections())
713    }
714
715    pub fn connect_timeout_ms(&self) -> u64 {
716        self.connect_timeout_ms
717            .unwrap_or(DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS)
718            .max(1)
719    }
720
721    pub fn key_hint_timeout_ms(&self) -> u64 {
722        self.key_hint_timeout_ms
723            .unwrap_or(DEFAULT_WEBSOCKET_KEY_HINT_TIMEOUT_MS)
724            .max(1)
725    }
726
727    pub fn reconnect_initial_ms(&self) -> u64 {
728        self.reconnect_initial_ms
729            .unwrap_or(DEFAULT_WEBSOCKET_RECONNECT_INITIAL_MS)
730            .max(1)
731    }
732
733    pub fn reconnect_max_ms(&self) -> u64 {
734        self.reconnect_max_ms
735            .unwrap_or(DEFAULT_WEBSOCKET_RECONNECT_MAX_MS)
736            .max(self.reconnect_initial_ms())
737    }
738
739    pub fn ping_interval_secs(&self) -> u64 {
740        self.ping_interval_secs
741            .unwrap_or(DEFAULT_WEBSOCKET_PING_INTERVAL_SECS)
742    }
743
744    pub fn idle_timeout_secs(&self) -> u64 {
745        self.idle_timeout_secs
746            .unwrap_or(DEFAULT_WEBSOCKET_IDLE_TIMEOUT_SECS)
747    }
748
749    pub fn accept_connections(&self) -> bool {
750        self.accept_connections
751            .unwrap_or_else(|| self.bind_addr.is_some() || !self.seed_urls.is_empty())
752    }
753
754    pub fn validate(&self) -> Result<(), String> {
755        if let Some(bind_addr) = self.bind_addr.as_deref() {
756            bind_addr
757                .parse::<SocketAddr>()
758                .map_err(|error| format!("invalid bind_addr {bind_addr:?}: {error}"))?;
759        }
760        if !self.path().starts_with('/') || self.path().contains('?') || self.path().contains('#') {
761            return Err("path must be an absolute HTTP path without query or fragment".into());
762        }
763        if let Some(public_url) = self.public_url.as_deref() {
764            validate_websocket_url(public_url, false)?;
765            let uri = public_url
766                .parse::<tokio_tungstenite::tungstenite::http::Uri>()
767                .map_err(|error| format!("invalid public_url: {error}"))?;
768            if uri.path() != self.path() {
769                return Err(format!(
770                    "public_url path {:?} does not match configured path {:?}",
771                    uri.path(),
772                    self.path()
773                ));
774            }
775            if self.bind_addr.is_none() {
776                return Err("public_url requires bind_addr".into());
777            }
778        }
779        let mut unique = std::collections::HashSet::new();
780        for seed_url in &self.seed_urls {
781            validate_websocket_url(seed_url, true)?;
782            if !unique.insert(seed_url) {
783                return Err(format!("duplicate seed URL {seed_url:?}"));
784            }
785        }
786        let minimum_frame = usize::from(self.mtu()).saturating_add(64);
787        if self.max_frame_bytes() < minimum_frame || self.max_frame_bytes() > 1024 * 1024 {
788            return Err(format!(
789                "max_frame_bytes must be between {minimum_frame} and 1048576"
790            ));
791        }
792        if self.max_send_queue() > 4096 {
793            return Err("max_send_queue must not exceed 4096".into());
794        }
795        if self.max_connections() > 4096 {
796            return Err("max_connections must not exceed 4096".into());
797        }
798        if self.max_inbound_connections() > self.max_connections() {
799            return Err("max_inbound_connections must not exceed max_connections".into());
800        }
801        if self.ping_interval_secs() > 0
802            && self.idle_timeout_secs() > 0
803            && self.idle_timeout_secs() <= self.ping_interval_secs()
804        {
805            return Err("idle_timeout_secs must exceed ping_interval_secs".into());
806        }
807        Ok(())
808    }
809}
810
811fn validate_websocket_url(raw: &str, allow_loopback_plaintext: bool) -> Result<(), String> {
812    let uri = raw
813        .parse::<tokio_tungstenite::tungstenite::http::Uri>()
814        .map_err(|error| format!("invalid WebSocket URL {raw:?}: {error}"))?;
815    let scheme = uri
816        .scheme_str()
817        .ok_or_else(|| format!("WebSocket URL {raw:?} is missing a scheme"))?;
818    let host = uri
819        .host()
820        .ok_or_else(|| format!("WebSocket URL {raw:?} is missing a host"))?;
821    if uri.authority().is_none() || uri.path().is_empty() {
822        return Err(format!("invalid WebSocket URL {raw:?}"));
823    }
824    match scheme {
825        "wss" => Ok(()),
826        "ws" if allow_loopback_plaintext && websocket_host_is_loopback(host) => Ok(()),
827        "ws" => Err(format!(
828            "plaintext WebSocket URL {raw:?} is allowed only for loopback seeds"
829        )),
830        _ => Err(format!("WebSocket URL {raw:?} must use wss://")),
831    }
832}
833
834fn websocket_host_is_loopback(host: &str) -> bool {
835    host.eq_ignore_ascii_case("localhost")
836        || host
837            .trim_matches(['[', ']'])
838            .parse::<IpAddr>()
839            .is_ok_and(|ip| ip.is_loopback())
840}
841
842// ============================================================================
843// Tor Transport Configuration
844// ============================================================================
845
846/// Default Tor SOCKS5 proxy address.
847const DEFAULT_TOR_SOCKS5_ADDR: &str = "127.0.0.1:9050";
848
849/// Default Tor control port address.
850const DEFAULT_TOR_CONTROL_ADDR: &str = "/run/tor/control";
851
852/// Default Tor control cookie file path (Debian standard location).
853const DEFAULT_TOR_COOKIE_PATH: &str = "/var/run/tor/control.authcookie";
854
855/// Default Tor connect timeout in milliseconds (120s — Tor circuit
856/// establishment can take 30-60s on first connect, plus SOCKS5 handshake).
857const DEFAULT_TOR_CONNECT_TIMEOUT_MS: u64 = 120_000;
858
859/// Default Tor dataplane/path budget (same as TCP).
860const DEFAULT_TOR_MTU: u16 = 1400;
861
862/// Default max inbound connections via onion service.
863const DEFAULT_TOR_MAX_INBOUND: usize = 64;
864
865/// Default HiddenServiceDir hostname file path.
866const DEFAULT_HOSTNAME_FILE: &str = "/var/lib/tor/fips_onion_service/hostname";
867
868/// Default directory mode bind address.
869const DEFAULT_DIRECTORY_BIND_ADDR: &str = "127.0.0.1:8443";
870
871/// Default advertised onion port for Nostr overlay discovery. Matches the
872/// Tor convention of `HiddenServicePort 443 127.0.0.1:<bind_port>` in torrc.
873const DEFAULT_TOR_ADVERTISED_PORT: u16 = 443;
874
875/// Tor transport instance configuration.
876///
877/// Supports three modes:
878/// - `socks5`: Outbound-only connections through a Tor SOCKS5 proxy.
879/// - `control_port`: Full bidirectional support — outbound via SOCKS5
880///   plus inbound via Tor onion service managed through the control port.
881/// - `directory`: Full bidirectional support — outbound via SOCKS5,
882///   inbound via a Tor-managed `HiddenServiceDir` onion service. No
883///   control port needed. Enables Tor `Sandbox 1` mode.
884#[derive(Debug, Clone, Default, Serialize, Deserialize)]
885#[serde(deny_unknown_fields)]
886pub struct TorConfig {
887    /// Tor access mode: "socks5", "control_port", or "directory".
888    /// Default: "socks5".
889    #[serde(default, skip_serializing_if = "Option::is_none")]
890    pub mode: Option<String>,
891
892    /// SOCKS5 proxy address (host:port). Defaults to "127.0.0.1:9050".
893    #[serde(default, skip_serializing_if = "Option::is_none")]
894    pub socks5_addr: Option<String>,
895
896    /// Outbound connect timeout in milliseconds. Defaults to 120000 (120s).
897    /// Tor circuit establishment can take 30-60s, so this must be generous.
898    #[serde(default, skip_serializing_if = "Option::is_none")]
899    pub connect_timeout_ms: Option<u64>,
900
901    /// Dataplane/path budget advertised for Tor routes. Defaults to 1400.
902    /// Tor byte-stream framing is bounded by the FMP/FSP wire record's u16
903    /// payload length, independently of this budget.
904    #[serde(default, skip_serializing_if = "Option::is_none")]
905    pub mtu: Option<u16>,
906
907    /// Control port address: a Unix socket path (`/run/tor/control`) or
908    /// TCP address (`host:port`). Unix sockets are preferred for security.
909    /// Defaults to "/run/tor/control".
910    #[serde(default, skip_serializing_if = "Option::is_none")]
911    pub control_addr: Option<String>,
912
913    /// Control port authentication method:
914    /// `"cookie"` (read from default path),
915    /// `"cookie:/path/to/cookie"` (read from specified path), or
916    /// `"password:secret"` (password auth). Default: `"cookie"`.
917    #[serde(default, skip_serializing_if = "Option::is_none")]
918    pub control_auth: Option<String>,
919
920    /// Path to the Tor control cookie file. Used when control_auth is "cookie".
921    /// Defaults to "/var/run/tor/control.authcookie".
922    #[serde(default, skip_serializing_if = "Option::is_none")]
923    pub cookie_path: Option<String>,
924
925    /// Maximum number of inbound connections via onion service. Default: 64.
926    #[serde(default, skip_serializing_if = "Option::is_none")]
927    pub max_inbound_connections: Option<usize>,
928
929    /// Directory-mode onion service configuration. Only valid in
930    /// "directory" mode. Tor manages the onion service via HiddenServiceDir
931    /// in torrc; fips reads the .onion hostname from a file.
932    #[serde(default, skip_serializing_if = "Option::is_none")]
933    pub directory_service: Option<DirectoryServiceConfig>,
934
935    /// Whether this transport should be advertised on Nostr overlay discovery.
936    /// Default: false.
937    #[serde(default, skip_serializing_if = "Option::is_none")]
938    pub advertise_on_nostr: Option<bool>,
939
940    /// Public-facing onion port published in Nostr overlay adverts. Must
941    /// match the virtual port in torrc's `HiddenServicePort <port>
942    /// 127.0.0.1:<bind_port>` directive — that is the port other peers
943    /// will use to reach this onion. Default: 443.
944    #[serde(default, skip_serializing_if = "Option::is_none")]
945    pub advertised_port: Option<u16>,
946}
947
948/// Directory-mode onion service configuration.
949///
950/// In `directory` mode, Tor manages the onion service via `HiddenServiceDir`
951/// in torrc. FIPS reads the `.onion` address from the hostname file and
952/// binds a local TCP listener for Tor to forward inbound connections to.
953/// This mode requires no control port and enables Tor's `Sandbox 1`.
954#[derive(Debug, Clone, Default, Serialize, Deserialize)]
955#[serde(deny_unknown_fields)]
956pub struct DirectoryServiceConfig {
957    /// Path to the Tor-managed hostname file containing the .onion address.
958    /// Defaults to "/var/lib/tor/fips_onion_service/hostname".
959    #[serde(default, skip_serializing_if = "Option::is_none")]
960    pub hostname_file: Option<String>,
961
962    /// Local bind address for the listener that Tor forwards inbound
963    /// connections to. Must match the target in torrc's `HiddenServicePort`.
964    /// Defaults to "127.0.0.1:8443".
965    #[serde(default, skip_serializing_if = "Option::is_none")]
966    pub bind_addr: Option<String>,
967}
968
969impl DirectoryServiceConfig {
970    /// Path to the hostname file. Default: "/var/lib/tor/fips_onion_service/hostname".
971    pub fn hostname_file(&self) -> &str {
972        self.hostname_file
973            .as_deref()
974            .unwrap_or(DEFAULT_HOSTNAME_FILE)
975    }
976
977    /// Local bind address for the listener. Default: "127.0.0.1:8443".
978    pub fn bind_addr(&self) -> &str {
979        self.bind_addr
980            .as_deref()
981            .unwrap_or(DEFAULT_DIRECTORY_BIND_ADDR)
982    }
983}
984
985impl TorConfig {
986    /// Get the access mode. Default: "socks5".
987    pub fn mode(&self) -> &str {
988        self.mode.as_deref().unwrap_or("socks5")
989    }
990
991    /// Get the SOCKS5 proxy address. Default: "127.0.0.1:9050".
992    pub fn socks5_addr(&self) -> &str {
993        self.socks5_addr
994            .as_deref()
995            .unwrap_or(DEFAULT_TOR_SOCKS5_ADDR)
996    }
997
998    /// Get the control port address. Default: "/run/tor/control".
999    pub fn control_addr(&self) -> &str {
1000        self.control_addr
1001            .as_deref()
1002            .unwrap_or(DEFAULT_TOR_CONTROL_ADDR)
1003    }
1004
1005    /// Get the control auth string. Default: "cookie".
1006    pub fn control_auth(&self) -> &str {
1007        self.control_auth.as_deref().unwrap_or("cookie")
1008    }
1009
1010    /// Get the cookie file path. Default: "/var/run/tor/control.authcookie".
1011    pub fn cookie_path(&self) -> &str {
1012        self.cookie_path
1013            .as_deref()
1014            .unwrap_or(DEFAULT_TOR_COOKIE_PATH)
1015    }
1016
1017    /// Get the connect timeout in milliseconds. Default: 120000.
1018    pub fn connect_timeout_ms(&self) -> u64 {
1019        self.connect_timeout_ms
1020            .unwrap_or(DEFAULT_TOR_CONNECT_TIMEOUT_MS)
1021    }
1022
1023    /// Get the default MTU. Default: 1400.
1024    pub fn mtu(&self) -> u16 {
1025        self.mtu.unwrap_or(DEFAULT_TOR_MTU)
1026    }
1027
1028    /// Get the max inbound connections. Default: 64.
1029    pub fn max_inbound_connections(&self) -> usize {
1030        self.max_inbound_connections
1031            .unwrap_or(DEFAULT_TOR_MAX_INBOUND)
1032    }
1033
1034    /// Whether this Tor transport should be advertised on Nostr discovery.
1035    pub fn advertise_on_nostr(&self) -> bool {
1036        self.advertise_on_nostr.unwrap_or(false)
1037    }
1038
1039    /// Public-facing onion port published in Nostr overlay adverts.
1040    /// Default: 443.
1041    pub fn advertised_port(&self) -> u16 {
1042        self.advertised_port.unwrap_or(DEFAULT_TOR_ADVERTISED_PORT)
1043    }
1044}
1045
1046// ============================================================================
1047// WebRTC Transport Configuration
1048// ============================================================================
1049
1050/// Default WebRTC data-channel MTU.
1051const DEFAULT_WEBRTC_MTU: u16 = 1200;
1052
1053/// Default WebRTC connection timeout in milliseconds.
1054const DEFAULT_WEBRTC_CONNECT_TIMEOUT_MS: u64 = 30_000;
1055
1056/// Default non-trickle ICE gathering timeout in milliseconds.
1057const DEFAULT_WEBRTC_ICE_GATHER_TIMEOUT_MS: u64 = 2_000;
1058
1059/// Default maximum simultaneous WebRTC peer connections.
1060const DEFAULT_WEBRTC_MAX_CONNECTIONS: usize = 6;
1061
1062/// Default WebRTC data channel label.
1063const DEFAULT_WEBRTC_DATA_CHANNEL_LABEL: &str = "fips";
1064
1065/// WebRTC transport instance configuration.
1066///
1067/// WebRTC negotiates over an existing authenticated FIPS session and carries
1068/// ordinary FIPS datagrams over an SCTP data channel.
1069#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1070#[serde(deny_unknown_fields)]
1071pub struct WebRtcConfig {
1072    /// Whether this transport should be advertised on Nostr overlay discovery.
1073    /// Default: false.
1074    #[serde(default, skip_serializing_if = "Option::is_none")]
1075    pub advertise_on_nostr: Option<bool>,
1076
1077    /// Whether to automatically connect to discovered WebRTC peers.
1078    /// Default: false.
1079    #[serde(default, skip_serializing_if = "Option::is_none")]
1080    pub auto_connect: Option<bool>,
1081
1082    /// Accept inbound WebRTC offers. Defaults to `advertise_on_nostr`: a
1083    /// non-advertising adapter has no inbound listener policy unless enabled.
1084    #[serde(default, skip_serializing_if = "Option::is_none")]
1085    pub accept_connections: Option<bool>,
1086
1087    /// Data-channel MTU. Defaults to 1200.
1088    #[serde(default, skip_serializing_if = "Option::is_none")]
1089    pub mtu: Option<u16>,
1090
1091    /// Maximum simultaneous WebRTC peer connections. Defaults to 6 and is
1092    /// additionally bounded by the configured ICE socket budget.
1093    #[serde(default, skip_serializing_if = "Option::is_none")]
1094    pub max_connections: Option<usize>,
1095
1096    /// Outbound connect timeout in milliseconds. Defaults to 30000.
1097    #[serde(default, skip_serializing_if = "Option::is_none")]
1098    pub connect_timeout_ms: Option<u64>,
1099
1100    /// Non-trickle ICE gathering timeout in milliseconds. Defaults to 2000.
1101    #[serde(default, skip_serializing_if = "Option::is_none")]
1102    pub ice_gather_timeout_ms: Option<u64>,
1103
1104    /// Data channel label. Defaults to "fips".
1105    #[serde(default, skip_serializing_if = "Option::is_none")]
1106    pub data_channel_label: Option<String>,
1107
1108    /// Ordered data channel delivery. Default: true.
1109    #[serde(default, skip_serializing_if = "Option::is_none")]
1110    pub ordered: Option<bool>,
1111
1112    /// Maximum retransmits for partial reliability. Default: unset, which uses
1113    /// WebRTC's reliable data-channel mode. Set to 0 for datagram-like delivery.
1114    #[serde(default, skip_serializing_if = "Option::is_none")]
1115    pub max_retransmits: Option<u16>,
1116
1117    /// Override STUN servers for this transport. Supports up to three `stun:`
1118    /// URLs. When unset, `node.discovery.nostr.stun_servers` is used.
1119    #[serde(default, skip_serializing_if = "Option::is_none")]
1120    pub stun_servers: Option<Vec<String>>,
1121
1122    /// Resolve browser `.local` ICE candidates through one shared mDNS owner.
1123    /// Every peer connection keeps its own ICE mDNS mode disabled. Default:
1124    /// true. Disable this for environments where multicast DNS is unavailable.
1125    #[serde(default, skip_serializing_if = "Option::is_none")]
1126    pub resolve_mdns_candidates: Option<bool>,
1127}
1128
1129impl WebRtcConfig {
1130    /// Whether this WebRTC transport should be advertised on Nostr discovery.
1131    pub fn advertise_on_nostr(&self) -> bool {
1132        self.advertise_on_nostr.unwrap_or(false)
1133    }
1134
1135    /// Whether this transport auto-connects to discovered peers.
1136    pub fn auto_connect(&self) -> bool {
1137        self.auto_connect.unwrap_or(false)
1138    }
1139
1140    /// Whether this transport accepts inbound offers.
1141    pub fn accept_connections(&self) -> bool {
1142        self.accept_connections
1143            .unwrap_or_else(|| self.advertise_on_nostr())
1144    }
1145
1146    /// Get the data-channel MTU.
1147    pub fn mtu(&self) -> u16 {
1148        self.mtu.unwrap_or(DEFAULT_WEBRTC_MTU)
1149    }
1150
1151    /// Get the maximum number of peer connections.
1152    pub fn max_connections(&self) -> usize {
1153        self.max_connections
1154            .unwrap_or(DEFAULT_WEBRTC_MAX_CONNECTIONS)
1155    }
1156
1157    /// Get the connect timeout in milliseconds.
1158    pub fn connect_timeout_ms(&self) -> u64 {
1159        self.connect_timeout_ms
1160            .unwrap_or(DEFAULT_WEBRTC_CONNECT_TIMEOUT_MS)
1161    }
1162
1163    /// Get the ICE gathering timeout in milliseconds.
1164    pub fn ice_gather_timeout_ms(&self) -> u64 {
1165        self.ice_gather_timeout_ms
1166            .unwrap_or(DEFAULT_WEBRTC_ICE_GATHER_TIMEOUT_MS)
1167    }
1168
1169    /// Get the data channel label.
1170    pub fn data_channel_label(&self) -> &str {
1171        self.data_channel_label
1172            .as_deref()
1173            .unwrap_or(DEFAULT_WEBRTC_DATA_CHANNEL_LABEL)
1174    }
1175
1176    /// Whether the data channel is ordered.
1177    pub fn ordered(&self) -> bool {
1178        self.ordered.unwrap_or(true)
1179    }
1180
1181    /// Get the configured max retransmits. None uses WebRTC's reliable mode.
1182    pub fn max_retransmits(&self) -> Option<u16> {
1183        self.max_retransmits
1184    }
1185
1186    /// Whether browser `.local` ICE candidates should be resolved.
1187    pub fn resolve_mdns_candidates(&self) -> bool {
1188        self.resolve_mdns_candidates.unwrap_or(true)
1189    }
1190
1191    /// Resolve STUN servers, falling back to node discovery STUN servers.
1192    pub fn stun_servers<'a>(&'a self, fallback: &'a [String]) -> Vec<String> {
1193        self.stun_servers
1194            .as_ref()
1195            .cloned()
1196            .unwrap_or_else(|| fallback.to_vec())
1197    }
1198}
1199
1200mod aggregate;
1201mod ble;
1202#[cfg(test)]
1203mod tests;
1204
1205pub use aggregate::TransportsConfig;
1206pub use ble::BleConfig;