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