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`, the configured address family is retained
66    /// while the transport binds to a kernel-assigned ephemeral port.
67    #[serde(default, skip_serializing_if = "Option::is_none")]
68    pub bind_addr: Option<String>,
69
70    /// Physical underlay interface used by this socket.
71    ///
72    /// On platforms with interface-bound sockets (currently macOS and Linux),
73    /// this prevents a VPN carrier from being routed back into a system VPN
74    /// default route. The interface is resolved when the socket starts, so a
75    /// link change should restart or rebuild the transport with the new name.
76    #[serde(default, skip_serializing_if = "Option::is_none")]
77    pub bind_interface: Option<String>,
78
79    /// UDP MTU (`mtu`). Defaults to 1280 (IPv6 minimum).
80    #[serde(default, skip_serializing_if = "Option::is_none")]
81    pub mtu: Option<u16>,
82
83    /// UDP receive buffer size in bytes (`recv_buf_size`). Defaults to 16 MiB.
84    #[serde(default, skip_serializing_if = "Option::is_none")]
85    pub recv_buf_size: Option<usize>,
86
87    /// UDP send buffer size in bytes (`send_buf_size`). Defaults to 8 MiB.
88    #[serde(default, skip_serializing_if = "Option::is_none")]
89    pub send_buf_size: Option<usize>,
90
91    /// Whether this transport should be advertised on Nostr overlay discovery.
92    /// Default: false. Implicitly forced false when `outbound_only = true`.
93    #[serde(default, skip_serializing_if = "Option::is_none")]
94    pub advertise_on_nostr: Option<bool>,
95
96    /// Whether UDP should be advertised as directly reachable (`host:port`) on
97    /// Nostr. When false and advertised, UDP is emitted as `addr: "nat"` to
98    /// trigger rendezvous traversal.
99    ///
100    /// Default: false.
101    #[serde(default, skip_serializing_if = "Option::is_none")]
102    pub public: Option<bool>,
103    /// Optional explicit public address to advertise when `public: true`
104    /// is set. Takes precedence over both the bound address and any
105    /// STUN-derived autodiscovery. Accepts either a bare IP
106    /// (`"198.51.100.1"` — the configured `bind_addr` port is appended)
107    /// or a full `host:port` (`"198.51.100.1:443"`). Useful when the
108    /// public IP isn't on a local interface (e.g. AWS EIP / cloud 1:1
109    /// NAT) and the operator wants to skip STUN autodiscovery for a
110    /// deterministic value.
111    #[serde(default, skip_serializing_if = "Option::is_none")]
112    pub external_addr: Option<String>,
113    /// Outbound-only mode. When true, the transport binds to a kernel-
114    /// assigned ephemeral port instead of the configured `bind_addr`, refuses
115    /// inbound handshake msg1, and is never
116    /// advertised on Nostr regardless of `advertise_on_nostr`. Use this
117    /// to participate in the mesh as a pure client — initiate outbound
118    /// links without exposing an inbound listener on a known port.
119    /// Default: false.
120    #[serde(default, skip_serializing_if = "Option::is_none")]
121    pub outbound_only: Option<bool>,
122
123    /// Accept inbound handshake msg1 from new peers. Default: true.
124    /// Setting this to false combined with `auto_connect: true` on
125    /// peer-side configurations gives a "client" posture: this node
126    /// initiates outbound links but refuses inbound handshakes from
127    /// unfamiliar addresses. The Node-level gate at
128    /// `src/node/handlers/handshake.rs` carves out msg1 from peers
129    /// already established on this transport (so rekey continues to
130    /// work) — see ISSUE-2026-0004.
131    #[serde(default, skip_serializing_if = "Option::is_none")]
132    pub accept_connections: Option<bool>,
133}
134
135impl UdpConfig {
136    /// Get the bind address, using default if not configured.
137    ///
138    /// When `outbound_only = true`, returns an IPv4 or IPv6 wildcard with port
139    /// zero so the kernel picks an ephemeral source port without changing the
140    /// configured socket family.
141    pub fn bind_addr(&self) -> &str {
142        if self.outbound_only() {
143            if self
144                .bind_addr
145                .as_deref()
146                .and_then(|addr| addr.parse::<SocketAddr>().ok())
147                .is_some_and(|addr| addr.is_ipv6())
148            {
149                "[::]:0"
150            } else {
151                "0.0.0.0:0"
152            }
153        } else {
154            self.bind_addr.as_deref().unwrap_or(DEFAULT_UDP_BIND_ADDR)
155        }
156    }
157
158    /// Get the UDP MTU, using default if not configured.
159    pub fn mtu(&self) -> u16 {
160        self.mtu.unwrap_or(DEFAULT_UDP_MTU)
161    }
162
163    /// Get the receive buffer size, using default if not configured.
164    pub fn recv_buf_size(&self) -> usize {
165        self.recv_buf_size.unwrap_or(DEFAULT_UDP_RECV_BUF)
166    }
167
168    /// Get the send buffer size, using default if not configured.
169    pub fn send_buf_size(&self) -> usize {
170        self.send_buf_size.unwrap_or(DEFAULT_UDP_SEND_BUF)
171    }
172
173    /// Whether this UDP transport should be advertised on Nostr discovery.
174    /// Always false when `outbound_only = true`.
175    pub fn advertise_on_nostr(&self) -> bool {
176        if self.outbound_only() {
177            false
178        } else {
179            self.advertise_on_nostr.unwrap_or(false)
180        }
181    }
182
183    /// Whether this UDP transport should be advertised as directly reachable.
184    pub fn is_public(&self) -> bool {
185        self.public.unwrap_or(false)
186    }
187
188    /// Parse `external_addr` against the configured `bind_addr` port,
189    /// returning the absolute `SocketAddr` to advertise on Nostr.
190    /// Returns `None` if `external_addr` is unset or malformed, or if
191    /// the port cannot be inferred.
192    pub fn external_advert_addr(&self) -> Option<SocketAddr> {
193        let raw = self.external_addr.as_deref()?;
194        let bind_port = parse_bind_port(self.bind_addr())?;
195        parse_external_advert_addr(raw, bind_port)
196    }
197
198    /// Whether this transport runs in outbound-only mode. Default: false.
199    pub fn outbound_only(&self) -> bool {
200        self.outbound_only.unwrap_or(false)
201    }
202
203    /// Whether this transport accepts inbound handshakes. Default: true.
204    pub fn accept_connections(&self) -> bool {
205        self.accept_connections.unwrap_or(true)
206    }
207}
208
209/// Default simulated transport MTU (IPv6 minimum).
210#[cfg(feature = "sim-transport")]
211const DEFAULT_SIM_MTU: u16 = 1280;
212
213/// Default simulated network registry name.
214#[cfg(feature = "sim-transport")]
215const DEFAULT_SIM_NETWORK: &str = "default";
216
217/// In-memory simulated transport instance configuration.
218///
219/// This transport is intended for production-backed simulations. It uses the
220/// normal node/session/routing stack, but delivers transport packets through a
221/// registered in-process network that can model latency, throughput, loss, and
222/// churn without binding real sockets.
223#[cfg(feature = "sim-transport")]
224#[derive(Debug, Clone, Default, Serialize, Deserialize)]
225#[serde(deny_unknown_fields)]
226pub struct SimTransportConfig {
227    /// Registry name of the in-process simulated network.
228    #[serde(default, skip_serializing_if = "Option::is_none")]
229    pub network: Option<String>,
230
231    /// Address of this simulated endpoint within the network.
232    #[serde(default, skip_serializing_if = "Option::is_none")]
233    pub addr: Option<String>,
234
235    /// Transport MTU. Defaults to 1280.
236    #[serde(default, skip_serializing_if = "Option::is_none")]
237    pub mtu: Option<u16>,
238
239    /// Whether discovery should auto-connect to discovered peers.
240    #[serde(default, skip_serializing_if = "Option::is_none")]
241    pub auto_connect: Option<bool>,
242
243    /// Accept inbound handshake msg1 from new peers. Default: true.
244    #[serde(default, skip_serializing_if = "Option::is_none")]
245    pub accept_connections: Option<bool>,
246}
247
248#[cfg(feature = "sim-transport")]
249impl SimTransportConfig {
250    /// Registry name of the in-process simulated network.
251    pub fn network(&self) -> &str {
252        self.network.as_deref().unwrap_or(DEFAULT_SIM_NETWORK)
253    }
254
255    /// Get the simulated MTU.
256    pub fn mtu(&self) -> u16 {
257        self.mtu.unwrap_or(DEFAULT_SIM_MTU)
258    }
259
260    /// Whether this transport auto-connects to discovered peers.
261    pub fn auto_connect(&self) -> bool {
262        self.auto_connect.unwrap_or(false)
263    }
264
265    /// Whether this transport accepts inbound handshakes.
266    pub fn accept_connections(&self) -> bool {
267        self.accept_connections.unwrap_or(true)
268    }
269}
270
271/// Transport instances - either a single config or named instances.
272///
273/// Allows both simple single-instance config:
274/// ```yaml
275/// transports:
276///   udp:
277///     bind_addr: "0.0.0.0:2121"
278/// ```
279///
280/// And multiple named instances:
281/// ```yaml
282/// transports:
283///   udp:
284///     main:
285///       bind_addr: "0.0.0.0:2121"
286///     backup:
287///       bind_addr: "192.168.1.100:2122"
288/// ```
289#[derive(Debug, Clone, Serialize, Deserialize)]
290#[serde(untagged)]
291pub enum TransportInstances<T> {
292    /// Single unnamed instance (config fields directly under transport type).
293    Single(T),
294    /// Multiple named instances.
295    Named(HashMap<String, T>),
296}
297
298impl<T> TransportInstances<T> {
299    /// Get the number of instances.
300    pub fn len(&self) -> usize {
301        match self {
302            TransportInstances::Single(_) => 1,
303            TransportInstances::Named(map) => map.len(),
304        }
305    }
306
307    /// Check if there are no instances.
308    pub fn is_empty(&self) -> bool {
309        match self {
310            TransportInstances::Single(_) => false,
311            TransportInstances::Named(map) => map.is_empty(),
312        }
313    }
314
315    /// Iterate over all instances as (name, config) pairs.
316    ///
317    /// Single instances have `None` as the name.
318    /// Named instances have `Some(name)`.
319    pub fn iter(&self) -> impl Iterator<Item = (Option<&str>, &T)> {
320        match self {
321            TransportInstances::Single(config) => vec![(None, config)].into_iter(),
322            TransportInstances::Named(map) => map
323                .iter()
324                .map(|(k, v)| (Some(k.as_str()), v))
325                .collect::<Vec<_>>()
326                .into_iter(),
327        }
328    }
329}
330
331impl<T> Default for TransportInstances<T> {
332    fn default() -> Self {
333        TransportInstances::Named(HashMap::new())
334    }
335}
336
337/// Default Ethernet EtherType (FIPS default).
338const DEFAULT_ETHERNET_ETHERTYPE: u16 = 0x2121;
339
340/// Default Ethernet receive buffer size (2 MB).
341const DEFAULT_ETHERNET_RECV_BUF: usize = 2 * 1024 * 1024;
342
343/// Default Ethernet send buffer size (2 MB).
344const DEFAULT_ETHERNET_SEND_BUF: usize = 2 * 1024 * 1024;
345
346/// Default beacon announcement interval in seconds.
347const DEFAULT_BEACON_INTERVAL_SECS: u64 = 30;
348
349/// Minimum beacon announcement interval in seconds.
350const MIN_BEACON_INTERVAL_SECS: u64 = 10;
351
352/// Ethernet transport instance configuration.
353///
354/// EthernetConfig is always compiled (for config parsing on any platform),
355/// but the transport runtime currently requires Linux or macOS raw sockets.
356#[derive(Debug, Clone, Default, Serialize, Deserialize)]
357#[serde(deny_unknown_fields)]
358pub struct EthernetConfig {
359    /// Network interface name (e.g., "eth0", "enp3s0"). Required.
360    pub interface: String,
361
362    /// Custom EtherType (default: 0x2121).
363    #[serde(default, skip_serializing_if = "Option::is_none")]
364    pub ethertype: Option<u16>,
365
366    /// MTU override. Defaults to the interface's MTU minus 1 (for frame type prefix).
367    /// Cannot exceed the interface's actual MTU.
368    #[serde(default, skip_serializing_if = "Option::is_none")]
369    pub mtu: Option<u16>,
370
371    /// Receive buffer size in bytes. Default: 2 MB.
372    #[serde(default, skip_serializing_if = "Option::is_none")]
373    pub recv_buf_size: Option<usize>,
374
375    /// Send buffer size in bytes. Default: 2 MB.
376    #[serde(default, skip_serializing_if = "Option::is_none")]
377    pub send_buf_size: Option<usize>,
378
379    /// Listen for discovery beacons from other nodes. Default: true.
380    #[serde(default, skip_serializing_if = "Option::is_none")]
381    pub discovery: Option<bool>,
382
383    /// Broadcast announcement beacons on the LAN. Default: false.
384    #[serde(default, skip_serializing_if = "Option::is_none")]
385    pub announce: Option<bool>,
386
387    /// Auto-connect to discovered peers. Default: false.
388    #[serde(default, skip_serializing_if = "Option::is_none")]
389    pub auto_connect: Option<bool>,
390
391    /// Accept incoming connection attempts. Default: false.
392    #[serde(default, skip_serializing_if = "Option::is_none")]
393    pub accept_connections: Option<bool>,
394
395    /// Optional discovery scope carried in Ethernet beacons.
396    ///
397    /// When set, this transport ignores Ethernet beacons from other scopes.
398    /// This is a discovery/noise filter, not an access-control mechanism. If
399    /// unset, the node-level LAN discovery scope is used when available.
400    #[serde(default, skip_serializing_if = "Option::is_none")]
401    pub discovery_scope: Option<String>,
402
403    /// Announcement beacon interval in seconds. Default: 30.
404    #[serde(default, skip_serializing_if = "Option::is_none")]
405    pub beacon_interval_secs: Option<u64>,
406}
407
408impl EthernetConfig {
409    /// Get the EtherType, using default if not configured.
410    pub fn ethertype(&self) -> u16 {
411        self.ethertype.unwrap_or(DEFAULT_ETHERNET_ETHERTYPE)
412    }
413
414    /// Get the receive buffer size, using default if not configured.
415    pub fn recv_buf_size(&self) -> usize {
416        self.recv_buf_size.unwrap_or(DEFAULT_ETHERNET_RECV_BUF)
417    }
418
419    /// Get the send buffer size, using default if not configured.
420    pub fn send_buf_size(&self) -> usize {
421        self.send_buf_size.unwrap_or(DEFAULT_ETHERNET_SEND_BUF)
422    }
423
424    /// Whether to listen for discovery beacons. Default: true.
425    pub fn discovery(&self) -> bool {
426        self.discovery.unwrap_or(true)
427    }
428
429    /// Whether to broadcast announcement beacons. Default: false.
430    pub fn announce(&self) -> bool {
431        self.announce.unwrap_or(false)
432    }
433
434    /// Whether to auto-connect to discovered peers. Default: false.
435    pub fn auto_connect(&self) -> bool {
436        self.auto_connect.unwrap_or(false)
437    }
438
439    /// Whether to accept incoming connections. Default: false.
440    pub fn accept_connections(&self) -> bool {
441        self.accept_connections.unwrap_or(false)
442    }
443
444    /// Optional discovery scope for Ethernet beacons.
445    pub fn discovery_scope(&self) -> Option<&str> {
446        self.discovery_scope.as_deref().filter(|s| !s.is_empty())
447    }
448
449    /// Get the beacon interval, clamped to minimum. Default: 30s.
450    pub fn beacon_interval_secs(&self) -> u64 {
451        self.beacon_interval_secs
452            .unwrap_or(DEFAULT_BEACON_INTERVAL_SECS)
453            .max(MIN_BEACON_INTERVAL_SECS)
454    }
455}
456
457// ============================================================================
458// TCP Transport Configuration
459// ============================================================================
460
461/// Default TCP dataplane/path budget.
462const DEFAULT_TCP_MTU: u16 = 1400;
463
464/// Default TCP connect timeout in milliseconds.
465const DEFAULT_TCP_CONNECT_TIMEOUT_MS: u64 = 5000;
466
467/// Default timeout for an accepted inbound TCP connection to deliver its
468/// first complete FMP frame.
469const DEFAULT_TCP_FIRST_FRAME_TIMEOUT_MS: u64 = 3000;
470
471/// Default TCP keepalive interval in seconds.
472const DEFAULT_TCP_KEEPALIVE_SECS: u64 = 30;
473
474/// Default TCP receive buffer size (2 MB).
475const DEFAULT_TCP_RECV_BUF: usize = 2 * 1024 * 1024;
476
477/// Default TCP send buffer size (2 MB).
478const DEFAULT_TCP_SEND_BUF: usize = 2 * 1024 * 1024;
479
480/// Default maximum inbound TCP connections.
481const DEFAULT_TCP_MAX_INBOUND: usize = 256;
482
483/// Default WebSocket path accepted by the native plain-WS listener.
484const DEFAULT_WEBSOCKET_PATH: &str = "/fips";
485
486/// Default WebSocket FIPS path MTU.
487const DEFAULT_WEBSOCKET_MTU: u16 = 1400;
488
489/// Largest legal FIPS record plus conservative header room.
490const DEFAULT_WEBSOCKET_MAX_FRAME_BYTES: usize = 66 * 1024;
491
492const DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS: u64 = 5_000;
493const DEFAULT_WEBSOCKET_KEY_HINT_TIMEOUT_MS: u64 = 3_000;
494const DEFAULT_WEBSOCKET_RECONNECT_INITIAL_MS: u64 = 1_000;
495const DEFAULT_WEBSOCKET_RECONNECT_MAX_MS: u64 = 30_000;
496const DEFAULT_WEBSOCKET_MAX_CONNECTIONS: usize = 256;
497const DEFAULT_WEBSOCKET_MAX_INBOUND: usize = 128;
498const DEFAULT_WEBSOCKET_MAX_SEND_QUEUE: usize = 256;
499const DEFAULT_WEBSOCKET_PING_INTERVAL_SECS: u64 = 20;
500const DEFAULT_WEBSOCKET_IDLE_TIMEOUT_SECS: u64 = 90;
501
502/// TCP transport instance configuration.
503#[derive(Debug, Clone, Default, Serialize, Deserialize)]
504#[serde(deny_unknown_fields)]
505pub struct TcpConfig {
506    /// Listen address (e.g., "0.0.0.0:443"). If not set, outbound-only.
507    #[serde(default, skip_serializing_if = "Option::is_none")]
508    pub bind_addr: Option<String>,
509
510    /// Dataplane/path budget advertised for TCP routes. Defaults to 1400.
511    /// TCP byte-stream framing is independent of TCP_MAXSEG and is bounded by
512    /// the FMP/FSP wire record's u16 payload length.
513    #[serde(default, skip_serializing_if = "Option::is_none")]
514    pub mtu: Option<u16>,
515
516    /// Outbound connect timeout in milliseconds. Defaults to 5000.
517    #[serde(default, skip_serializing_if = "Option::is_none")]
518    pub connect_timeout_ms: Option<u64>,
519
520    /// Inbound first-frame timeout in milliseconds. Accepted connections
521    /// must deliver one complete FMP frame within this window or they are
522    /// closed. Set to 0 to disable. Defaults to 3000.
523    #[serde(default, skip_serializing_if = "Option::is_none")]
524    pub first_frame_timeout_ms: Option<u64>,
525
526    /// Enable TCP_NODELAY (disable Nagle). Defaults to true.
527    #[serde(default, skip_serializing_if = "Option::is_none")]
528    pub nodelay: Option<bool>,
529
530    /// TCP keepalive interval in seconds. 0 = disabled. Defaults to 30.
531    #[serde(default, skip_serializing_if = "Option::is_none")]
532    pub keepalive_secs: Option<u64>,
533
534    /// TCP receive buffer size in bytes. Defaults to 2 MB.
535    #[serde(default, skip_serializing_if = "Option::is_none")]
536    pub recv_buf_size: Option<usize>,
537
538    /// TCP send buffer size in bytes. Defaults to 2 MB.
539    #[serde(default, skip_serializing_if = "Option::is_none")]
540    pub send_buf_size: Option<usize>,
541
542    /// Maximum simultaneous inbound connections. Defaults to 256.
543    #[serde(default, skip_serializing_if = "Option::is_none")]
544    pub max_inbound_connections: Option<usize>,
545
546    /// Whether this transport should be advertised on Nostr overlay discovery.
547    /// Default: false.
548    #[serde(default, skip_serializing_if = "Option::is_none")]
549    pub advertise_on_nostr: Option<bool>,
550
551    /// Optional explicit public address to advertise. Required when
552    /// `bind_addr` is wildcard (e.g. `"0.0.0.0:443"`) and
553    /// `advertise_on_nostr: true`, since TCP has no STUN equivalent
554    /// for autodiscovery. Accepts either a bare IP (`"198.51.100.1"`
555    /// — the configured `bind_addr` port is appended) or a full
556    /// `host:port`. Common pattern on AWS EIP / cloud 1:1 NAT setups
557    /// where the public IP isn't bindable on the host.
558    #[serde(default, skip_serializing_if = "Option::is_none")]
559    pub external_addr: Option<String>,
560}
561
562impl TcpConfig {
563    /// Get the default MTU.
564    pub fn mtu(&self) -> u16 {
565        self.mtu.unwrap_or(DEFAULT_TCP_MTU)
566    }
567
568    /// Get the connect timeout in milliseconds.
569    pub fn connect_timeout_ms(&self) -> u64 {
570        self.connect_timeout_ms
571            .unwrap_or(DEFAULT_TCP_CONNECT_TIMEOUT_MS)
572    }
573
574    /// Get the inbound first-frame timeout in milliseconds. 0 disables it.
575    pub fn first_frame_timeout_ms(&self) -> u64 {
576        self.first_frame_timeout_ms
577            .unwrap_or(DEFAULT_TCP_FIRST_FRAME_TIMEOUT_MS)
578    }
579
580    /// Whether TCP_NODELAY is enabled. Default: true.
581    pub fn nodelay(&self) -> bool {
582        self.nodelay.unwrap_or(true)
583    }
584
585    /// Get the keepalive interval in seconds. 0 = disabled. Default: 30.
586    pub fn keepalive_secs(&self) -> u64 {
587        self.keepalive_secs.unwrap_or(DEFAULT_TCP_KEEPALIVE_SECS)
588    }
589
590    /// Get the receive buffer size. Default: 2 MB.
591    pub fn recv_buf_size(&self) -> usize {
592        self.recv_buf_size.unwrap_or(DEFAULT_TCP_RECV_BUF)
593    }
594
595    /// Get the send buffer size. Default: 2 MB.
596    pub fn send_buf_size(&self) -> usize {
597        self.send_buf_size.unwrap_or(DEFAULT_TCP_SEND_BUF)
598    }
599
600    /// Get the maximum number of inbound connections. Default: 256.
601    pub fn max_inbound_connections(&self) -> usize {
602        self.max_inbound_connections
603            .unwrap_or(DEFAULT_TCP_MAX_INBOUND)
604    }
605
606    /// Whether this TCP transport should be advertised on Nostr discovery.
607    pub fn advertise_on_nostr(&self) -> bool {
608        self.advertise_on_nostr.unwrap_or(false)
609    }
610
611    /// Parse `external_addr` against the configured `bind_addr` port,
612    /// returning the absolute `SocketAddr` to advertise on Nostr.
613    /// Returns `None` if `external_addr` is unset or malformed, or if
614    /// `bind_addr` is unset / unparseable so no port can be inferred.
615    pub fn external_advert_addr(&self) -> Option<SocketAddr> {
616        let raw = self.external_addr.as_deref()?;
617        let bind_port = parse_bind_port(self.bind_addr.as_deref()?)?;
618        parse_external_advert_addr(raw, bind_port)
619    }
620}
621
622/// WebSocket physical transport configuration.
623///
624/// The native listener intentionally speaks plain WebSocket so deployments can
625/// bind it to localhost or a private interface and terminate TLS in a reverse
626/// proxy. Clients use explicit `wss://` seed URLs; plaintext `ws://` seeds are
627/// accepted only for loopback development and tests. A bounded nonce/key-hint
628/// exchange identifies a URL-only seed before Noise IK; after that exchange,
629/// every binary WebSocket message carries exactly one bounded FIPS physical
630/// record.
631#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
632#[serde(deny_unknown_fields)]
633pub struct WebSocketConfig {
634    /// Optional native plain-WS listener address. Unset means client-only.
635    #[serde(default, skip_serializing_if = "Option::is_none")]
636    pub bind_addr: Option<String>,
637
638    /// Public `wss://` URL advertised for this listener, separate from bind.
639    #[serde(default, skip_serializing_if = "Option::is_none")]
640    pub public_url: Option<String>,
641
642    /// One or more explicit first-adjacency seed URLs.
643    #[serde(default, skip_serializing_if = "Vec::is_empty")]
644    pub seed_urls: Vec<String>,
645
646    /// HTTP path accepted by the native listener. Defaults to `/fips`.
647    #[serde(default, skip_serializing_if = "Option::is_none")]
648    pub path: Option<String>,
649
650    /// Dataplane/path budget. Defaults to 1400 bytes.
651    #[serde(default, skip_serializing_if = "Option::is_none")]
652    pub mtu: Option<u16>,
653
654    /// Maximum binary WebSocket message size.
655    #[serde(default, skip_serializing_if = "Option::is_none")]
656    pub max_frame_bytes: Option<usize>,
657
658    /// Maximum queued outbound records per connection.
659    #[serde(default, skip_serializing_if = "Option::is_none")]
660    pub max_send_queue: Option<usize>,
661
662    /// Maximum total WebSocket connections for this transport instance.
663    #[serde(default, skip_serializing_if = "Option::is_none")]
664    pub max_connections: Option<usize>,
665
666    /// Maximum simultaneous inbound WebSocket connections.
667    #[serde(default, skip_serializing_if = "Option::is_none")]
668    pub max_inbound_connections: Option<usize>,
669
670    /// Outbound TCP/TLS/WebSocket connect timeout.
671    #[serde(default, skip_serializing_if = "Option::is_none")]
672    pub connect_timeout_ms: Option<u64>,
673
674    /// Time allowed for the untrusted seed-key hint exchange.
675    #[serde(default, skip_serializing_if = "Option::is_none")]
676    pub key_hint_timeout_ms: Option<u64>,
677
678    /// Initial reconnect delay for configured seeds.
679    #[serde(default, skip_serializing_if = "Option::is_none")]
680    pub reconnect_initial_ms: Option<u64>,
681
682    /// Maximum reconnect delay for configured seeds.
683    #[serde(default, skip_serializing_if = "Option::is_none")]
684    pub reconnect_max_ms: Option<u64>,
685
686    /// WebSocket ping interval. Zero disables transport pings.
687    #[serde(default, skip_serializing_if = "Option::is_none")]
688    pub ping_interval_secs: Option<u64>,
689
690    /// Close connections with no received frame for this long. Zero disables.
691    #[serde(default, skip_serializing_if = "Option::is_none")]
692    pub idle_timeout_secs: Option<u64>,
693
694    /// Accept fresh inbound Noise IK handshakes. Defaults to true whenever the
695    /// transport has a listener or seed URL. WebSocket dial direction does not
696    /// constrain FIPS session direction on an established routed adjacency.
697    #[serde(default, skip_serializing_if = "Option::is_none")]
698    pub accept_connections: Option<bool>,
699}
700
701impl WebSocketConfig {
702    pub fn path(&self) -> &str {
703        self.path.as_deref().unwrap_or(DEFAULT_WEBSOCKET_PATH)
704    }
705
706    pub fn mtu(&self) -> u16 {
707        self.mtu.unwrap_or(DEFAULT_WEBSOCKET_MTU)
708    }
709
710    pub fn max_frame_bytes(&self) -> usize {
711        self.max_frame_bytes
712            .unwrap_or(DEFAULT_WEBSOCKET_MAX_FRAME_BYTES)
713    }
714
715    pub fn max_send_queue(&self) -> usize {
716        self.max_send_queue
717            .unwrap_or(DEFAULT_WEBSOCKET_MAX_SEND_QUEUE)
718            .max(1)
719    }
720
721    pub fn max_connections(&self) -> usize {
722        self.max_connections
723            .unwrap_or(DEFAULT_WEBSOCKET_MAX_CONNECTIONS)
724            .max(1)
725    }
726
727    pub fn max_inbound_connections(&self) -> usize {
728        self.max_inbound_connections
729            .unwrap_or(DEFAULT_WEBSOCKET_MAX_INBOUND)
730            .max(1)
731            .min(self.max_connections())
732    }
733
734    pub fn connect_timeout_ms(&self) -> u64 {
735        self.connect_timeout_ms
736            .unwrap_or(DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS)
737            .max(1)
738    }
739
740    pub fn key_hint_timeout_ms(&self) -> u64 {
741        self.key_hint_timeout_ms
742            .unwrap_or(DEFAULT_WEBSOCKET_KEY_HINT_TIMEOUT_MS)
743            .max(1)
744    }
745
746    pub fn reconnect_initial_ms(&self) -> u64 {
747        self.reconnect_initial_ms
748            .unwrap_or(DEFAULT_WEBSOCKET_RECONNECT_INITIAL_MS)
749            .max(1)
750    }
751
752    pub fn reconnect_max_ms(&self) -> u64 {
753        self.reconnect_max_ms
754            .unwrap_or(DEFAULT_WEBSOCKET_RECONNECT_MAX_MS)
755            .max(self.reconnect_initial_ms())
756    }
757
758    pub fn ping_interval_secs(&self) -> u64 {
759        self.ping_interval_secs
760            .unwrap_or(DEFAULT_WEBSOCKET_PING_INTERVAL_SECS)
761    }
762
763    pub fn idle_timeout_secs(&self) -> u64 {
764        self.idle_timeout_secs
765            .unwrap_or(DEFAULT_WEBSOCKET_IDLE_TIMEOUT_SECS)
766    }
767
768    pub fn accept_connections(&self) -> bool {
769        self.accept_connections
770            .unwrap_or_else(|| self.bind_addr.is_some() || !self.seed_urls.is_empty())
771    }
772
773    pub fn validate(&self) -> Result<(), String> {
774        if let Some(bind_addr) = self.bind_addr.as_deref() {
775            bind_addr
776                .parse::<SocketAddr>()
777                .map_err(|error| format!("invalid bind_addr {bind_addr:?}: {error}"))?;
778        }
779        if !self.path().starts_with('/') || self.path().contains('?') || self.path().contains('#') {
780            return Err("path must be an absolute HTTP path without query or fragment".into());
781        }
782        if let Some(public_url) = self.public_url.as_deref() {
783            validate_websocket_url(public_url, false)?;
784            let uri = public_url
785                .parse::<tokio_tungstenite::tungstenite::http::Uri>()
786                .map_err(|error| format!("invalid public_url: {error}"))?;
787            if uri.path() != self.path() {
788                return Err(format!(
789                    "public_url path {:?} does not match configured path {:?}",
790                    uri.path(),
791                    self.path()
792                ));
793            }
794            if self.bind_addr.is_none() {
795                return Err("public_url requires bind_addr".into());
796            }
797        }
798        let mut unique = std::collections::HashSet::new();
799        for seed_url in &self.seed_urls {
800            validate_websocket_url(seed_url, true)?;
801            if !unique.insert(seed_url) {
802                return Err(format!("duplicate seed URL {seed_url:?}"));
803            }
804        }
805        let minimum_frame = usize::from(self.mtu()).saturating_add(64);
806        if self.max_frame_bytes() < minimum_frame || self.max_frame_bytes() > 1024 * 1024 {
807            return Err(format!(
808                "max_frame_bytes must be between {minimum_frame} and 1048576"
809            ));
810        }
811        if self.max_send_queue() > 4096 {
812            return Err("max_send_queue must not exceed 4096".into());
813        }
814        if self.max_connections() > 4096 {
815            return Err("max_connections must not exceed 4096".into());
816        }
817        if self.max_inbound_connections() > self.max_connections() {
818            return Err("max_inbound_connections must not exceed max_connections".into());
819        }
820        if self.ping_interval_secs() > 0
821            && self.idle_timeout_secs() > 0
822            && self.idle_timeout_secs() <= self.ping_interval_secs()
823        {
824            return Err("idle_timeout_secs must exceed ping_interval_secs".into());
825        }
826        Ok(())
827    }
828}
829
830fn validate_websocket_url(raw: &str, allow_loopback_plaintext: bool) -> Result<(), String> {
831    let uri = raw
832        .parse::<tokio_tungstenite::tungstenite::http::Uri>()
833        .map_err(|error| format!("invalid WebSocket URL {raw:?}: {error}"))?;
834    let scheme = uri
835        .scheme_str()
836        .ok_or_else(|| format!("WebSocket URL {raw:?} is missing a scheme"))?;
837    let host = uri
838        .host()
839        .ok_or_else(|| format!("WebSocket URL {raw:?} is missing a host"))?;
840    if uri.authority().is_none() || uri.path().is_empty() {
841        return Err(format!("invalid WebSocket URL {raw:?}"));
842    }
843    match scheme {
844        "wss" => Ok(()),
845        "ws" if allow_loopback_plaintext && websocket_host_is_loopback(host) => Ok(()),
846        "ws" => Err(format!(
847            "plaintext WebSocket URL {raw:?} is allowed only for loopback seeds"
848        )),
849        _ => Err(format!("WebSocket URL {raw:?} must use wss://")),
850    }
851}
852
853fn websocket_host_is_loopback(host: &str) -> bool {
854    host.eq_ignore_ascii_case("localhost")
855        || host
856            .trim_matches(['[', ']'])
857            .parse::<IpAddr>()
858            .is_ok_and(|ip| ip.is_loopback())
859}
860
861mod aggregate;
862mod ble;
863#[cfg(test)]
864mod tests;
865mod tor;
866mod webrtc;
867
868pub use aggregate::TransportsConfig;
869pub use ble::BleConfig;
870pub use tor::{DirectoryServiceConfig, TorConfig};
871pub use webrtc::WebRtcConfig;