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