Skip to main content

fips_core/node/lifecycle/
runtime.rs

1use super::*;
2
3impl Node {
4    // === State Transitions ===
5
6    /// Start the node.
7    ///
8    /// Initializes the TUN interface (if configured), spawns I/O threads,
9    /// and transitions to the Running state.
10    pub async fn start(&mut self) -> Result<(), NodeError> {
11        node_start_debug_log("Node::start begin");
12        if !self.state.can_start() {
13            return Err(NodeError::AlreadyStarted);
14        }
15        self.state = NodeState::Starting;
16        node_start_debug_log("Node::start state set to starting");
17
18        // Create packet channel for transport -> Node communication
19        let packet_buffer_size = self.config.node.buffers.packet_channel;
20        let (packet_tx, packet_rx) = packet_channel(packet_buffer_size);
21        self.packet_tx = Some(packet_tx.clone());
22        self.packet_rx = Some(packet_rx);
23        node_start_debug_log("Node::start packet channel created");
24
25        // Initialize transports first (before TUN, before Nostr discovery).
26        node_start_debug_log("Node::start create transports begin");
27        let transport_handles = self.create_transports(&packet_tx).await;
28        node_start_debug_log(format!(
29            "Node::start create transports complete count={}",
30            transport_handles.len()
31        ));
32
33        for mut handle in transport_handles {
34            let transport_id = handle.transport_id();
35            let transport_type = handle.transport_type().name;
36            let name = handle.name().map(|s| s.to_string());
37
38            node_start_debug_log(format!(
39                "Node::start transport start begin id={} type={} name={:?}",
40                transport_id, transport_type, name
41            ));
42            match handle.start().await {
43                Ok(()) => {
44                    node_start_debug_log(format!(
45                        "Node::start transport start ok id={} type={}",
46                        transport_id, transport_type
47                    ));
48                    self.transports.insert(transport_id, handle);
49                }
50                Err(e) => {
51                    node_start_debug_log(format!(
52                        "Node::start transport start error id={} type={} error={}",
53                        transport_id, transport_type, e
54                    ));
55                    if let Some(ref n) = name {
56                        warn!(transport_type, name = %n, error = %e, "Transport failed to start");
57                    } else {
58                        warn!(transport_type, error = %e, "Transport failed to start");
59                    }
60                }
61            }
62        }
63
64        if !self.transports.is_empty() {
65            info!(count = self.transports.len(), "Transports initialized");
66        }
67
68        // Spawn the off-task FMP-encrypt + UDP-send + FMP-decrypt
69        // worker pools. **Unix only** — both pools issue direct
70        // sendmmsg(2) / sendmsg(2)+UDP_GSO / recvmmsg(2) calls on
71        // raw file descriptors via `AsRawFd`, which is a unix-only
72        // trait. On Windows the rx_loop's tokio-based send/recv
73        // remain the canonical path; the perf overhaul lands its
74        // gains on unix.
75        //
76        // Worker count defaults to the number of CPUs, overridable
77        // via `FIPS_ENCRYPT_WORKERS=N` / `FIPS_DECRYPT_WORKERS=N`
78        // for debug / benchmarking. Hash-by-destination means a
79        // single TCP flow pins to one worker (preserves wire
80        // ordering); additional workers light up under multi-flow
81        // / multi-peer load. See `node::encrypt_worker` /
82        // `node::decrypt_worker` for full rationale.
83        #[cfg(unix)]
84        {
85            if self.config.node.worker_pools_enabled {
86                node_start_debug_log("Node::start worker pools begin");
87                let cpu_default = std::thread::available_parallelism()
88                    .map(|n| n.get())
89                    .unwrap_or(1)
90                    .max(1);
91                let encrypt_worker_count: usize = std::env::var("FIPS_ENCRYPT_WORKERS")
92                    .ok()
93                    .and_then(|s| s.parse().ok())
94                    .unwrap_or(cpu_default)
95                    .max(1);
96                self.encrypt_workers = Some(crate::node::encrypt_worker::EncryptWorkerPool::spawn(
97                    encrypt_worker_count,
98                ));
99                info!(
100                    workers = encrypt_worker_count,
101                    "Spawned FMP-encrypt worker pool"
102                );
103
104                // `FIPS_DECRYPT_WORKERS=0` disables the pool entirely and
105                // falls through to the in-line rx_loop decrypt path (the
106                // "test-mode" branch in `handle_encrypted_frame`, which is
107                // in fact a fully functional synchronous decrypt). Useful
108                // as an A/B against the worker pipeline when chasing
109                // scheduling/queueing regressions on the native macOS
110                // path. Any non-zero value (env or default) spawns the
111                // pool as before.
112                let decrypt_worker_count: usize = std::env::var("FIPS_DECRYPT_WORKERS")
113                    .ok()
114                    .and_then(|s| s.parse().ok())
115                    .unwrap_or(cpu_default);
116                if decrypt_worker_count == 0 {
117                    info!("FIPS_DECRYPT_WORKERS=0 → in-line decrypt in rx_loop (no worker pool)");
118                } else {
119                    let direct_delivery_sink = self.decrypt_direct_session_delivery_sink();
120                    self.decrypt_workers = Some(
121                        crate::node::decrypt_worker::DecryptWorkerPool::spawn_with_direct_delivery_sink(
122                            decrypt_worker_count,
123                            direct_delivery_sink,
124                        ),
125                    );
126                    info!(
127                        workers = decrypt_worker_count,
128                        "Spawned FMP+FSP-decrypt worker pool"
129                    );
130                }
131                node_start_debug_log("Node::start worker pools complete");
132            } else {
133                node_start_debug_log("Node::start worker pools disabled");
134                info!("FIPS worker pools disabled; using in-line crypto/send path");
135            }
136        }
137
138        if self.config.node.discovery.nostr.enabled {
139            node_start_debug_log("Node::start nostr discovery start begin");
140            match NostrDiscovery::start(&self.identity, self.config.node.discovery.nostr.clone())
141                .await
142            {
143                Ok(runtime) => {
144                    node_start_debug_log("Node::start nostr discovery runtime created");
145                    if let Err(err) = self.refresh_overlay_advert(&runtime).await {
146                        warn!(error = %err, "Failed to publish initial Nostr overlay advert");
147                    }
148                    node_start_debug_log("Node::start nostr overlay advert refreshed");
149                    self.nostr_discovery = Some(runtime);
150                    self.nostr_discovery_started_at_ms = Some(Self::now_ms());
151                    info!("Nostr overlay discovery enabled");
152                }
153                Err(err) => {
154                    node_start_debug_log(format!(
155                        "Node::start nostr discovery start error error={}",
156                        err
157                    ));
158                    warn!(error = %err, "Failed to start Nostr overlay discovery");
159                }
160            }
161        }
162
163        // mDNS / DNS-SD LAN discovery. Independent of Nostr — runs even
164        // when Nostr is disabled, since it gives us sub-second pairing
165        // on the same link without any relay or NAT-traversal roundtrip.
166        if self.config.node.discovery.lan.enabled {
167            node_start_debug_log("Node::start lan discovery start begin");
168            let advertised_udp_port = self
169                .transports
170                .values()
171                .filter(|h| h.is_operational())
172                .filter(|h| h.transport_type().name == "udp")
173                .find_map(|h| h.local_addr().map(|addr| addr.port()))
174                .unwrap_or(0);
175            let scope = self.lan_discovery_scope();
176            match crate::discovery::lan::LanDiscovery::start(
177                &self.identity,
178                scope,
179                advertised_udp_port,
180                self.config.node.discovery.lan.clone(),
181            )
182            .await
183            {
184                Ok(runtime) => {
185                    node_start_debug_log("Node::start lan discovery start ok");
186                    self.lan_discovery = Some(runtime);
187                    info!("LAN mDNS discovery enabled");
188                }
189                Err(err) => {
190                    node_start_debug_log(format!(
191                        "Node::start lan discovery start error error={}",
192                        err
193                    ));
194                    debug!(error = %err, "LAN mDNS discovery not started");
195                }
196            }
197        }
198
199        self.start_local_instance_discovery();
200        self.poll_local_instance_discovery().await;
201
202        // Connect to static peers before TUN is active
203        // This allows handshake messages to be sent before we start accepting packets
204        node_start_debug_log("Node::start initiate peer connections begin");
205        self.initiate_peer_connections().await;
206        node_start_debug_log("Node::start initiate peer connections complete");
207
208        // Initialize TUN interface last, after transports and peers are ready
209        if self.config.tun.enabled {
210            node_start_debug_log("Node::start tun init begin");
211            let address = *self.identity.address();
212            match TunDevice::create(&self.config.tun, address).await {
213                Ok(device) => {
214                    let mtu = device.mtu();
215                    let name = device.name().to_string();
216                    let our_addr = *device.address();
217
218                    info!("TUN device active:");
219                    info!("     name: {}", name);
220                    info!("  address: {}", device.address());
221                    info!("      mtu: {}", mtu);
222
223                    // Calculate max MSS for TCP clamping
224                    let effective_mtu = self.effective_ipv6_mtu();
225                    let max_mss = effective_mtu.saturating_sub(40).saturating_sub(20); // IPv6 + TCP headers
226
227                    info!("effective MTU: {} bytes", effective_mtu);
228                    debug!("   max TCP MSS: {} bytes", max_mss);
229
230                    // On macOS, create a shutdown pipe. Writing to it unblocks the
231                    // reader thread's select() loop without closing the TUN fd
232                    // (which would cause a double-close when TunDevice drops).
233                    #[cfg(target_os = "macos")]
234                    let (shutdown_read_fd, shutdown_write_fd) = {
235                        let mut fds = [0i32; 2];
236                        if unsafe { libc::pipe(fds.as_mut_ptr()) } < 0 {
237                            return Err(NodeError::Tun(crate::upper::tun::TunError::Configure(
238                                "failed to create shutdown pipe".into(),
239                            )));
240                        }
241                        (fds[0], fds[1])
242                    };
243
244                    // Create writer (dups the fd for independent write access).
245                    // Pass path_mtu_lookup so inbound SYN-ACK clamp can read
246                    // per-destination path MTU learned via discovery.
247                    let (writer, tun_tx) =
248                        device.create_writer(max_mss, self.path_mtu_lookup.clone())?;
249
250                    // Spawn writer thread
251                    let writer_handle = thread::spawn(move || {
252                        writer.run();
253                    });
254
255                    // Clone tun_tx for the reader
256                    let reader_tun_tx = tun_tx.clone();
257
258                    // Create outbound channel for TUN reader → Node
259                    let tun_channel_size = self.config.node.buffers.tun_channel;
260                    let (outbound_tx, outbound_rx) = tokio::sync::mpsc::channel(tun_channel_size);
261
262                    // Spawn reader thread
263                    let transport_mtu = self.transport_mtu();
264                    let path_mtu_lookup = self.path_mtu_lookup.clone();
265                    #[cfg(target_os = "macos")]
266                    let reader_handle = thread::spawn(move || {
267                        run_tun_reader(
268                            device,
269                            mtu,
270                            our_addr,
271                            reader_tun_tx,
272                            outbound_tx,
273                            transport_mtu,
274                            path_mtu_lookup,
275                            shutdown_read_fd,
276                        );
277                    });
278                    #[cfg(not(target_os = "macos"))]
279                    let reader_handle = thread::spawn(move || {
280                        run_tun_reader(
281                            device,
282                            mtu,
283                            our_addr,
284                            reader_tun_tx,
285                            outbound_tx,
286                            transport_mtu,
287                            path_mtu_lookup,
288                        );
289                    });
290
291                    self.tun_state = TunState::Active;
292                    self.tun_name = Some(name);
293                    self.tun_tx = Some(tun_tx);
294                    self.tun_outbound_rx = Some(outbound_rx);
295                    self.tun_reader_handle = Some(reader_handle);
296                    self.tun_writer_handle = Some(writer_handle);
297                    #[cfg(target_os = "macos")]
298                    {
299                        self.tun_shutdown_fd = Some(shutdown_write_fd);
300                    }
301                }
302                Err(e) => {
303                    self.tun_state = TunState::Failed;
304                    warn!(error = %e, "Failed to initialize TUN, continuing without it");
305                }
306            }
307            node_start_debug_log("Node::start tun init complete");
308        }
309
310        // Initialize DNS responder (independent of TUN).
311        //
312        // Default bind_addr is "::1" (IPv6 loopback). The shipped
313        // fips-dns-setup configures systemd-resolved via a global
314        // /etc/systemd/resolved.conf.d/fips.conf drop-in pointing at
315        // [::1]:5354, which sidesteps a Linux IPV6_PKTINFO behaviour
316        // where self-destined traffic to fips0's address is attributed
317        // to fips0 in PKTINFO and gets silently dropped by the
318        // mesh-interface filter in src/upper/dns.rs.
319        //
320        // For mesh-reachable resolution (rare), set bind_addr: "::"
321        // in fips.yaml. The mesh-interface filter remains active to
322        // prevent hosts-file alias enumeration in that mode.
323        // `IPV6_V6ONLY=0` is set explicitly so IPv4 clients on
324        // 127.0.0.1 still reach us regardless of kernel sysctl
325        // defaults — but only when bind is on a wildcard / IPv6 path.
326        if self.config.dns.enabled {
327            node_start_debug_log("Node::start dns init begin");
328            let addr_str = self.config.dns.bind_addr();
329            match addr_str.parse::<std::net::IpAddr>() {
330                Ok(ip) => {
331                    let bind = std::net::SocketAddr::new(ip, self.config.dns.port());
332                    match Self::bind_dns_socket(bind) {
333                        Ok(socket) => {
334                            let dns_channel_size = self.config.node.buffers.dns_channel;
335                            let (identity_tx, identity_rx) =
336                                tokio::sync::mpsc::channel(dns_channel_size);
337                            let dns_ttl = self.config.dns.ttl();
338                            let base_hosts = crate::upper::hosts::HostMap::from_peer_configs(
339                                self.config.peers(),
340                            );
341                            let reloader = if self.config.node.system_files_enabled {
342                                let hosts_path = std::path::PathBuf::from(
343                                    crate::upper::hosts::DEFAULT_HOSTS_PATH,
344                                );
345                                crate::upper::hosts::HostMapReloader::new(base_hosts, hosts_path)
346                            } else {
347                                crate::upper::hosts::HostMapReloader::memory_only(base_hosts)
348                            };
349                            // Resolve the TUN ifindex so the responder can
350                            // drop queries arriving on the mesh interface
351                            // (fips0). Without this, the `::` bind exposes
352                            // /etc/fips/hosts alias probing to any mesh peer.
353                            // When TUN isn't enabled or the name can't be
354                            // resolved, `None` disables the filter (there
355                            // is no mesh surface to defend anyway).
356                            let mesh_ifindex = Self::lookup_mesh_ifindex(self.config.tun.name());
357                            info!(
358                                bind = %bind,
359                                hosts = reloader.hosts().len(),
360                                mesh_ifindex = ?mesh_ifindex,
361                                "DNS responder started for .fips domain (auto-reload enabled)"
362                            );
363                            let handle = tokio::spawn(crate::upper::dns::run_dns_responder(
364                                socket,
365                                identity_tx,
366                                dns_ttl,
367                                reloader,
368                                mesh_ifindex,
369                            ));
370                            self.dns_identity_rx = Some(identity_rx);
371                            self.dns_task = Some(handle);
372                        }
373                        Err(e) => {
374                            warn!(bind = %bind, error = %e, "Failed to start DNS responder");
375                        }
376                    }
377                }
378                Err(e) => {
379                    warn!(addr = %addr_str, error = %e, "Invalid dns.bind_addr; DNS responder not started");
380                }
381            }
382            node_start_debug_log("Node::start dns init complete");
383        }
384
385        self.state = NodeState::Running;
386        node_start_debug_log("Node::start running");
387        info!("Node started:");
388        info!("       state: {}", self.state);
389        info!("  transports: {}", self.transports.len());
390        info!(" connections: {}", self.peers.connection_len());
391        Ok(())
392    }
393
394    /// Bind a UDP socket for the DNS responder.
395    ///
396    /// For IPv6 binds (including `::`), sets `IPV6_V6ONLY=0` so the socket
397    /// also accepts IPv4-mapped addresses. This guarantees dual-stack
398    /// delivery regardless of `net.ipv6.bindv6only` sysctl on the host —
399    /// v4 clients on 127.0.0.1 and v6 clients on the fips0 address both
400    /// land on the same socket.
401    ///
402    /// Also enables `IPV6_RECVPKTINFO` on IPv6 sockets so the responder
403    /// can learn the arrival interface per packet. The responder uses that
404    /// to drop queries arriving on the mesh TUN, closing the hosts-file
405    /// probing side-channel created by the `::` bind.
406    pub(super) fn bind_dns_socket(
407        addr: std::net::SocketAddr,
408    ) -> Result<tokio::net::UdpSocket, std::io::Error> {
409        use socket2::{Domain, Protocol, Socket, Type};
410        let domain = if addr.is_ipv4() {
411            Domain::IPV4
412        } else {
413            Domain::IPV6
414        };
415        let sock = Socket::new(domain, Type::DGRAM, Some(Protocol::UDP))?;
416        if addr.is_ipv6() {
417            sock.set_only_v6(false)?;
418            #[cfg(unix)]
419            Self::set_recv_pktinfo_v6(&sock)?;
420        }
421        sock.set_nonblocking(true)?;
422        sock.bind(&addr.into())?;
423        tokio::net::UdpSocket::from_std(sock.into())
424    }
425
426    /// Enable `IPV6_RECVPKTINFO` on an IPv6 UDP socket.
427    ///
428    /// After this setsockopt, each `recvmsg()` call on the socket receives
429    /// an `IPV6_PKTINFO` control message containing the arrival interface
430    /// index, which the DNS responder uses for its mesh-interface filter.
431    #[cfg(unix)]
432    pub(super) fn set_recv_pktinfo_v6(sock: &socket2::Socket) -> Result<(), std::io::Error> {
433        use std::os::fd::AsRawFd;
434        let enable: libc::c_int = 1;
435        let ret = unsafe {
436            libc::setsockopt(
437                sock.as_raw_fd(),
438                libc::IPPROTO_IPV6,
439                libc::IPV6_RECVPKTINFO,
440                &enable as *const _ as *const libc::c_void,
441                std::mem::size_of::<libc::c_int>() as libc::socklen_t,
442            )
443        };
444        if ret < 0 {
445            return Err(std::io::Error::last_os_error());
446        }
447        Ok(())
448    }
449
450    /// Resolve the mesh TUN interface index by name.
451    ///
452    /// Returns `None` if the interface does not exist (e.g. TUN disabled
453    /// or not yet created). A `None` result disables the DNS responder's
454    /// mesh-interface filter — safe, because if there is no fips0 there
455    /// is no mesh exposure to defend against.
456    pub(super) fn lookup_mesh_ifindex(name: &str) -> Option<u32> {
457        #[cfg(unix)]
458        {
459            let c_name = std::ffi::CString::new(name).ok()?;
460            let idx = unsafe { libc::if_nametoindex(c_name.as_ptr()) };
461            if idx == 0 { None } else { Some(idx) }
462        }
463        #[cfg(not(unix))]
464        {
465            let _ = name;
466            None
467        }
468    }
469
470    /// Stop the node.
471    ///
472    /// Shuts down TUN interface, stops I/O threads, and transitions to
473    /// the Stopped state.
474    pub async fn stop(&mut self) -> Result<(), NodeError> {
475        if !self.state.can_stop() {
476            return Err(NodeError::NotStarted);
477        }
478        self.state = NodeState::Stopping;
479        info!(state = %self.state, "Node stopping");
480
481        // Stop DNS responder
482        if let Some(handle) = self.dns_task.take() {
483            handle.abort();
484            debug!("DNS responder stopped");
485        }
486
487        // Send disconnect notifications to all active peers before closing transports
488        self.send_disconnect_to_all_peers(DisconnectReason::Shutdown)
489            .await;
490
491        // Stop Nostr overlay discovery background work and withdraw any advert.
492        if let Some(bootstrap) = self.nostr_discovery.take()
493            && let Err(e) = bootstrap.shutdown().await
494        {
495            warn!(error = %e, "Failed to shutdown Nostr overlay discovery");
496        }
497
498        // Tear down LAN mDNS responder + browser. Best-effort: the
499        // OS will eventually time the advert out via its TTL even if
500        // we don't get a clean unregister out before the daemon exits.
501        if let Some(lan) = self.lan_discovery.take() {
502            lan.shutdown().await;
503        }
504
505        if let Some(registry) = self.local_instance_registry.take()
506            && let Err(err) = registry.remove()
507        {
508            debug!(error = %err, "failed to remove same-host FIPS instance record");
509        }
510
511        // Shutdown transports (they're packet producers)
512        let transport_ids: Vec<_> = self.transports.keys().cloned().collect();
513        for transport_id in transport_ids {
514            if let Some(mut handle) = self.transports.remove(&transport_id) {
515                let transport_type = handle.transport_type().name;
516                match handle.stop().await {
517                    Ok(()) => {
518                        info!(transport_id = %transport_id, transport_type, "Transport stopped");
519                    }
520                    Err(e) => {
521                        warn!(
522                            transport_id = %transport_id,
523                            transport_type,
524                            error = %e,
525                            "Transport stop failed"
526                        );
527                    }
528                }
529            }
530        }
531
532        // Drop packet channels
533        self.packet_tx.take();
534        self.packet_rx.take();
535
536        // Shutdown TUN interface
537        if let Some(name) = self.tun_name.take() {
538            info!(name = %name, "Shutting down TUN interface");
539
540            // Drop the tun_tx to signal the writer to stop
541            self.tun_tx.take();
542
543            // Delete the interface (on Linux, causes reader to get EFAULT)
544            if let Err(e) = shutdown_tun_interface(&name).await {
545                warn!(name = %name, error = %e, "Failed to shutdown TUN interface");
546            }
547
548            // On macOS, signal the reader thread to exit by writing to the
549            // shutdown pipe. The reader's select() will wake up and break.
550            #[cfg(target_os = "macos")]
551            if let Some(fd) = self.tun_shutdown_fd.take() {
552                unsafe {
553                    libc::write(fd, b"x".as_ptr() as *const libc::c_void, 1);
554                    libc::close(fd);
555                }
556            }
557
558            // Wait for threads to finish
559            if let Some(handle) = self.tun_reader_handle.take() {
560                let _ = handle.join();
561            }
562            if let Some(handle) = self.tun_writer_handle.take() {
563                let _ = handle.join();
564            }
565
566            self.tun_state = TunState::Disabled;
567        }
568
569        self.state = NodeState::Stopped;
570        info!(state = %self.state, "Node stopped");
571        Ok(())
572    }
573
574    /// Send disconnect notifications to all active peers.
575    ///
576    /// Best-effort: send failures are logged and ignored since the transport
577    /// may already be degraded. This runs before transports are shut down.
578    pub(super) async fn send_disconnect_to_all_peers(&mut self, reason: DisconnectReason) {
579        // Collect node_addrs to avoid borrow conflict with send helper
580        let peer_addrs: Vec<NodeAddr> = self
581            .peers
582            .iter()
583            .filter(|(_, peer)| peer.can_send() && peer.has_session())
584            .map(|(addr, _)| *addr)
585            .collect();
586
587        if peer_addrs.is_empty() {
588            debug!(
589                total_peers = self.peers.len(),
590                "No sendable peers for disconnect notification"
591            );
592            return;
593        }
594
595        let mut sent = 0usize;
596        for node_addr in &peer_addrs {
597            if self.send_disconnect_to_peer(node_addr, reason).await {
598                sent += 1;
599            }
600        }
601
602        info!(sent, total = peer_addrs.len(), reason = %reason, "Sent disconnect notifications");
603    }
604
605    /// Send a Disconnect notification to one peer, swallowing transport failures.
606    pub(super) async fn send_disconnect_to_peer(
607        &mut self,
608        node_addr: &NodeAddr,
609        reason: DisconnectReason,
610    ) -> bool {
611        let plaintext = Disconnect::new(reason).encode();
612        match self
613            .send_encrypted_link_message(node_addr, &plaintext)
614            .await
615        {
616            Ok(()) => true,
617            Err(e) => {
618                debug!(
619                    peer = %self.peer_display_name(node_addr),
620                    error = %e,
621                    "Failed to send disconnect (transport may be down)"
622                );
623                false
624            }
625        }
626    }
627}