Skip to main content

fips_core/endpoint/
builder.rs

1use super::*;
2
3/// Builder for an embedded FIPS endpoint.
4#[derive(Debug, Clone)]
5pub struct FipsEndpointBuilder {
6    config: Config,
7    identity_nsec: Option<String>,
8    discovery_scope: Option<String>,
9    local_ethernet_interfaces: Vec<String>,
10    disable_system_networking: bool,
11    packet_channel_capacity: usize,
12}
13
14const DEFAULT_ENDPOINT_PACKET_CHANNEL_CAPACITY: usize = 4096;
15
16impl Default for FipsEndpointBuilder {
17    fn default() -> Self {
18        Self {
19            config: Config::new(),
20            identity_nsec: None,
21            discovery_scope: None,
22            local_ethernet_interfaces: Vec::new(),
23            disable_system_networking: true,
24            packet_channel_capacity: DEFAULT_ENDPOINT_PACKET_CHANNEL_CAPACITY,
25        }
26    }
27}
28
29impl FipsEndpointBuilder {
30    /// Start from an explicit FIPS config.
31    pub fn config(mut self, config: Config) -> Self {
32        self.config = config;
33        self
34    }
35
36    /// Use an `nsec` or hex secret for the endpoint identity.
37    pub fn identity_nsec(mut self, nsec: impl Into<String>) -> Self {
38        self.identity_nsec = Some(nsec.into());
39        self
40    }
41
42    /// Set an application-level discovery scope.
43    ///
44    /// When the builder owns the default empty connectivity config, this also
45    /// enables scoped Nostr discovery, open same-scope peer discovery, local
46    /// LAN candidates, and a UDP NAT advert. If an explicit transport or
47    /// Nostr config was supplied, the explicit config is left in control and
48    /// the scope is retained as endpoint metadata.
49    pub fn discovery_scope(mut self, scope: impl Into<String>) -> Self {
50        self.discovery_scope = Some(scope.into());
51        self
52    }
53
54    /// Enable host-local Ethernet discovery on a private L2 interface.
55    ///
56    /// This is intended for veth/TAP interfaces attached to a per-host bridge
57    /// shared by FIPS-aware applications. The endpoint announces Ethernet
58    /// beacons, listens for matching peers, auto-connects to them, and accepts
59    /// inbound handshakes over the interface.
60    pub fn local_ethernet(mut self, interface: impl Into<String>) -> Self {
61        self.local_ethernet_interfaces.push(interface.into());
62        self
63    }
64
65    /// Disable FIPS-owned TUN and DNS system integration.
66    pub fn without_system_tun(mut self) -> Self {
67        self.disable_system_networking = true;
68        self
69    }
70
71    /// Set the app packet/data channel capacity.
72    pub fn packet_channel_capacity(mut self, capacity: usize) -> Self {
73        self.packet_channel_capacity = capacity.max(1);
74        self
75    }
76
77    pub(super) fn prepared_config(&self) -> Config {
78        let mut config = self.config.clone();
79        if let Some(nsec) = &self.identity_nsec {
80            config.node.identity = IdentityConfig {
81                nsec: Some(nsec.clone()),
82                persistent: false,
83            };
84        }
85        if self.disable_system_networking {
86            config.tun.enabled = false;
87            config.dns.enabled = false;
88            config.node.system_files_enabled = false;
89        }
90        if let Some(scope) = self.discovery_scope.as_deref() {
91            config.node.discovery.lan.scope = Some(scope.to_string());
92            config.node.discovery.local.enabled = true;
93            apply_default_scoped_discovery(&mut config, scope);
94        }
95        for interface in &self.local_ethernet_interfaces {
96            add_endpoint_ethernet_transport(
97                &mut config,
98                interface,
99                self.discovery_scope.as_deref(),
100            );
101        }
102        config
103    }
104
105    /// Bind and start the embedded endpoint.
106    pub async fn bind(self) -> Result<FipsEndpoint, FipsEndpointError> {
107        self.bind_inner(None).await
108    }
109
110    /// Bind and start the endpoint with a direct PM2 endpoint-data sink.
111    ///
112    /// Decrypted PM2 endpoint output is delivered to `sink` synchronously from
113    /// the PM2 output path. Generic endpoint events, including loopback sends
114    /// and non-PM2 delivery, continue to use the regular receive queue.
115    pub async fn bind_with_direct_sink<S>(self, sink: S) -> Result<FipsEndpoint, FipsEndpointError>
116    where
117        S: FipsEndpointDirectSink,
118    {
119        self.bind_inner(Some(EndpointDirectSink::new(sink))).await
120    }
121
122    async fn bind_inner(
123        self,
124        direct_sink: Option<EndpointDirectSink>,
125    ) -> Result<FipsEndpoint, FipsEndpointError> {
126        endpoint_debug_log("FipsEndpointBuilder::bind begin");
127        let config = self.prepared_config();
128        endpoint_debug_log("FipsEndpointBuilder::bind config prepared");
129
130        let mut node = Node::new(config)?;
131        endpoint_debug_log("FipsEndpointBuilder::bind node created");
132        let identity = PeerIdentity::from_pubkey_full(node.identity().pubkey_full());
133        let npub = identity.npub();
134        let node_addr = *identity.node_addr();
135        let address = *identity.address();
136        let packet_io = node.attach_external_packet_io(self.packet_channel_capacity)?;
137        endpoint_debug_log("FipsEndpointBuilder::bind packet io attached");
138        let endpoint_data_io = match direct_sink {
139            Some(sink) => {
140                node.attach_endpoint_data_io_with_direct_sink(self.packet_channel_capacity, sink)?
141            }
142            None => node.attach_endpoint_data_io(self.packet_channel_capacity)?,
143        };
144        endpoint_debug_log("FipsEndpointBuilder::bind endpoint data io attached");
145        endpoint_debug_log("FipsEndpointBuilder::bind node.start begin");
146        node.start().await?;
147        endpoint_debug_log("FipsEndpointBuilder::bind node.start complete");
148
149        let (shutdown_tx, shutdown_rx) = oneshot::channel();
150        let task = spawn_node_task(node, shutdown_rx);
151        endpoint_debug_log("FipsEndpointBuilder::bind node task spawned");
152        let endpoint_control_tx = endpoint_data_io.control_tx;
153        let endpoint_data_batches = endpoint_data_io.data_batch_tx;
154
155        Ok(FipsEndpoint {
156            identity,
157            npub,
158            node_addr,
159            address,
160            discovery_scope: self.discovery_scope,
161            outbound_packets: packet_io.outbound_tx,
162            delivered_packets: Arc::new(Mutex::new(packet_io.inbound_rx)),
163            endpoint_control_tx,
164            endpoint_data_batches,
165            inbound_endpoint_tx: endpoint_data_io.event_tx,
166            inbound_endpoint_rx: Arc::new(Mutex::new(EndpointReceiveState::new(
167                endpoint_data_io.event_rx,
168            ))),
169            peer_identity_cache: std::sync::Mutex::new(std::collections::HashMap::new()),
170            shutdown_tx: std::sync::Mutex::new(Some(shutdown_tx)),
171            task: std::sync::Mutex::new(Some(task)),
172        })
173    }
174}