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