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