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