Skip to main content

fips_core/node/
core_impl.rs

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    /// Create a new node from configuration.
12    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        // Initialize tree state with signed self-declaration
35        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    /// Create a node with a specific identity.
165    ///
166    /// This constructor validates cross-field config invariants before
167    /// constructing the node, same as [`Node::new`].
168    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        // Initialize tree state with signed self-declaration
182        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    /// Create a leaf-only node (simplified state).
310    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    /// Create transport instances from configuration.
342    ///
343    /// Returns a vector of TransportHandles for all configured transports.
344    pub(super) async fn create_transports(&mut self, packet_tx: &PacketTx) -> Vec<TransportHandle> {
345        let mut transports = Vec::new();
346
347        // Collect UDP configs with optional names to avoid borrow conflicts
348        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        // Create UDP transport instances
362        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        // Create Ethernet transport instances where raw-socket support exists.
395        #[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        // Create TCP transport instances
419        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        // Create Tor transport instances
453        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        // Create BLE transport instances
530        #[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    /// Find an operational transport that matches the given transport type name.
579    ///
580    /// Adopted UDP bootstrap transports are point-to-point sockets handed off
581    /// from Nostr/STUN traversal. They must not be reused for ordinary
582    /// `udp host:port` dials discovered through static config, mDNS, or overlay
583    /// adverts: on macOS a `send_to` through the wrong adopted socket can fail
584    /// with `EINVAL`, and even on platforms that allow it the packet would use
585    /// the wrong 5-tuple/NAT mapping. Prefer configured transports and make the
586    /// choice deterministic by lowest transport id instead of HashMap order.
587    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    /// Resolve an Ethernet peer address ("interface/mac") to a transport ID
601    /// and binary TransportAddr.
602    ///
603    /// Finds the Ethernet transport instance bound to the named interface
604    /// and parses the MAC portion into a 6-byte TransportAddr.
605    #[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        // Find the Ethernet transport bound to this interface
618        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    /// Resolve a BLE address string (`"adapter/AA:BB:CC:DD:EE:FF"`) to a
652    /// (TransportId, TransportAddr) pair by finding the BLE transport
653    /// instance matching the adapter name.
654    #[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        // Find the BLE transport for this adapter
668        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        // Validate the address format
681        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}