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