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