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