Skip to main content

fips_core/
endpoint.rs

1//! Library-first endpoint API for embedding FIPS in applications.
2//!
3//! This module exposes a no-system-TUN runtime shape for apps that want to own
4//! peer admission and local routing policy while reusing FIPS connectivity.
5
6use crate::config::{NostrDiscoveryPolicy, TransportInstances, UdpConfig};
7#[cfg(test)]
8use crate::node::ENDPOINT_EVENT_TEST_PAYLOAD_LEN;
9use crate::node::{
10    EndpointDataBatchTx, EndpointDataPayload, EndpointDirectSink, EndpointEventSender,
11    EndpointServiceEventSender, NodeEndpointControlCommand, NodeEndpointDataBatch,
12    NodeEndpointEvent,
13};
14use crate::upper::tun::TunOutboundTx;
15use crate::{
16    Config, FipsAddress, IdentityConfig, Node, NodeAddr, NodeDeliveredPacket, NodeError,
17    PeerIdentity,
18};
19use std::collections::HashMap;
20use std::sync::{Arc, Mutex as StdMutex};
21use std::time::Duration;
22use thiserror::Error;
23use tokio::sync::{Mutex, mpsc, oneshot};
24use tokio::task::JoinHandle;
25
26const ENDPOINT_DATA_BATCH_MAX: usize = 128;
27const ENDPOINT_RECV_BATCH_MAX: usize = 128;
28const ENDPOINT_OPERATION_TIMEOUT: Duration = Duration::from_secs(5);
29
30mod builder;
31#[path = "endpoint/nostr.rs"]
32mod nostr_api;
33mod receive;
34mod service_receiver;
35mod status;
36
37#[cfg(test)]
38mod tests;
39
40pub use crate::node::{
41    FIPS_ENDPOINT_DIRECT_PACKET_QUEUE_MAX_PACKETS, FIPS_ENDPOINT_DIRECT_PACKET_RUN_MAX_PACKETS,
42    FipsEndpointDirectDeliveryError, FipsEndpointDirectPacketBatch, FipsEndpointDirectPacketRun,
43    FipsEndpointDirectReceiver, FipsEndpointDirectSink,
44};
45pub use builder::FipsEndpointBuilder;
46use receive::{EndpointReceiveState, ServiceReceiveState};
47pub use status::{FipsEndpointPeer, FipsEndpointRelayStatus};
48
49/// Endpoint data bytes delivered by FIPS.
50///
51/// This is the same pooled packet owner used by the transport/dataplane, so
52/// embedders can forward endpoint data without forcing another hot-path copy.
53pub type FipsEndpointData = crate::transport::PacketBuffer;
54
55/// Errors returned by the endpoint API.
56#[derive(Debug, Error)]
57pub enum FipsEndpointError {
58    #[error("node error: {0}")]
59    Node(#[from] NodeError),
60
61    #[error("endpoint task failed: {0}")]
62    TaskJoin(#[from] tokio::task::JoinError),
63
64    #[error("endpoint is closed")]
65    Closed,
66
67    #[error("endpoint {operation} timed out")]
68    Timeout { operation: &'static str },
69
70    #[error("endpoint data payload is too large: {len} bytes exceeds max {max} bytes")]
71    EndpointDataTooLarge { len: usize, max: usize },
72
73    #[error("service datagram payload is too large: {len} bytes exceeds max {max} bytes")]
74    ServiceDatagramTooLarge { len: usize, max: usize },
75
76    #[error("FSP service port {port} is reserved")]
77    ServicePortReserved { port: u16 },
78
79    #[error("FSP service port {port} is already registered")]
80    ServicePortAlreadyRegistered { port: u16 },
81
82    #[cfg(feature = "host-ble-transport")]
83    #[error("host BLE adapter was already consumed by another endpoint bind")]
84    HostBleAdapterConsumed,
85}
86
87/// Errors specific to capability-advertised local service registration.
88#[derive(Debug, Error)]
89pub enum LocalServiceRegistrationError {
90    #[error("local FSP service capability must include a port")]
91    ServiceCapabilityMissingPort,
92
93    #[error("local FSP service capability name must not be empty")]
94    ServiceCapabilityNameEmpty,
95
96    #[error("local FSP service capability name exceeds {max} bytes")]
97    ServiceCapabilityNameTooLong { max: usize },
98
99    #[error(transparent)]
100    Endpoint(#[from] FipsEndpointError),
101}
102
103/// Source-attributed endpoint data delivered to an embedded application.
104#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct FipsEndpointMessage {
106    /// Authenticated FIPS peer that originated the endpoint data.
107    pub source_peer: PeerIdentity,
108    /// Application-owned payload bytes.
109    pub data: FipsEndpointData,
110    /// Unix-millisecond time when FIPS queued this message for the embedder.
111    pub enqueued_at_ms: u64,
112}
113
114/// One owned outbound FSP DataPacket service payload.
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct FipsEndpointOutboundDatagram {
117    pub source_port: u16,
118    pub destination_port: u16,
119    pub data: Vec<u8>,
120}
121
122impl FipsEndpointOutboundDatagram {
123    pub fn new(source_port: u16, destination_port: u16, data: Vec<u8>) -> Self {
124        Self {
125            source_port,
126            destination_port,
127            data,
128        }
129    }
130}
131
132/// Authenticated FSP DataPacket service payload delivered to an embedder.
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct FipsEndpointServiceDatagram {
135    pub source_peer: PeerIdentity,
136    pub source_port: u16,
137    pub destination_port: u16,
138    pub data: FipsEndpointData,
139    pub enqueued_at_ms: u64,
140}
141
142/// Port-scoped receiver for one registered FSP service.
143///
144/// Unlike [`FipsEndpoint::recv_service_datagram_batch_into`], this receiver
145/// cannot consume datagrams registered by another service owner.
146pub struct FipsEndpointServiceReceiver {
147    state: Mutex<ServiceReceiveState>,
148}
149
150/// Reports what changed in response to [`FipsEndpoint::update_peers`].
151#[derive(Debug, Clone, Default, PartialEq, Eq)]
152pub struct UpdatePeersOutcome {
153    /// Number of npubs that were not previously in the runtime peer list
154    /// and got an `initiate_peer_connection` call.
155    pub added: usize,
156    /// Number of npubs that were dropped from the runtime peer list. Their
157    /// retry entries are gone; any active session stays up until the
158    /// regular liveness timeout reaps it.
159    pub removed: usize,
160    /// Number of npubs that were already in the list but had a different
161    /// `addresses`, `alias`, `connect_policy`, or `auto_reconnect` value.
162    /// The new values are now in effect for retries and aliasing; refreshed
163    /// direct addresses may also trigger a new direct dial for auto peers.
164    pub updated: usize,
165    /// Number of npubs that were in the list and identical to the new entry.
166    pub unchanged: usize,
167}
168
169impl From<crate::node::UpdatePeersOutcome> for UpdatePeersOutcome {
170    fn from(value: crate::node::UpdatePeersOutcome) -> Self {
171        Self {
172            added: value.added,
173            removed: value.removed,
174            updated: value.updated,
175            unchanged: value.unchanged,
176        }
177    }
178}
179
180fn apply_default_scoped_discovery(config: &mut Config, scope: &str) {
181    if config.node.discovery.nostr.enabled || !config.transports.is_empty() {
182        return;
183    }
184
185    config.node.discovery.nostr.enabled = true;
186    config.node.discovery.nostr.advertise = true;
187    config.node.discovery.nostr.policy = NostrDiscoveryPolicy::Open;
188    config.node.discovery.nostr.share_local_candidates = true;
189    config.node.discovery.nostr.app = scope.to_string();
190    config.node.discovery.lan.scope = Some(scope.to_string());
191    config.node.discovery.local.enabled = true;
192    config.transports.udp = TransportInstances::Single(UdpConfig {
193        bind_addr: Some("0.0.0.0:0".to_string()),
194        advertise_on_nostr: Some(true),
195        public: Some(false),
196        outbound_only: Some(false),
197        accept_connections: Some(true),
198        ..UdpConfig::default()
199    });
200}
201
202fn endpoint_data_payloads_from_vecs(
203    payloads: Vec<Vec<u8>>,
204) -> Result<Vec<EndpointDataPayload>, FipsEndpointError> {
205    let mut converted = Vec::with_capacity(payloads.len());
206    for payload in payloads {
207        let len = payload.len();
208        let Some(payload) = EndpointDataPayload::from_packet_payload(payload) else {
209            let max = crate::node::session_wire::fsp_endpoint_data_max_body_len();
210            return Err(FipsEndpointError::EndpointDataTooLarge { len, max });
211        };
212        converted.push(payload);
213    }
214    Ok(converted)
215}
216
217fn service_datagram_payloads(
218    datagrams: Vec<FipsEndpointOutboundDatagram>,
219) -> Result<Vec<EndpointDataPayload>, FipsEndpointError> {
220    let max = crate::node::session_wire::fsp_service_datagram_max_body_len();
221    let mut payloads = Vec::with_capacity(datagrams.len());
222    for datagram in datagrams {
223        let len = datagram.data.len();
224        let Some(payload) = EndpointDataPayload::from_service_datagram(
225            datagram.source_port,
226            datagram.destination_port,
227            datagram.data,
228        ) else {
229            return Err(FipsEndpointError::ServiceDatagramTooLarge { len, max });
230        };
231        payloads.push(payload);
232    }
233    Ok(payloads)
234}
235
236fn spawn_node_task(
237    mut node: Node,
238    shutdown_rx: oneshot::Receiver<()>,
239) -> JoinHandle<Result<(), NodeError>> {
240    tokio::spawn(async move {
241        tokio::pin!(shutdown_rx);
242        let loop_result = tokio::select! {
243            result = node.run_rx_loop() => result,
244            _ = &mut shutdown_rx => Ok(()),
245        };
246        let stop_result = if node.state().can_stop() {
247            node.stop().await
248        } else {
249            Ok(())
250        };
251        loop_result?;
252        stop_result
253    })
254}
255
256/// A running embedded FIPS endpoint.
257pub struct FipsEndpoint {
258    identity: PeerIdentity,
259    npub: String,
260    node_addr: NodeAddr,
261    address: FipsAddress,
262    discovery_scope: Option<String>,
263    local_capability_directory: crate::discovery::local::LocalCapabilityDirectory,
264    outbound_packets: TunOutboundTx,
265    delivered_packets: Arc<Mutex<mpsc::Receiver<NodeDeliveredPacket>>>,
266    endpoint_control_tx: mpsc::Sender<NodeEndpointControlCommand>,
267    endpoint_data_batches: EndpointDataBatchTx,
268    /// In-process loopback sender for local peer sends. It injects an event into
269    /// the same queue without going through the wire/encrypt path. The node's
270    /// rx_loop also sends into this channel directly (it holds a clone of this
271    /// sender) so there is no per-packet relay task between the node task and
272    /// `recv_batch_into()`.
273    inbound_endpoint_tx: EndpointEventSender,
274    /// Unbounded receiver plus pending tail from an internal batch. This was
275    /// previously fed by a per-packet relay task
276    /// that translated node endpoint events into `FipsEndpointMessage`
277    /// across an additional bounded mpsc; collapsed into a single channel
278    /// -- the translation happens inline in `recv()` and the second hop
279    /// (with its scheduler wake per packet) is gone.
280    inbound_endpoint_rx: Arc<Mutex<EndpointReceiveState>>,
281    inbound_service_tx: EndpointServiceEventSender,
282    inbound_service_rx: Arc<Mutex<ServiceReceiveState>>,
283    registered_services: Arc<StdMutex<HashMap<u16, EndpointServiceEventSender>>>,
284    service_channel_capacity: usize,
285    shutdown_tx: StdMutex<Option<oneshot::Sender<()>>>,
286    task: StdMutex<Option<JoinHandle<Result<(), NodeError>>>>,
287}
288
289impl FipsEndpoint {
290    /// Create a builder for an embedded endpoint.
291    pub fn builder() -> FipsEndpointBuilder {
292        FipsEndpointBuilder::default()
293    }
294
295    async fn control<T>(
296        &self,
297        operation: &'static str,
298        command: NodeEndpointControlCommand,
299        response_rx: oneshot::Receiver<T>,
300    ) -> Result<T, FipsEndpointError> {
301        tokio::time::timeout(ENDPOINT_OPERATION_TIMEOUT, async {
302            self.endpoint_control_tx
303                .send(command)
304                .await
305                .map_err(|_| FipsEndpointError::Closed)?;
306            response_rx.await.map_err(|_| FipsEndpointError::Closed)
307        })
308        .await
309        .map_err(|_| FipsEndpointError::Timeout { operation })?
310    }
311
312    /// Local endpoint npub.
313    pub fn npub(&self) -> &str {
314        &self.npub
315    }
316
317    /// Local FIPS node address.
318    pub fn node_addr(&self) -> &NodeAddr {
319        &self.node_addr
320    }
321
322    /// Local FIPS IPv6-compatible address.
323    pub fn address(&self) -> FipsAddress {
324        self.address
325    }
326
327    /// Application-level discovery scope, if configured.
328    pub fn discovery_scope(&self) -> Option<&str> {
329        self.discovery_scope.as_deref()
330    }
331
332    /// Snapshot reusable services and roles advertised by live same-host FIPS
333    /// instances, including this endpoint. Records are routing hints; callers
334    /// must still authenticate the peer and validate the selected protocol.
335    pub fn local_instance_advertisements(
336        &self,
337    ) -> Result<
338        Vec<crate::discovery::local::LocalInstanceAdvertisement>,
339        crate::discovery::local::LocalInstanceRegistryError,
340    > {
341        Ok(self.local_capability_directory.snapshot())
342    }
343
344    /// Send application-owned endpoint payloads to one resolved peer.
345    ///
346    /// This is the canonical endpoint-data send path for applications that
347    /// already validate and cache peer identities in their own routing table.
348    /// It avoids per-packet npub allocation, endpoint cache lookup, and
349    /// `PeerIdentity::from_npub` parsing while preserving owned-payload
350    /// semantics.
351    pub async fn send_batch_to_peer(
352        &self,
353        remote: PeerIdentity,
354        payloads: Vec<Vec<u8>>,
355    ) -> Result<(), FipsEndpointError> {
356        self.send_payloads_to_peer(remote, payloads)
357    }
358
359    /// Register one local FSP DataPacket destination port.
360    ///
361    /// Ports 256-258 remain reserved for built-in FIPS services. Datagrams for
362    /// unregistered ports are discarded by the authenticated receive path.
363    pub async fn register_service(&self, port: u16) -> Result<(), FipsEndpointError> {
364        self.register_service_with_sender(port, self.inbound_service_tx.clone(), None)
365            .await
366    }
367
368    /// Register one local FSP service port with an isolated receiver.
369    pub async fn register_service_receiver(
370        &self,
371        port: u16,
372    ) -> Result<FipsEndpointServiceReceiver, FipsEndpointError> {
373        let (sender, receiver) = EndpointServiceEventSender::channel(self.service_channel_capacity);
374        self.register_service_with_sender(port, sender, None)
375            .await?;
376        Ok(FipsEndpointServiceReceiver {
377            state: Mutex::new(ServiceReceiveState::new(receiver)),
378        })
379    }
380
381    /// Register and advertise one reusable same-host FSP service. The
382    /// capability becomes visible only after the port registration succeeds.
383    pub async fn register_service_receiver_with_capability(
384        &self,
385        mut capability: crate::discovery::local::LocalInstanceCapability,
386    ) -> Result<FipsEndpointServiceReceiver, LocalServiceRegistrationError> {
387        let Some(port) = capability.fsp_port else {
388            return Err(LocalServiceRegistrationError::ServiceCapabilityMissingPort);
389        };
390        if capability.name.trim().is_empty() {
391            return Err(LocalServiceRegistrationError::ServiceCapabilityNameEmpty);
392        }
393        capability.name = capability.name.trim().to_string();
394        if !crate::discovery::local_udp::local_capability_name_is_valid(&capability.name) {
395            return Err(
396                LocalServiceRegistrationError::ServiceCapabilityNameTooLong {
397                    max: crate::discovery::local_udp::LOCAL_CAPABILITY_MAX_NAME_BYTES,
398                },
399            );
400        }
401        let (sender, receiver) = EndpointServiceEventSender::channel(self.service_channel_capacity);
402        self.register_service_with_sender(port, sender, Some(capability))
403            .await?;
404        Ok(FipsEndpointServiceReceiver {
405            state: Mutex::new(ServiceReceiveState::new(receiver)),
406        })
407    }
408
409    async fn register_service_with_sender(
410        &self,
411        port: u16,
412        sender: EndpointServiceEventSender,
413        capability: Option<crate::discovery::local::LocalInstanceCapability>,
414    ) -> Result<(), FipsEndpointError> {
415        if port == crate::node::session_wire::FSP_PORT_IPV6_SHIM
416            || port == crate::transport::link_negotiation::LINK_NEGOTIATION_SERVICE_PORT
417            || port == crate::discovery::local_udp::LOCAL_CAPABILITY_FSP_PORT
418        {
419            return Err(FipsEndpointError::ServicePortReserved { port });
420        }
421
422        let (response_tx, response_rx) = oneshot::channel();
423        if !self
424            .control(
425                "service registration",
426                NodeEndpointControlCommand::RegisterService {
427                    port,
428                    sender: sender.clone(),
429                    capability,
430                    response_tx,
431                },
432                response_rx,
433            )
434            .await?
435        {
436            return Err(FipsEndpointError::ServicePortAlreadyRegistered { port });
437        }
438        self.registered_services
439            .lock()
440            .map_err(|_| FipsEndpointError::Closed)?
441            .insert(port, sender);
442        Ok(())
443    }
444
445    /// Send one owned FSP DataPacket service payload to a resolved peer.
446    pub async fn send_datagram(
447        &self,
448        remote: PeerIdentity,
449        source_port: u16,
450        destination_port: u16,
451        payload: Vec<u8>,
452    ) -> Result<(), FipsEndpointError> {
453        self.send_service_datagrams_to_peer(
454            remote,
455            vec![FipsEndpointOutboundDatagram::new(
456                source_port,
457                destination_port,
458                payload,
459            )],
460        )
461    }
462
463    /// Send a caller-owned batch of FSP DataPacket service payloads to one peer.
464    pub async fn send_datagram_batch_to_peer(
465        &self,
466        remote: PeerIdentity,
467        datagrams: Vec<FipsEndpointOutboundDatagram>,
468    ) -> Result<(), FipsEndpointError> {
469        self.send_service_datagrams_to_peer(remote, datagrams)
470    }
471
472    fn send_service_datagrams_to_peer(
473        &self,
474        remote: PeerIdentity,
475        datagrams: Vec<FipsEndpointOutboundDatagram>,
476    ) -> Result<(), FipsEndpointError> {
477        let max = crate::node::session_wire::fsp_service_datagram_max_body_len();
478        if let Some(datagram) = datagrams.iter().find(|datagram| datagram.data.len() > max) {
479            return Err(FipsEndpointError::ServiceDatagramTooLarge {
480                len: datagram.data.len(),
481                max,
482            });
483        }
484        if datagrams.is_empty() {
485            return Ok(());
486        }
487
488        if *remote.node_addr() == self.node_addr {
489            let deliveries_by_port = {
490                let mut registered = self
491                    .registered_services
492                    .lock()
493                    .map_err(|_| FipsEndpointError::Closed)?;
494                registered.retain(|_, sender| !sender.is_closed());
495                let mut grouped: HashMap<
496                    u16,
497                    (
498                        EndpointServiceEventSender,
499                        Vec<crate::node::EndpointServiceDatagramDelivery>,
500                    ),
501                > = HashMap::new();
502                for datagram in datagrams {
503                    let Some(sender) = registered.get(&datagram.destination_port) else {
504                        continue;
505                    };
506                    grouped
507                        .entry(datagram.destination_port)
508                        .or_insert_with(|| (sender.clone(), Vec::new()))
509                        .1
510                        .push(crate::node::EndpointServiceDatagramDelivery::new(
511                            self.identity,
512                            datagram.source_port,
513                            datagram.destination_port,
514                            crate::transport::PacketBuffer::new(datagram.data),
515                        ));
516                }
517                grouped
518            };
519            for (_, (sender, deliveries)) in deliveries_by_port {
520                sender
521                    .send(deliveries)
522                    .map_err(|_| FipsEndpointError::Closed)?;
523            }
524            return Ok(());
525        }
526
527        self.send_endpoint_data_batch(remote, service_datagram_payloads(datagrams)?)
528    }
529
530    fn send_payloads_to_peer(
531        &self,
532        remote: PeerIdentity,
533        payloads: Vec<Vec<u8>>,
534    ) -> Result<(), FipsEndpointError> {
535        let payloads = endpoint_data_payloads_from_vecs(payloads)?;
536        if *remote.node_addr() == self.node_addr {
537            for payload in payloads {
538                self.send_loopback(payload)?;
539            }
540            return Ok(());
541        }
542
543        self.send_endpoint_data_batch(remote, payloads)
544    }
545
546    fn send_endpoint_data_batch(
547        &self,
548        remote: PeerIdentity,
549        payloads: Vec<EndpointDataPayload>,
550    ) -> Result<(), FipsEndpointError> {
551        if payloads.is_empty() {
552            return Ok(());
553        }
554
555        if payloads.len() <= ENDPOINT_DATA_BATCH_MAX {
556            self.enqueue_endpoint_data_batch(remote, payloads)?;
557            return Ok(());
558        }
559
560        let mut payloads = payloads.into_iter();
561        loop {
562            let payload_batch: Vec<_> = payloads.by_ref().take(ENDPOINT_DATA_BATCH_MAX).collect();
563            if payload_batch.is_empty() {
564                break;
565            }
566            self.enqueue_endpoint_data_batch(remote, payload_batch)?;
567        }
568        Ok(())
569    }
570
571    fn enqueue_endpoint_data_batch(
572        &self,
573        remote: PeerIdentity,
574        payload_batch: Vec<EndpointDataPayload>,
575    ) -> Result<(), FipsEndpointError> {
576        // Fire-and-forget: caller already drops the result, so skip
577        // the per-packet `oneshot::channel()` allocation entirely.
578        // Endpoint data now enters the dataplane bulk lane directly, without a
579        // per-packet oneshot or control-command hop.
580        if let Some(batch) = NodeEndpointDataBatch::from_payloads(
581            remote,
582            payload_batch,
583            crate::perf_profile::stamp(),
584        ) {
585            self.endpoint_data_batches
586                .send_or_drop(batch)
587                .map_err(|_| FipsEndpointError::Closed)?;
588        }
589        Ok(())
590    }
591
592    fn send_loopback(&self, payload: EndpointDataPayload) -> Result<(), FipsEndpointError> {
593        self.inbound_endpoint_tx
594            .send(NodeEndpointEvent {
595                messages: vec![crate::node::EndpointDataDelivery::new(
596                    self.identity,
597                    payload.into_body(),
598                )],
599                queued_at: crate::perf_profile::stamp(),
600            })
601            .map_err(|_| FipsEndpointError::Closed)
602    }
603
604    /// Receive one endpoint message, then drain ready follow-ons into a caller-owned buffer.
605    ///
606    /// This is the receive-side counterpart to [`Self::send_batch_to_peer`]:
607    /// callers still get individual source-attributed messages, but a hot
608    /// dataplane consumer can amortize the endpoint receiver lock, task wake,
609    /// and message buffer allocation across a bounded burst.
610    pub async fn recv_batch_into(
611        &self,
612        messages: &mut Vec<FipsEndpointMessage>,
613        max: usize,
614    ) -> Option<usize> {
615        let max = max.clamp(1, ENDPOINT_RECV_BATCH_MAX);
616        messages.clear();
617
618        let mut state = self.inbound_endpoint_rx.lock().await;
619        state.drain_pending_into(messages, max);
620
621        while messages.len() < max {
622            let event = if messages.is_empty() {
623                state.rx.recv().await?
624            } else {
625                match state.rx.try_recv() {
626                    Ok(event) => event,
627                    Err(_) => break,
628                }
629            };
630            state.push_event_into(event, messages, max);
631        }
632
633        Some(messages.len())
634    }
635
636    /// Receive one registered service datagram and drain ready follow-ons.
637    pub async fn recv_service_datagram_batch_into(
638        &self,
639        datagrams: &mut Vec<FipsEndpointServiceDatagram>,
640        max: usize,
641    ) -> Option<usize> {
642        let max = max.clamp(1, ENDPOINT_RECV_BATCH_MAX);
643        datagrams.clear();
644
645        let mut state = self.inbound_service_rx.lock().await;
646        state.drain_pending_into(datagrams, max);
647        while datagrams.len() < max {
648            let event = if datagrams.is_empty() {
649                state.rx.recv().await?
650            } else {
651                match state.rx.try_recv() {
652                    Ok(event) => event,
653                    Err(_) => break,
654                }
655            };
656            state.push_event_into(event, datagrams, max);
657        }
658        Some(datagrams.len())
659    }
660
661    /// Synchronous blocking batch send to one resolved remote identity.
662    ///
663    /// This is the blocking-thread counterpart to [`Self::send_batch_to_peer`].
664    /// The caller keeps routing authority: FIPS only receives already-owned
665    /// endpoint payloads for the resolved peer.
666    pub fn blocking_send_batch_to_peer(
667        &self,
668        remote: PeerIdentity,
669        payloads: Vec<Vec<u8>>,
670    ) -> Result<(), FipsEndpointError> {
671        self.send_payloads_to_peer(remote, payloads)
672    }
673
674    /// Synchronous blocking send of one FSP DataPacket service payload.
675    pub fn blocking_send_datagram(
676        &self,
677        remote: PeerIdentity,
678        source_port: u16,
679        destination_port: u16,
680        payload: Vec<u8>,
681    ) -> Result<(), FipsEndpointError> {
682        self.send_service_datagrams_to_peer(
683            remote,
684            vec![FipsEndpointOutboundDatagram::new(
685                source_port,
686                destination_port,
687                payload,
688            )],
689        )
690    }
691
692    /// Synchronous blocking send of an owned service datagram batch.
693    pub fn blocking_send_datagram_batch_to_peer(
694        &self,
695        remote: PeerIdentity,
696        datagrams: Vec<FipsEndpointOutboundDatagram>,
697    ) -> Result<(), FipsEndpointError> {
698        self.send_service_datagrams_to_peer(remote, datagrams)
699    }
700
701    /// Synchronous blocking batch receive into a caller-owned buffer.
702    ///
703    /// This is the blocking-thread counterpart to [`Self::recv_batch_into`]:
704    /// it parks the calling **OS thread** for the first message, then drains
705    /// ready follow-ons while holding the endpoint receiver lock. MUST NOT be
706    /// called from inside a tokio runtime; use this only from a dedicated
707    /// blocking thread.
708    pub fn blocking_recv_batch_into(
709        &self,
710        messages: &mut Vec<FipsEndpointMessage>,
711        max: usize,
712    ) -> Option<usize> {
713        let max = max.clamp(1, ENDPOINT_RECV_BATCH_MAX);
714        messages.clear();
715
716        let mut state = self.inbound_endpoint_rx.blocking_lock();
717        state.drain_pending_into(messages, max);
718
719        while messages.len() < max {
720            let event = if messages.is_empty() {
721                state.rx.blocking_recv()?
722            } else {
723                match state.rx.try_recv() {
724                    Ok(event) => event,
725                    Err(_) => break,
726                }
727            };
728            state.push_event_into(event, messages, max);
729        }
730
731        Some(messages.len())
732    }
733
734    /// Synchronous blocking receive of registered service datagrams.
735    pub fn blocking_recv_service_datagram_batch_into(
736        &self,
737        datagrams: &mut Vec<FipsEndpointServiceDatagram>,
738        max: usize,
739    ) -> Option<usize> {
740        let max = max.clamp(1, ENDPOINT_RECV_BATCH_MAX);
741        datagrams.clear();
742
743        let mut state = self.inbound_service_rx.blocking_lock();
744        state.drain_pending_into(datagrams, max);
745        while datagrams.len() < max {
746            let event = if datagrams.is_empty() {
747                state.rx.blocking_recv()?
748            } else {
749                match state.rx.try_recv() {
750                    Ok(event) => event,
751                    Err(_) => break,
752                }
753            };
754            state.push_event_into(event, datagrams, max);
755        }
756        Some(datagrams.len())
757    }
758
759    /// Replace the runtime peer list. Newly added auto-connect peers get
760    /// dialed immediately using every known address (overlay-fresh first,
761    /// then operator/cache hints). Removed peers are dropped from the
762    /// retry queue but stay connected if they currently are — the regular
763    /// liveness timeout reaps idle sessions. Existing entries get their
764    /// `addresses` field refreshed so the next retry sees the latest hints.
765    ///
766    /// Pass an empty `addresses` vector for a peer if you want fips to
767    /// resolve them entirely from the Nostr advert at dial time.
768    pub async fn update_peers(
769        &self,
770        peers: Vec<crate::config::PeerConfig>,
771    ) -> Result<UpdatePeersOutcome, FipsEndpointError> {
772        let (response_tx, response_rx) = oneshot::channel();
773        match self
774            .control(
775                "peer update",
776                NodeEndpointControlCommand::UpdatePeers { peers, response_tx },
777                response_rx,
778            )
779            .await?
780        {
781            Ok(outcome) => Ok(UpdatePeersOutcome::from(outcome)),
782            Err(error) => Err(FipsEndpointError::Node(error)),
783        }
784    }
785
786    /// Force immediate direct-path refresh attempts for configured peers.
787    ///
788    /// Unlike [`FipsEndpoint::update_peers`], this does not require a config
789    /// diff. It asks the running node to race a fresh direct handshake for the
790    /// supplied active peers while preserving existing sessions and routes.
791    /// Established end-to-end sessions also start an in-place recovery rekey
792    /// when rekeying is enabled; pending handshakes keep their retry state.
793    pub async fn refresh_peer_paths(
794        &self,
795        peers: Vec<PeerIdentity>,
796    ) -> Result<usize, FipsEndpointError> {
797        let (response_tx, response_rx) = oneshot::channel();
798        let npubs = peers.into_iter().map(|peer| peer.npub()).collect();
799        match self
800            .control(
801                "peer path refresh",
802                NodeEndpointControlCommand::RefreshPeerPaths { npubs, response_tx },
803                response_rx,
804            )
805            .await?
806        {
807            Ok(refreshed) => Ok(refreshed),
808            Err(error) => Err(FipsEndpointError::Node(error)),
809        }
810    }
811
812    /// Register a DNS-resolved peer identity for subsequent IPv6 packet routing.
813    ///
814    /// This only populates the bounded identity cache. It does not configure,
815    /// admit, or connect the peer; the normal authenticated discovery and
816    /// session policy still applies when traffic is sent.
817    pub async fn register_peer_identity(
818        &self,
819        identity: PeerIdentity,
820    ) -> Result<bool, FipsEndpointError> {
821        let (response_tx, response_rx) = oneshot::channel();
822        self.control(
823            "identity registration",
824            NodeEndpointControlCommand::RegisterIdentity {
825                identity,
826                response_tx,
827            },
828            response_rx,
829        )
830        .await
831    }
832
833    /// Snapshot authenticated peers known by the endpoint.
834    pub async fn peers(&self) -> Result<Vec<FipsEndpointPeer>, FipsEndpointError> {
835        let (response_tx, response_rx) = oneshot::channel();
836        self.control(
837            "peer snapshot",
838            NodeEndpointControlCommand::PeerSnapshot { response_tx },
839            response_rx,
840        )
841        .await
842        .map(|peers| peers.into_iter().map(FipsEndpointPeer::from).collect())
843    }
844
845    /// Send an outbound IPv6 packet into the FIPS session pipeline.
846    pub async fn send_ip_packet(
847        &self,
848        packet: impl Into<Vec<u8>>,
849    ) -> Result<(), FipsEndpointError> {
850        self.outbound_packets
851            .send(packet.into())
852            .await
853            .map_err(|_| FipsEndpointError::Closed)
854    }
855
856    /// Enqueue an outbound IPv6 packet from a blocking system-TUN reader.
857    pub fn blocking_send_ip_packet(
858        &self,
859        packet: impl Into<Vec<u8>>,
860    ) -> Result<(), FipsEndpointError> {
861        self.outbound_packets
862            .blocking_send(packet.into())
863            .map_err(|_| FipsEndpointError::Closed)
864    }
865
866    /// Receive the next source-attributed IPv6 packet delivered by FIPS.
867    pub async fn recv_ip_packet(&self) -> Option<NodeDeliveredPacket> {
868        self.delivered_packets.lock().await.recv().await
869    }
870
871    /// Shut down the endpoint and wait for the node task to stop.
872    pub async fn shutdown(&self) -> Result<(), FipsEndpointError> {
873        let shutdown_tx = self
874            .shutdown_tx
875            .lock()
876            .map_err(|_| FipsEndpointError::Closed)?
877            .take();
878        if let Some(shutdown_tx) = shutdown_tx {
879            let _ = shutdown_tx.send(());
880        }
881        let task = self
882            .task
883            .lock()
884            .map_err(|_| FipsEndpointError::Closed)?
885            .take();
886        if let Some(mut task) = task {
887            match tokio::time::timeout(ENDPOINT_OPERATION_TIMEOUT, &mut task).await {
888                Ok(result) => result??,
889                Err(_) => {
890                    task.abort();
891                    let _ = task.await;
892                    return Err(FipsEndpointError::Timeout {
893                        operation: "shutdown",
894                    });
895                }
896            }
897        }
898        Ok(())
899    }
900}
901
902impl Drop for FipsEndpoint {
903    fn drop(&mut self) {
904        if let Ok(mut shutdown_tx) = self.shutdown_tx.lock()
905            && let Some(shutdown_tx) = shutdown_tx.take()
906        {
907            let _ = shutdown_tx.send(());
908        }
909        if let Ok(mut task) = self.task.lock()
910            && task.is_some()
911        {
912            // Dropping a Tokio JoinHandle detaches it. The shutdown signal lets
913            // the owned node reach node.stop(), including bounded WebRTC
914            // physical cleanup, instead of aborting that cleanup at Drop.
915            drop(task.take());
916        }
917    }
918}