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