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