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_rendezvous: bool,
10    local_instance_roles: Vec<crate::discovery::local::LocalInstanceCapability>,
11    disable_system_networking: bool,
12    packet_channel_capacity: usize,
13}
14
15const DEFAULT_ENDPOINT_PACKET_CHANNEL_CAPACITY: usize = 4096;
16
17impl Default for FipsEndpointBuilder {
18    fn default() -> Self {
19        Self {
20            config: Config::new(),
21            identity_nsec: None,
22            discovery_scope: None,
23            local_rendezvous: false,
24            local_instance_roles: Vec::new(),
25            disable_system_networking: true,
26            packet_channel_capacity: DEFAULT_ENDPOINT_PACKET_CHANNEL_CAPACITY,
27        }
28    }
29}
30
31impl FipsEndpointBuilder {
32    /// Start from an explicit FIPS config.
33    pub fn config(mut self, config: Config) -> Self {
34        self.config = config;
35        self
36    }
37
38    /// Use an `nsec` or hex secret for the endpoint identity.
39    pub fn identity_nsec(mut self, nsec: impl Into<String>) -> Self {
40        self.identity_nsec = Some(nsec.into());
41        self
42    }
43
44    /// Set an application-level discovery scope.
45    ///
46    /// When the builder owns the default empty connectivity config, this also
47    /// enables scoped Nostr discovery, open same-scope peer discovery, local
48    /// LAN candidates, host-wide loopback rendezvous, and a UDP NAT advert.
49    /// If an explicit transport or Nostr config was supplied, the explicit
50    /// config is left in control and the scope is retained as endpoint
51    /// metadata; call [`Self::local_rendezvous`] to opt that endpoint into
52    /// same-host composition.
53    pub fn discovery_scope(mut self, scope: impl Into<String>) -> Self {
54        self.discovery_scope = Some(scope.into());
55        self
56    }
57
58    /// Enable host-wide authenticated loopback composition.
59    pub fn local_rendezvous(mut self) -> Self {
60        self.local_rendezvous = true;
61        self
62    }
63
64    /// Advertise a portless same-host role, such as `nostr.pubsub/1`.
65    /// Empty, oversized, and excess names are ignored.
66    pub fn local_role(mut self, name: impl Into<String>, priority: i16) -> Self {
67        let name = name.into().trim().to_string();
68        if crate::discovery::local_udp::local_capability_name_is_valid(&name)
69            && self.local_instance_roles.len()
70                < crate::discovery::local_udp::LOCAL_CAPABILITY_MAX_COUNT
71        {
72            self.local_instance_roles.push(
73                crate::discovery::local::LocalInstanceCapability::role(name)
74                    .with_priority(priority),
75            );
76        }
77        self
78    }
79
80    /// Disable FIPS-owned TUN and DNS system integration.
81    pub fn without_system_tun(mut self) -> Self {
82        self.disable_system_networking = true;
83        self
84    }
85
86    /// Set the app packet/data channel capacity.
87    pub fn packet_channel_capacity(mut self, capacity: usize) -> Self {
88        self.packet_channel_capacity = capacity.max(1);
89        self
90    }
91
92    pub(super) fn prepared_config(&self) -> Config {
93        let mut config = self.config.clone();
94        if let Some(nsec) = &self.identity_nsec {
95            config.node.identity = IdentityConfig {
96                nsec: Some(nsec.clone()),
97                persistent: false,
98            };
99        }
100        if self.disable_system_networking {
101            config.tun.enabled = false;
102            config.dns.enabled = false;
103            config.node.system_files_enabled = false;
104        }
105        if self.local_rendezvous {
106            config.node.discovery.local.enabled = true;
107        }
108        if let Some(scope) = self.discovery_scope.as_deref() {
109            if config
110                .node
111                .discovery
112                .lan
113                .scope
114                .as_deref()
115                .is_none_or(|scope| scope.trim().is_empty())
116            {
117                config.node.discovery.lan.scope = Some(scope.to_string());
118            }
119            apply_default_scoped_discovery(&mut config, scope);
120        }
121        config
122    }
123
124    /// Bind and start the embedded endpoint.
125    pub async fn bind(self) -> Result<FipsEndpoint, FipsEndpointError> {
126        self.bind_inner(None).await
127    }
128
129    /// Bind with a bounded receiver for direct dataplane endpoint packet runs.
130    pub async fn bind_with_direct_receiver(
131        self,
132    ) -> Result<(FipsEndpoint, FipsEndpointDirectReceiver), FipsEndpointError> {
133        let (sink, receiver) = FipsEndpointDirectReceiver::channel();
134        let endpoint = self.bind_with_direct_sink(sink).await?;
135        Ok((endpoint, receiver))
136    }
137
138    /// Bind and start the endpoint with a direct dataplane endpoint-data sink.
139    ///
140    /// Decrypted dataplane endpoint output is delivered to `sink` synchronously from
141    /// the dataplane output path. Generic endpoint events, including loopback sends
142    /// and non-dataplane delivery, continue to use the regular receive queue.
143    pub async fn bind_with_direct_sink<S>(self, sink: S) -> Result<FipsEndpoint, FipsEndpointError>
144    where
145        S: FipsEndpointDirectSink,
146    {
147        self.bind_inner(Some(EndpointDirectSink::new(sink))).await
148    }
149
150    async fn bind_inner(
151        self,
152        direct_sink: Option<EndpointDirectSink>,
153    ) -> Result<FipsEndpoint, FipsEndpointError> {
154        let config = self.prepared_config();
155
156        let mut node = Node::new(config)?;
157        node.set_local_instance_roles(self.local_instance_roles);
158        let identity = PeerIdentity::from_pubkey_full(node.identity().pubkey_full());
159        let npub = identity.npub();
160        let node_addr = *identity.node_addr();
161        let address = *identity.address();
162        let packet_io = node.attach_external_packet_io(self.packet_channel_capacity)?;
163        let endpoint_data_io = match direct_sink {
164            Some(sink) => {
165                node.attach_endpoint_data_io_with_direct_sink(self.packet_channel_capacity, sink)?
166            }
167            None => node.attach_endpoint_data_io(self.packet_channel_capacity)?,
168        };
169        node.start().await?;
170        let local_capability_directory = node.local_capability_directory();
171
172        let (shutdown_tx, shutdown_rx) = oneshot::channel();
173        let task = spawn_node_task(node, shutdown_rx);
174        let endpoint_control_tx = endpoint_data_io.control_tx;
175        let endpoint_data_batches = endpoint_data_io.data_batch_tx;
176        let inbound_service_tx = endpoint_data_io.service_event_tx;
177
178        Ok(FipsEndpoint {
179            identity,
180            npub,
181            node_addr,
182            address,
183            discovery_scope: self.discovery_scope,
184            local_capability_directory,
185            outbound_packets: packet_io.outbound_tx,
186            delivered_packets: Arc::new(Mutex::new(packet_io.inbound_rx)),
187            endpoint_control_tx,
188            endpoint_data_batches,
189            inbound_endpoint_tx: endpoint_data_io.event_tx,
190            inbound_endpoint_rx: Arc::new(Mutex::new(EndpointReceiveState::new(
191                endpoint_data_io.event_rx,
192            ))),
193            inbound_service_tx,
194            inbound_service_rx: Arc::new(Mutex::new(ServiceReceiveState::new(
195                endpoint_data_io.service_event_rx,
196            ))),
197            registered_services: Arc::new(StdMutex::new(HashMap::new())),
198            service_channel_capacity: self.packet_channel_capacity,
199            shutdown_tx: std::sync::Mutex::new(Some(shutdown_tx)),
200            task: std::sync::Mutex::new(Some(task)),
201        })
202    }
203}