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