1use super::*;
2
3impl Node {
4 fn new_dataplane_node() -> DataplaneNode {
5 DataplaneLiveNode::new(AdmissionConfig::new(
6 1024,
7 FIPS_ENDPOINT_DIRECT_PACKET_QUEUE_MAX_PACKETS,
8 ))
9 }
10
11 pub fn new(config: Config) -> Result<Self, NodeError> {
13 config.validate()?;
14 let identity = config.create_identity()?;
15 let node_addr = *identity.node_addr();
16 let is_leaf_only = config.is_leaf_only();
17
18 let mut startup_epoch = [0u8; 8];
19 rand::rng().fill_bytes(&mut startup_epoch);
20
21 let mut bloom_state = if is_leaf_only {
22 BloomState::leaf_only(node_addr)
23 } else {
24 BloomState::new(node_addr)
25 };
26 bloom_state.set_update_debounce_ms(config.node.bloom.update_debounce_ms);
27
28 let tun_state = if config.tun.enabled {
29 TunState::Configured
30 } else {
31 TunState::Disabled
32 };
33
34 let mut tree_state = TreeState::new(node_addr);
36 tree_state.set_parent_hysteresis(config.node.tree.parent_hysteresis);
37 tree_state.set_hold_down(config.node.tree.hold_down_secs);
38 tree_state.set_flap_dampening(
39 config.node.tree.flap_threshold,
40 config.node.tree.flap_window_secs,
41 config.node.tree.flap_dampening_secs,
42 );
43 tree_state
44 .sign_declaration(&identity)
45 .expect("signing own declaration should never fail");
46
47 let coord_cache = CoordCache::new(
48 config.node.cache.coord_size,
49 config.node.cache.coord_ttl_secs * 1000,
50 );
51 let rl = &config.node.rate_limit;
52 let msg1_rate_limiter = HandshakeRateLimiter::with_params(
53 rate_limit::TokenBucket::with_params(rl.handshake_burst, rl.handshake_rate),
54 config.node.limits.max_pending_inbound,
55 );
56
57 let max_connections = config.node.limits.max_connections;
58 let max_peers = config.node.limits.max_peers;
59 let max_links = config.node.limits.max_links;
60 let coords_response_interval_ms = config.node.session.coords_response_interval_ms;
61 let backoff_base_secs = config.node.discovery.backoff_base_secs;
62 let backoff_max_secs = config.node.discovery.backoff_max_secs;
63 let forward_min_interval_secs = config.node.discovery.forward_min_interval_secs;
64
65 let (host_map, peer_acl) = Self::host_map_and_peer_acl(&config);
66 let configured_peers = ConfiguredPeerLookup::from_config(&config);
67 let local_rendezvous =
68 local_rendezvous::LocalRendezvous::new(&config, &identity, startup_epoch);
69
70 Ok(Self {
71 identity,
72 startup_epoch,
73 started_at: std::time::Instant::now(),
74 config,
75 state: NodeState::Created,
76 is_leaf_only,
77 tree_state,
78 bloom_state,
79 coord_cache,
80 learned_routes: LearnedRouteTable::default(),
81 session_direct_degradation: SessionDirectDegradation::default(),
82 recent_requests: RecentDiscoveryRequests::default(),
83 transports: HashMap::new(),
84 transport_rebind_packet_cutoffs_ms: HashMap::new(),
85 #[cfg(feature = "host-ble-transport")]
86 host_ble_io: None,
87 transport_drops: TransportDropTracker::default(),
88 transport_socket_drops: TransportDropTracker::default(),
89 transport_namespace_drops: TransportDropTracker::default(),
90 links: LinkRegistry::default(),
91 packet_tx: None,
92 packet_rx: None,
93 dataplane: Self::new_dataplane_node(),
94 deferred_dataplane_control_turns: VecDeque::new(),
95 deferred_session_forwards: Default::default(),
96 dataplane_fast_ingress_rx: None,
97 dataplane_direct_fsp_sources: Arc::new(HashMap::new()),
98 dataplane_direct_fsp_sources_dirty: true,
99 dataplane_transport_send_batch_packets: DATAPLANE_TRANSPORT_SEND_BATCH_PACKETS,
100 peers: PeerLifecycleRegistry::default(),
101 sessions: SessionRegistry::default(),
102 identity_cache: IdentityCache::default(),
103 pending_session_traffic: PendingSessionTrafficQueues::default(),
104 pending_lookups: handlers::discovery::PendingDiscoveryLookups::default(),
105 max_connections,
106 max_peers,
107 max_links,
108 next_link_id: 1,
109 next_transport_id: 1,
110 stats: stats::NodeStats::new(),
111 stats_history: stats_history::StatsHistory::new(),
112 tun_state,
113 tun_name: None,
114 tun_tx: None,
115 tun_outbound_rx: None,
116 external_packet_tx: None,
117 endpoint_control_rx: None,
118 endpoint_data_rx: None,
119 endpoint_events: EndpointEventRuntime::default(),
120 endpoint_services: EndpointServiceRuntime::default(),
121 tun_reader_handle: None,
122 tun_writer_handle: None,
123 #[cfg(target_os = "macos")]
124 tun_shutdown_fd: None,
125 dns_identity_rx: None,
126 dns_task: None,
127 index_allocator: IndexAllocator::new(),
128 pending_outbound: PendingOutboundHandshakes::default(),
129 msg1_rate_limiter,
130 icmp_rate_limiter: IcmpRateLimiter::new(),
131 routing_error_rate_limiter: RoutingErrorRateLimiter::new(),
132 coords_response_rate_limiter: RoutingErrorRateLimiter::with_interval(
133 std::time::Duration::from_millis(coords_response_interval_ms),
134 ),
135 discovery_backoff: DiscoveryBackoff::with_params(backoff_base_secs, backoff_max_secs),
136 discovery_forward_limiter: DiscoveryForwardRateLimiter::with_interval(
137 std::time::Duration::from_secs(forward_min_interval_secs),
138 ),
139 pending_connects: Vec::new(),
140 retry_pending: retry::PendingRouteRetries::default(),
141 nostr_discovery: None,
142 pending_mesh_signals: HashMap::new(),
143 nostr_discovery_started_at_ms: None,
144 lan_discovery: None,
145 local_rendezvous,
146 startup_open_discovery_sweep_done: false,
147 bootstrap_transports: BootstrapTransports::default(),
148 discovery_fallback_transit: DiscoveryFallbackTransit::default(),
149 last_parent_reeval: None,
150 last_congestion_log: None,
151 estimated_mesh_size: None,
152 last_mesh_size_log: None,
153 last_self_warn: None,
154 local_send_failures: LocalSendFailures::default(),
155 last_rx_loop_maintenance_timeout_at: None,
156 peer_aliases: HashMap::new(),
157 configured_peers,
158 peer_acl,
159 host_map,
160 path_mtu_lookup: Arc::new(std::sync::RwLock::new(HashMap::new())),
161 })
162 }
163
164 pub fn with_identity(identity: Identity, config: Config) -> Result<Self, NodeError> {
169 config.validate()?;
170 let node_addr = *identity.node_addr();
171
172 let mut startup_epoch = [0u8; 8];
173 rand::rng().fill_bytes(&mut startup_epoch);
174
175 let tun_state = if config.tun.enabled {
176 TunState::Configured
177 } else {
178 TunState::Disabled
179 };
180
181 let mut tree_state = TreeState::new(node_addr);
183 tree_state.set_parent_hysteresis(config.node.tree.parent_hysteresis);
184 tree_state.set_hold_down(config.node.tree.hold_down_secs);
185 tree_state.set_flap_dampening(
186 config.node.tree.flap_threshold,
187 config.node.tree.flap_window_secs,
188 config.node.tree.flap_dampening_secs,
189 );
190 tree_state
191 .sign_declaration(&identity)
192 .expect("signing own declaration should never fail");
193
194 let mut bloom_state = BloomState::new(node_addr);
195 bloom_state.set_update_debounce_ms(config.node.bloom.update_debounce_ms);
196
197 let coord_cache = CoordCache::new(
198 config.node.cache.coord_size,
199 config.node.cache.coord_ttl_secs * 1000,
200 );
201 let rl = &config.node.rate_limit;
202 let msg1_rate_limiter = HandshakeRateLimiter::with_params(
203 rate_limit::TokenBucket::with_params(rl.handshake_burst, rl.handshake_rate),
204 config.node.limits.max_pending_inbound,
205 );
206
207 let max_connections = config.node.limits.max_connections;
208 let max_peers = config.node.limits.max_peers;
209 let max_links = config.node.limits.max_links;
210 let coords_response_interval_ms = config.node.session.coords_response_interval_ms;
211
212 let (host_map, peer_acl) = Self::host_map_and_peer_acl(&config);
213 let configured_peers = ConfiguredPeerLookup::from_config(&config);
214 let local_rendezvous =
215 local_rendezvous::LocalRendezvous::new(&config, &identity, startup_epoch);
216
217 Ok(Self {
218 identity,
219 startup_epoch,
220 started_at: std::time::Instant::now(),
221 config,
222 state: NodeState::Created,
223 is_leaf_only: false,
224 tree_state,
225 bloom_state,
226 coord_cache,
227 learned_routes: LearnedRouteTable::default(),
228 session_direct_degradation: SessionDirectDegradation::default(),
229 recent_requests: RecentDiscoveryRequests::default(),
230 transports: HashMap::new(),
231 transport_rebind_packet_cutoffs_ms: HashMap::new(),
232 #[cfg(feature = "host-ble-transport")]
233 host_ble_io: None,
234 transport_drops: TransportDropTracker::default(),
235 transport_socket_drops: TransportDropTracker::default(),
236 transport_namespace_drops: TransportDropTracker::default(),
237 links: LinkRegistry::default(),
238 packet_tx: None,
239 packet_rx: None,
240 dataplane: Self::new_dataplane_node(),
241 deferred_dataplane_control_turns: VecDeque::new(),
242 deferred_session_forwards: Default::default(),
243 dataplane_fast_ingress_rx: None,
244 dataplane_direct_fsp_sources: Arc::new(HashMap::new()),
245 dataplane_direct_fsp_sources_dirty: true,
246 dataplane_transport_send_batch_packets: DATAPLANE_TRANSPORT_SEND_BATCH_PACKETS,
247 peers: PeerLifecycleRegistry::default(),
248 sessions: SessionRegistry::default(),
249 identity_cache: IdentityCache::default(),
250 pending_session_traffic: PendingSessionTrafficQueues::default(),
251 pending_lookups: handlers::discovery::PendingDiscoveryLookups::default(),
252 max_connections,
253 max_peers,
254 max_links,
255 next_link_id: 1,
256 next_transport_id: 1,
257 stats: stats::NodeStats::new(),
258 stats_history: stats_history::StatsHistory::new(),
259 tun_state,
260 tun_name: None,
261 tun_tx: None,
262 tun_outbound_rx: None,
263 external_packet_tx: None,
264 endpoint_control_rx: None,
265 endpoint_data_rx: None,
266 endpoint_events: EndpointEventRuntime::default(),
267 endpoint_services: EndpointServiceRuntime::default(),
268 tun_reader_handle: None,
269 tun_writer_handle: None,
270 #[cfg(target_os = "macos")]
271 tun_shutdown_fd: None,
272 dns_identity_rx: None,
273 dns_task: None,
274 index_allocator: IndexAllocator::new(),
275 pending_outbound: PendingOutboundHandshakes::default(),
276 msg1_rate_limiter,
277 icmp_rate_limiter: IcmpRateLimiter::new(),
278 routing_error_rate_limiter: RoutingErrorRateLimiter::new(),
279 coords_response_rate_limiter: RoutingErrorRateLimiter::with_interval(
280 std::time::Duration::from_millis(coords_response_interval_ms),
281 ),
282 discovery_backoff: DiscoveryBackoff::new(),
283 discovery_forward_limiter: DiscoveryForwardRateLimiter::new(),
284 pending_connects: Vec::new(),
285 retry_pending: retry::PendingRouteRetries::default(),
286 nostr_discovery: None,
287 pending_mesh_signals: HashMap::new(),
288 nostr_discovery_started_at_ms: None,
289 lan_discovery: None,
290 local_rendezvous,
291 startup_open_discovery_sweep_done: false,
292 bootstrap_transports: BootstrapTransports::default(),
293 discovery_fallback_transit: DiscoveryFallbackTransit::default(),
294 last_parent_reeval: None,
295 last_congestion_log: None,
296 estimated_mesh_size: None,
297 last_mesh_size_log: None,
298 last_self_warn: None,
299 local_send_failures: LocalSendFailures::default(),
300 last_rx_loop_maintenance_timeout_at: None,
301 peer_aliases: HashMap::new(),
302 configured_peers,
303 peer_acl,
304 host_map,
305 path_mtu_lookup: Arc::new(std::sync::RwLock::new(HashMap::new())),
306 })
307 }
308
309 pub fn leaf_only(config: Config) -> Result<Self, NodeError> {
311 let mut node = Self::new(config)?;
312 node.is_leaf_only = true;
313 node.bloom_state = BloomState::leaf_only(*node.identity.node_addr());
314 Ok(node)
315 }
316
317 pub(super) fn host_map_and_peer_acl(config: &Config) -> (Arc<HostMap>, acl::PeerAclReloader) {
318 let base_host_map = HostMap::from_peer_configs(config.peers());
319 if !config.node.system_files_enabled {
320 return (
321 Arc::new(base_host_map.clone()),
322 acl::PeerAclReloader::memory_only(base_host_map),
323 );
324 }
325
326 let mut host_map = base_host_map.clone();
327 let hosts_path = std::path::PathBuf::from(crate::upper::hosts::DEFAULT_HOSTS_PATH);
328 let hosts_file = HostMap::load_hosts_file(std::path::Path::new(
329 crate::upper::hosts::DEFAULT_HOSTS_PATH,
330 ));
331 host_map.merge(hosts_file);
332 let peer_acl = acl::PeerAclReloader::with_alias_sources(
333 std::path::PathBuf::from(acl::DEFAULT_PEERS_ALLOW_PATH),
334 std::path::PathBuf::from(acl::DEFAULT_PEERS_DENY_PATH),
335 base_host_map,
336 hosts_path,
337 );
338 (Arc::new(host_map), peer_acl)
339 }
340
341 pub(super) async fn create_transports(&mut self, packet_tx: &PacketTx) -> Vec<TransportHandle> {
345 let mut transports = Vec::new();
346
347 let udp_instances: Vec<_> = self
349 .config
350 .transports
351 .udp
352 .iter()
353 .map(|(name, config)| (name.map(|s| s.to_string()), config.clone()))
354 .collect();
355 let ipv4_bindings: HashSet<_> = udp_instances
356 .iter()
357 .filter(|(name, _)| name.is_some())
358 .filter_map(|(_, config)| wildcard_udp_binding(config, false))
359 .collect();
360
361 for (name, udp_config) in udp_instances {
363 let transport_id = self.allocate_transport_id();
364 let ipv6_only = name.is_some()
365 && wildcard_udp_binding(&udp_config, true)
366 .is_some_and(|binding| ipv4_bindings.contains(&binding));
367 let udp = UdpTransport::new(transport_id, name, udp_config, packet_tx.clone())
368 .with_ipv6_only(ipv6_only);
369 transports.push(TransportHandle::Udp(udp));
370 }
371
372 #[cfg(feature = "sim-transport")]
373 {
374 let sim_instances: Vec<_> = self
375 .config
376 .transports
377 .sim
378 .iter()
379 .map(|(name, config)| (name.map(|s| s.to_string()), config.clone()))
380 .collect();
381
382 for (name, sim_config) in sim_instances {
383 let transport_id = self.allocate_transport_id();
384 let sim = crate::transport::sim::SimTransport::new(
385 transport_id,
386 name,
387 sim_config,
388 packet_tx.clone(),
389 );
390 transports.push(TransportHandle::Sim(sim));
391 }
392 }
393
394 #[cfg(any(target_os = "linux", target_os = "macos"))]
396 {
397 let eth_instances: Vec<_> = self
398 .config
399 .transports
400 .ethernet
401 .iter()
402 .map(|(name, config)| (name.map(|s| s.to_string()), config.clone()))
403 .collect();
404 let xonly = self.identity.pubkey();
405 for (name, eth_config) in eth_instances {
406 let mut eth_config = eth_config;
407 if eth_config.discovery_scope.is_none() {
408 eth_config.discovery_scope = self.lan_discovery_scope();
409 }
410 let transport_id = self.allocate_transport_id();
411 let mut eth =
412 EthernetTransport::new(transport_id, name, eth_config, packet_tx.clone());
413 eth.set_local_pubkey(xonly);
414 transports.push(TransportHandle::Ethernet(eth));
415 }
416 }
417
418 let tcp_instances: Vec<_> = self
420 .config
421 .transports
422 .tcp
423 .iter()
424 .map(|(name, config)| (name.map(|s| s.to_string()), config.clone()))
425 .collect();
426
427 for (name, tcp_config) in tcp_instances {
428 let transport_id = self.allocate_transport_id();
429 let tcp = TcpTransport::new(transport_id, name, tcp_config, packet_tx.clone());
430 transports.push(TransportHandle::Tcp(tcp));
431 }
432
433 let websocket_instances: Vec<_> = self
434 .config
435 .transports
436 .websocket
437 .iter()
438 .map(|(name, config)| (name.map(str::to_string), config.clone()))
439 .collect();
440 for (name, websocket_config) in websocket_instances {
441 let transport_id = self.allocate_transport_id();
442 let websocket = WebSocketTransport::new(
443 transport_id,
444 name,
445 websocket_config,
446 packet_tx.clone(),
447 &self.identity,
448 );
449 transports.push(TransportHandle::WebSocket(Box::new(websocket)));
450 }
451
452 let tor_instances: Vec<_> = self
454 .config
455 .transports
456 .tor
457 .iter()
458 .map(|(name, config)| (name.map(|s| s.to_string()), config.clone()))
459 .collect();
460
461 for (name, tor_config) in tor_instances {
462 let transport_id = self.allocate_transport_id();
463 let tor = TorTransport::new(transport_id, name, tor_config, packet_tx.clone());
464 transports.push(TransportHandle::Tor(tor));
465 }
466
467 let webrtc_instances: Vec<_> = self
468 .config
469 .transports
470 .webrtc
471 .iter()
472 .map(|(name, config)| (name.map(|s| s.to_string()), config.clone()))
473 .collect();
474
475 #[cfg(feature = "webrtc-transport")]
476 {
477 for (name, webrtc_config) in webrtc_instances {
478 let transport_id = self.allocate_transport_id();
479 match WebRtcTransport::new(
480 transport_id,
481 name,
482 webrtc_config,
483 packet_tx.clone(),
484 &self.identity,
485 &self.config.node.discovery.nostr,
486 ) {
487 Ok(webrtc) => transports.push(TransportHandle::WebRtc(Box::new(webrtc))),
488 Err(err) => {
489 warn!(
490 transport_id = %transport_id,
491 error = %err,
492 "failed to initialize WebRTC transport"
493 );
494 }
495 }
496 }
497 }
498 #[cfg(not(feature = "webrtc-transport"))]
499 if !webrtc_instances.is_empty() {
500 warn!("WebRTC transport configured but this build lacks WebRTC transport support");
501 }
502
503 #[cfg(feature = "host-ble-transport")]
504 {
505 if let Some(io) = self.host_ble_io.take() {
506 let (name, ble_config) = self
507 .config
508 .transports
509 .ble
510 .iter()
511 .next()
512 .map(|(name, config)| (name.map(str::to_string), config.clone()))
513 .unwrap_or((None, crate::config::BleConfig::default()));
514 let transport_id = self.allocate_transport_id();
515 let mut ble = crate::transport::ble::BleTransport::new(
516 transport_id,
517 name,
518 ble_config,
519 io,
520 packet_tx.clone(),
521 );
522 ble.set_local_pubkey(self.identity.pubkey().serialize());
523 transports.push(TransportHandle::Ble(ble));
524 } else if !self.config.transports.ble.is_empty() {
525 warn!("host BLE configured without a platform adapter");
526 }
527 }
528
529 #[cfg(all(bluer_available, not(feature = "host-ble-transport")))]
531 {
532 let ble_instances: Vec<_> = self
533 .config
534 .transports
535 .ble
536 .iter()
537 .map(|(name, config)| (name.map(|s| s.to_string()), config.clone()))
538 .collect();
539
540 #[cfg(all(bluer_available, not(test)))]
541 for (name, ble_config) in ble_instances {
542 let transport_id = self.allocate_transport_id();
543 let adapter = ble_config.adapter().to_string();
544 let mtu = ble_config.mtu();
545 match crate::transport::ble::io::BluerIo::new(&adapter, mtu).await {
546 Ok(io) => {
547 let mut ble = crate::transport::ble::BleTransport::new(
548 transport_id,
549 name,
550 ble_config,
551 io,
552 packet_tx.clone(),
553 );
554 ble.set_local_pubkey(self.identity.pubkey().serialize());
555 transports.push(TransportHandle::Ble(ble));
556 }
557 Err(e) => {
558 tracing::warn!(adapter = %adapter, error = %e, "failed to initialize BLE adapter");
559 }
560 }
561 }
562
563 #[cfg(any(not(bluer_available), test))]
564 if !ble_instances.is_empty() {
565 #[cfg(not(test))]
566 tracing::warn!("BLE transport configured but this build lacks BlueZ support");
567 }
568 }
569
570 transports
571 }
572
573 #[cfg(feature = "host-ble-transport")]
574 pub(crate) fn set_host_ble_io(&mut self, io: crate::transport::ble::host::HostBleIo) {
575 self.host_ble_io = Some(io);
576 }
577
578 pub(super) fn find_transport_for_type(&self, transport_type: &str) -> Option<TransportId> {
588 self.transports
589 .iter()
590 .filter(|(id, handle)| {
591 handle.transport_type().name == transport_type
592 && handle.is_operational()
593 && !self.bootstrap_transports.contains(id)
594 && !self.is_local_rendezvous_transport(id)
595 })
596 .min_by_key(|(id, _)| id.as_u32())
597 .map(|(id, _)| *id)
598 }
599
600 #[cfg(any(target_os = "linux", target_os = "macos"))]
606 pub(super) fn resolve_ethernet_addr(
607 &self,
608 addr_str: &str,
609 ) -> Result<(TransportId, TransportAddr), NodeError> {
610 let (iface, mac_str) = addr_str.split_once('/').ok_or_else(|| {
611 NodeError::NoTransportForType(format!(
612 "invalid Ethernet address format '{}': expected 'interface/mac'",
613 addr_str
614 ))
615 })?;
616
617 let transport_id = self
619 .transports
620 .iter()
621 .find(|(_, handle)| {
622 handle.transport_type().name == "ethernet"
623 && handle.is_operational()
624 && handle.interface_name() == Some(iface)
625 })
626 .map(|(id, _)| *id)
627 .ok_or_else(|| {
628 NodeError::NoTransportForType(format!(
629 "no operational Ethernet transport for interface '{}'",
630 iface
631 ))
632 })?;
633
634 let mac = crate::transport::ethernet::parse_mac_string(mac_str).map_err(|e| {
635 NodeError::NoTransportForType(format!("invalid MAC in '{}': {}", addr_str, e))
636 })?;
637
638 Ok((transport_id, TransportAddr::from_bytes(&mac)))
639 }
640
641 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
642 pub(super) fn resolve_ethernet_addr(
643 &self,
644 _addr_str: &str,
645 ) -> Result<(TransportId, TransportAddr), NodeError> {
646 Err(NodeError::NoTransportForType(
647 "Ethernet transport is not supported on this platform".to_string(),
648 ))
649 }
650
651 #[cfg(bluer_available)]
655 pub(super) fn resolve_ble_addr(
656 &self,
657 addr_str: &str,
658 ) -> Result<(TransportId, TransportAddr), NodeError> {
659 let ta = TransportAddr::from_string(addr_str);
660 let adapter = crate::transport::ble::addr::adapter_from_addr(&ta).ok_or_else(|| {
661 NodeError::NoTransportForType(format!(
662 "invalid BLE address format '{}': expected 'adapter/mac'",
663 addr_str
664 ))
665 })?;
666
667 let transport_id = self
669 .transports
670 .iter()
671 .find(|(_, handle)| handle.transport_type().name == "ble" && handle.is_operational())
672 .map(|(id, _)| *id)
673 .ok_or_else(|| {
674 NodeError::NoTransportForType(format!(
675 "no operational BLE transport for adapter '{}'",
676 adapter
677 ))
678 })?;
679
680 crate::transport::ble::addr::BleAddr::parse(addr_str).map_err(|e| {
682 NodeError::NoTransportForType(format!("invalid BLE address '{}': {}", addr_str, e))
683 })?;
684
685 Ok((transport_id, TransportAddr::from_string(addr_str)))
686 }
687}
688
689fn wildcard_udp_binding(
690 config: &crate::config::UdpConfig,
691 ipv6: bool,
692) -> Option<(u16, Option<String>)> {
693 if config.outbound_only() {
694 return None;
695 }
696 let addr = config
697 .bind_addr
698 .as_deref()?
699 .parse::<std::net::SocketAddr>()
700 .ok()?;
701 (addr.ip().is_unspecified() && addr.is_ipv6() == ipv6 && addr.port() != 0)
702 .then(|| (addr.port(), config.bind_interface.clone()))
703}