fips_core/node/lifecycle/
runtime.rs1use super::*;
2
3const SHUTDOWN_DISCONNECT_BUDGET: Duration = Duration::from_millis(100);
4const SHUTDOWN_FORWARDING_DRAIN_BUDGET: Duration = Duration::from_millis(100);
5
6async fn disconnect_notifications_within_shutdown_budget<F>(notifications: F) -> bool
7where
8 F: std::future::Future<Output = ()>,
9{
10 tokio::time::timeout(SHUTDOWN_DISCONNECT_BUDGET, notifications)
11 .await
12 .is_ok()
13}
14
15impl Node {
16 pub async fn start(&mut self) -> Result<(), NodeError> {
23 if !self.state.can_start() {
24 return Err(NodeError::AlreadyStarted);
25 }
26 self.state = NodeState::Starting;
27
28 let packet_buffer_size = self.config.node.buffers.packet_channel;
30 let (mut packet_tx, packet_rx) = packet_channel(packet_buffer_size);
31 self.dataplane_fast_ingress_rx = Some(
32 self.dataplane
33 .attach_established_fast_ingress(&mut packet_tx),
34 );
35 self.packet_tx = Some(packet_tx.clone());
36 self.packet_rx = Some(packet_rx);
37
38 let transport_handles = self.create_transports(&packet_tx).await;
40
41 for mut handle in transport_handles {
42 let transport_id = handle.transport_id();
43 let transport_type = handle.transport_type().name;
44 let name = handle.name().map(|s| s.to_string());
45
46 match handle.start().await {
47 Ok(()) => {
48 self.transports.insert(transport_id, handle);
49 }
50 Err(e) => {
51 if let Some(ref n) = name {
52 warn!(transport_type, name = %n, error = %e, "Transport failed to start");
53 } else {
54 warn!(transport_type, error = %e, "Transport failed to start");
55 }
56 }
57 }
58 }
59
60 if !self.transports.is_empty() {
61 info!(count = self.transports.len(), "Transports initialized");
62 }
63
64 if self.config.node.discovery.nostr.enabled {
65 match NostrDiscovery::start(&self.identity, self.config.node.discovery.nostr.clone())
66 .await
67 {
68 Ok(runtime) => {
69 if let Err(err) = self.refresh_overlay_advert(&runtime).await {
70 warn!(error = %err, "Failed to publish initial Nostr overlay advert");
71 }
72 self.nostr_discovery = Some(runtime);
73 self.nostr_discovery_started_at_ms = Some(Self::now_ms());
74 info!("Nostr overlay discovery enabled");
75 }
76 Err(err) => {
77 warn!(error = %err, "Failed to start Nostr overlay discovery");
78 }
79 }
80 }
81
82 if self.config.node.discovery.lan.enabled {
86 let advertised_udp_port = self
87 .transports
88 .values()
89 .filter(|h| h.is_operational())
90 .filter(|h| h.transport_type().name == "udp")
91 .find_map(|h| h.local_addr().map(|addr| addr.port()))
92 .unwrap_or(0);
93 let scope = self.lan_discovery_scope();
94 match crate::discovery::lan::LanDiscovery::start(
95 &self.identity,
96 scope,
97 advertised_udp_port,
98 self.config.node.discovery.lan.clone(),
99 )
100 .await
101 {
102 Ok(runtime) => {
103 self.lan_discovery = Some(runtime);
104 info!("LAN mDNS discovery enabled");
105 }
106 Err(err) => {
107 debug!(error = %err, "LAN mDNS discovery not started");
108 }
109 }
110 }
111
112 self.start_local_instance_discovery();
113 self.poll_local_instance_discovery().await;
114
115 self.initiate_peer_connections().await;
118
119 if self.config.tun.enabled {
121 let address = *self.identity.address();
122 let mut tun_config = self.config.tun.clone();
123 if tun_config.mtu.is_none() {
124 tun_config.mtu = Some(self.runtime_tun_mtu());
125 }
126 match TunDevice::create(&tun_config, address).await {
127 Ok(device) => {
128 let mtu = device.mtu();
129 let name = device.name().to_string();
130 let our_addr = *device.address();
131
132 info!("TUN device active:");
133 info!(" name: {}", name);
134 info!(" address: {}", device.address());
135 info!(" mtu: {}", mtu);
136
137 let effective_mtu = self.effective_ipv6_mtu();
139 let max_mss = effective_mtu.saturating_sub(40).saturating_sub(20); info!("effective MTU: {} bytes", effective_mtu);
142 debug!(" max TCP MSS: {} bytes", max_mss);
143
144 #[cfg(target_os = "macos")]
148 let (shutdown_read_fd, shutdown_write_fd) = {
149 let mut fds = [0i32; 2];
150 if unsafe { libc::pipe(fds.as_mut_ptr()) } < 0 {
151 return Err(NodeError::Tun(crate::upper::tun::TunError::Configure(
152 "failed to create shutdown pipe".into(),
153 )));
154 }
155 (fds[0], fds[1])
156 };
157
158 let (writer, tun_tx) =
162 device.create_writer(max_mss, self.path_mtu_lookup.clone())?;
163
164 let writer_handle = thread::spawn(move || {
166 writer.run();
167 });
168
169 let reader_tun_tx = tun_tx.clone();
171
172 let tun_channel_size = self.config.node.buffers.tun_channel;
174 let (outbound_tx, outbound_rx) =
175 crate::upper::tun::tun_outbound_channel(tun_channel_size);
176
177 let transport_mtu = self.transport_mtu();
179 let path_mtu_lookup = self.path_mtu_lookup.clone();
180 let reader_runtime = TunReaderRuntime {
181 device,
182 mtu,
183 our_addr,
184 tun_tx: reader_tun_tx,
185 outbound_tx,
186 transport_mtu,
187 path_mtu_lookup,
188 };
189 #[cfg(target_os = "macos")]
190 let reader_handle = thread::spawn(move || {
191 run_tun_reader(reader_runtime, shutdown_read_fd);
192 });
193 #[cfg(not(target_os = "macos"))]
194 let reader_handle = thread::spawn(move || {
195 run_tun_reader(reader_runtime);
196 });
197
198 self.tun_state = TunState::Active;
199 self.tun_name = Some(name);
200 self.tun_tx = Some(tun_tx);
201 self.tun_outbound_rx = Some(outbound_rx);
202 self.tun_reader_handle = Some(reader_handle);
203 self.tun_writer_handle = Some(writer_handle);
204 #[cfg(target_os = "macos")]
205 {
206 self.tun_shutdown_fd = Some(shutdown_write_fd);
207 }
208 }
209 Err(e) => {
210 self.tun_state = TunState::Failed;
211 warn!(error = %e, "Failed to initialize TUN, continuing without it");
212 }
213 }
214 }
215
216 if self.config.dns.enabled {
233 let addr_str = self.config.dns.bind_addr();
234 match addr_str.parse::<std::net::IpAddr>() {
235 Ok(ip) => {
236 let bind = std::net::SocketAddr::new(ip, self.config.dns.port());
237 match Self::bind_dns_socket(bind) {
238 Ok(socket) => {
239 let dns_channel_size = self.config.node.buffers.dns_channel;
240 let (identity_tx, identity_rx) =
241 tokio::sync::mpsc::channel(dns_channel_size);
242 let dns_ttl = self.config.dns.ttl();
243 let base_hosts = crate::upper::hosts::HostMap::from_peer_configs(
244 self.config.peers(),
245 );
246 let reloader = if self.config.node.system_files_enabled {
247 let hosts_path = std::path::PathBuf::from(
248 crate::upper::hosts::DEFAULT_HOSTS_PATH,
249 );
250 crate::upper::hosts::HostMapReloader::new(base_hosts, hosts_path)
251 } else {
252 crate::upper::hosts::HostMapReloader::memory_only(base_hosts)
253 };
254 let mesh_ifindex = Self::lookup_mesh_ifindex(self.config.tun.name());
262 info!(
263 bind = %bind,
264 hosts = reloader.hosts().len(),
265 mesh_ifindex = ?mesh_ifindex,
266 "DNS responder started for .fips domain (auto-reload enabled)"
267 );
268 let handle = tokio::spawn(crate::upper::dns::run_dns_responder(
269 socket,
270 identity_tx,
271 dns_ttl,
272 reloader,
273 mesh_ifindex,
274 ));
275 self.dns_identity_rx = Some(identity_rx);
276 self.dns_task = Some(handle);
277 }
278 Err(e) => {
279 warn!(bind = %bind, error = %e, "Failed to start DNS responder");
280 }
281 }
282 }
283 Err(e) => {
284 warn!(addr = %addr_str, error = %e, "Invalid dns.bind_addr; DNS responder not started");
285 }
286 }
287 }
288
289 self.state = NodeState::Running;
290 info!("Node started:");
291 info!(" state: {}", self.state);
292 info!(" transports: {}", self.transports.len());
293 info!(" connections: {}", self.peers.connection_len());
294 Ok(())
295 }
296
297 pub(super) fn bind_dns_socket(
310 addr: std::net::SocketAddr,
311 ) -> Result<tokio::net::UdpSocket, std::io::Error> {
312 use socket2::{Domain, Protocol, Socket, Type};
313 let domain = if addr.is_ipv4() {
314 Domain::IPV4
315 } else {
316 Domain::IPV6
317 };
318 let sock = Socket::new(domain, Type::DGRAM, Some(Protocol::UDP))?;
319 if addr.is_ipv6() {
320 sock.set_only_v6(false)?;
321 #[cfg(unix)]
322 Self::set_recv_pktinfo_v6(&sock)?;
323 }
324 sock.set_nonblocking(true)?;
325 sock.bind(&addr.into())?;
326 tokio::net::UdpSocket::from_std(sock.into())
327 }
328
329 #[cfg(unix)]
335 pub(super) fn set_recv_pktinfo_v6(sock: &socket2::Socket) -> Result<(), std::io::Error> {
336 use std::os::fd::AsRawFd;
337 let enable: libc::c_int = 1;
338 let ret = unsafe {
339 libc::setsockopt(
340 sock.as_raw_fd(),
341 libc::IPPROTO_IPV6,
342 libc::IPV6_RECVPKTINFO,
343 &enable as *const _ as *const libc::c_void,
344 std::mem::size_of::<libc::c_int>() as libc::socklen_t,
345 )
346 };
347 if ret < 0 {
348 return Err(std::io::Error::last_os_error());
349 }
350 Ok(())
351 }
352
353 pub(super) fn lookup_mesh_ifindex(name: &str) -> Option<u32> {
360 #[cfg(unix)]
361 {
362 let c_name = std::ffi::CString::new(name).ok()?;
363 let idx = unsafe { libc::if_nametoindex(c_name.as_ptr()) };
364 if idx == 0 { None } else { Some(idx) }
365 }
366 #[cfg(not(unix))]
367 {
368 let _ = name;
369 None
370 }
371 }
372
373 pub async fn stop(&mut self) -> Result<(), NodeError> {
378 if !self.state.can_stop() {
379 return Err(NodeError::NotStarted);
380 }
381 self.state = NodeState::Stopping;
382 info!(state = %self.state, "Node stopping");
383
384 if tokio::time::timeout(
385 SHUTDOWN_FORWARDING_DRAIN_BUDGET,
386 self.drain_deferred_session_forwards(),
387 )
388 .await
389 .is_err()
390 {
391 let aborted = self
392 .abort_deferred_session_forwards("node stopped before forwarding receipt")
393 .await;
394 warn!(aborted, "Forwarding drain budget expired during shutdown");
395 }
396
397 if let Some(handle) = self.dns_task.take() {
399 handle.abort();
400 debug!("DNS responder stopped");
401 }
402
403 if !disconnect_notifications_within_shutdown_budget(
405 self.send_disconnect_to_all_peers(DisconnectReason::Shutdown),
406 )
407 .await
408 {
409 warn!(
410 budget_ms = SHUTDOWN_DISCONNECT_BUDGET.as_millis(),
411 "Disconnect notification budget expired; continuing shutdown"
412 );
413 }
414
415 if let Some(bootstrap) = self.nostr_discovery.take()
417 && let Err(e) = bootstrap.shutdown().await
418 {
419 warn!(error = %e, "Failed to shutdown Nostr overlay discovery");
420 }
421
422 if let Some(lan) = self.lan_discovery.take() {
426 lan.shutdown().await;
427 }
428
429 if let Some(registry) = self.local_instance_registry.take()
430 && let Err(err) = registry.remove()
431 {
432 debug!(error = %err, "failed to remove same-host FIPS instance record");
433 }
434
435 let transport_ids: Vec<_> = self.transports.keys().cloned().collect();
437 for transport_id in transport_ids {
438 if let Some(mut handle) = self.transports.remove(&transport_id) {
439 let transport_type = handle.transport_type().name;
440 match handle.stop().await {
441 Ok(()) => {
442 info!(transport_id = %transport_id, transport_type, "Transport stopped");
443 }
444 Err(e) => {
445 warn!(
446 transport_id = %transport_id,
447 transport_type,
448 error = %e,
449 "Transport stop failed"
450 );
451 }
452 }
453 }
454 }
455
456 self.packet_tx.take();
458 self.packet_rx.take();
459
460 if let Some(name) = self.tun_name.take() {
462 info!(name = %name, "Shutting down TUN interface");
463
464 self.tun_tx.take();
466
467 if let Err(e) = shutdown_tun_interface(&name).await {
469 warn!(name = %name, error = %e, "Failed to shutdown TUN interface");
470 }
471
472 #[cfg(target_os = "macos")]
475 if let Some(fd) = self.tun_shutdown_fd.take() {
476 unsafe {
477 libc::write(fd, b"x".as_ptr() as *const libc::c_void, 1);
478 libc::close(fd);
479 }
480 }
481
482 if let Some(handle) = self.tun_reader_handle.take() {
484 let _ = handle.join();
485 }
486 if let Some(handle) = self.tun_writer_handle.take() {
487 let _ = handle.join();
488 }
489
490 self.tun_state = TunState::Disabled;
491 }
492
493 self.state = NodeState::Stopped;
494 info!(state = %self.state, "Node stopped");
495 Ok(())
496 }
497
498 pub(super) async fn send_disconnect_to_all_peers(&mut self, reason: DisconnectReason) {
503 let peer_addrs: Vec<NodeAddr> = self
505 .peers
506 .iter()
507 .filter(|(_, peer)| peer.can_send() && peer.has_session())
508 .map(|(addr, _)| *addr)
509 .collect();
510
511 if peer_addrs.is_empty() {
512 debug!(
513 total_peers = self.peers.len(),
514 "No sendable peers for disconnect notification"
515 );
516 return;
517 }
518
519 let mut sent = 0usize;
520 for node_addr in &peer_addrs {
521 if self.send_disconnect_to_peer(node_addr, reason).await {
522 sent += 1;
523 }
524 }
525
526 info!(sent, total = peer_addrs.len(), reason = %reason, "Sent disconnect notifications");
527 }
528
529 pub(super) async fn send_disconnect_to_peer(
531 &mut self,
532 node_addr: &NodeAddr,
533 reason: DisconnectReason,
534 ) -> bool {
535 let plaintext = Disconnect::new(reason).encode();
536 match self
537 .send_dataplane_fmp_link_plaintext(node_addr, &plaintext, false)
538 .await
539 {
540 Ok(()) => true,
541 Err(e) => {
542 debug!(
543 peer = %self.peer_display_name(node_addr),
544 error = %e,
545 "Failed to send disconnect (transport may be down)"
546 );
547 false
548 }
549 }
550 }
551}
552
553#[cfg(test)]
554mod deadline_tests {
555 use super::*;
556
557 #[tokio::test(start_paused = true)]
558 async fn disconnect_notifications_share_one_shutdown_budget() {
559 let started = tokio::time::Instant::now();
560 let completed =
561 disconnect_notifications_within_shutdown_budget(std::future::pending::<()>()).await;
562
563 assert!(!completed);
564 assert_eq!(started.elapsed(), SHUTDOWN_DISCONNECT_BUDGET);
565 }
566}