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;
15const MAX_ENDPOINT_PACKET_CHANNEL_CAPACITY: usize = 65_536;
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_ethernet_interfaces: Vec::new(),
24            disable_system_networking: true,
25            packet_channel_capacity: default_endpoint_packet_channel_capacity(),
26        }
27    }
28}
29
30fn default_endpoint_packet_channel_capacity() -> usize {
31    parse_endpoint_packet_channel_capacity(
32        std::env::var("FIPS_ENDPOINT_PACKET_CHANNEL_CAP")
33            .ok()
34            .as_deref(),
35        DEFAULT_ENDPOINT_PACKET_CHANNEL_CAPACITY,
36    )
37}
38
39fn parse_endpoint_packet_channel_capacity(raw: Option<&str>, default: usize) -> usize {
40    raw.and_then(|raw| raw.trim().parse::<usize>().ok())
41        .unwrap_or(default)
42        .clamp(1, MAX_ENDPOINT_PACKET_CHANNEL_CAPACITY)
43}
44
45impl FipsEndpointBuilder {
46    /// Start from an explicit FIPS config.
47    pub fn config(mut self, config: Config) -> Self {
48        self.config = config;
49        self
50    }
51
52    /// Use an `nsec` or hex secret for the endpoint identity.
53    pub fn identity_nsec(mut self, nsec: impl Into<String>) -> Self {
54        self.identity_nsec = Some(nsec.into());
55        self
56    }
57
58    /// Set an application-level discovery scope.
59    ///
60    /// When the builder owns the default empty connectivity config, this also
61    /// enables scoped Nostr discovery, open same-scope peer discovery, local
62    /// LAN candidates, and a UDP NAT advert. If an explicit transport or
63    /// Nostr config was supplied, the explicit config is left in control and
64    /// the scope is retained as endpoint metadata.
65    pub fn discovery_scope(mut self, scope: impl Into<String>) -> Self {
66        self.discovery_scope = Some(scope.into());
67        self
68    }
69
70    /// Enable host-local Ethernet discovery on a private L2 interface.
71    ///
72    /// This is intended for veth/TAP interfaces attached to a per-host bridge
73    /// shared by FIPS-aware applications. The endpoint announces Ethernet
74    /// beacons, listens for matching peers, auto-connects to them, and accepts
75    /// inbound handshakes over the interface.
76    pub fn local_ethernet(mut self, interface: impl Into<String>) -> Self {
77        self.local_ethernet_interfaces.push(interface.into());
78        self
79    }
80
81    /// Disable FIPS-owned TUN and DNS system integration.
82    pub fn without_system_tun(mut self) -> Self {
83        self.disable_system_networking = true;
84        self
85    }
86
87    /// Set the app packet/data channel capacity.
88    pub fn packet_channel_capacity(mut self, capacity: usize) -> Self {
89        self.packet_channel_capacity = capacity.max(1);
90        self
91    }
92
93    pub(super) fn prepared_config(&self) -> Config {
94        let mut config = self.config.clone();
95        if let Some(nsec) = &self.identity_nsec {
96            config.node.identity = IdentityConfig {
97                nsec: Some(nsec.clone()),
98                persistent: false,
99            };
100        }
101        if self.disable_system_networking {
102            config.tun.enabled = false;
103            config.dns.enabled = false;
104            config.node.system_files_enabled = false;
105        }
106        if let Some(scope) = self.discovery_scope.as_deref() {
107            config.node.discovery.lan.scope = Some(scope.to_string());
108            config.node.discovery.local.enabled = true;
109            apply_default_scoped_discovery(&mut config, scope);
110        }
111        for interface in &self.local_ethernet_interfaces {
112            add_endpoint_ethernet_transport(
113                &mut config,
114                interface,
115                self.discovery_scope.as_deref(),
116            );
117        }
118        config
119    }
120
121    /// Bind and start the embedded endpoint.
122    pub async fn bind(self) -> Result<FipsEndpoint, FipsEndpointError> {
123        endpoint_debug_log("FipsEndpointBuilder::bind begin");
124        let config = self.prepared_config();
125        endpoint_debug_log("FipsEndpointBuilder::bind config prepared");
126
127        let mut node = Node::new(config)?;
128        endpoint_debug_log("FipsEndpointBuilder::bind node created");
129        let identity = PeerIdentity::from_pubkey_full(node.identity().pubkey_full());
130        let npub = identity.npub();
131        let node_addr = *identity.node_addr();
132        let address = *identity.address();
133        let packet_io = node.attach_external_packet_io(self.packet_channel_capacity)?;
134        endpoint_debug_log("FipsEndpointBuilder::bind packet io attached");
135        let endpoint_data_io = node.attach_endpoint_data_io(self.packet_channel_capacity)?;
136        endpoint_debug_log("FipsEndpointBuilder::bind endpoint data io attached");
137        endpoint_debug_log("FipsEndpointBuilder::bind node.start begin");
138        node.start().await?;
139        endpoint_debug_log("FipsEndpointBuilder::bind node.start complete");
140
141        let (shutdown_tx, shutdown_rx) = oneshot::channel();
142        let task = spawn_node_task(node, shutdown_rx);
143        endpoint_debug_log("FipsEndpointBuilder::bind node task spawned");
144        let endpoint_priority_commands = endpoint_data_io.priority_command_tx;
145        let endpoint_commands = endpoint_data_io.command_tx;
146        #[cfg(unix)]
147        let endpoint_bulk_send_runtime = endpoint_data_io.bulk_send_runtime;
148
149        Ok(FipsEndpoint {
150            identity,
151            npub,
152            node_addr,
153            address,
154            discovery_scope: self.discovery_scope,
155            outbound_packets: packet_io.outbound_tx,
156            delivered_packets: Arc::new(Mutex::new(packet_io.inbound_rx)),
157            endpoint_priority_commands,
158            endpoint_commands,
159            #[cfg(unix)]
160            endpoint_bulk_send_runtime,
161            inbound_endpoint_tx: endpoint_data_io.event_tx,
162            inbound_endpoint_rx: Arc::new(Mutex::new(EndpointReceiveState::new(
163                endpoint_data_io.event_rx,
164            ))),
165            peer_identity_cache: std::sync::Mutex::new(std::collections::HashMap::new()),
166            shutdown_tx: std::sync::Mutex::new(Some(shutdown_tx)),
167            task: std::sync::Mutex::new(Some(task)),
168        })
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn endpoint_packet_channel_capacity_env_keeps_safe_bounds() {
178        assert_eq!(
179            parse_endpoint_packet_channel_capacity(None, DEFAULT_ENDPOINT_PACKET_CHANNEL_CAPACITY),
180            DEFAULT_ENDPOINT_PACKET_CHANNEL_CAPACITY
181        );
182        assert_eq!(
183            parse_endpoint_packet_channel_capacity(
184                Some(""),
185                DEFAULT_ENDPOINT_PACKET_CHANNEL_CAPACITY
186            ),
187            DEFAULT_ENDPOINT_PACKET_CHANNEL_CAPACITY
188        );
189        assert_eq!(
190            parse_endpoint_packet_channel_capacity(
191                Some("not-a-number"),
192                DEFAULT_ENDPOINT_PACKET_CHANNEL_CAPACITY
193            ),
194            DEFAULT_ENDPOINT_PACKET_CHANNEL_CAPACITY
195        );
196        assert_eq!(
197            parse_endpoint_packet_channel_capacity(
198                Some("0"),
199                DEFAULT_ENDPOINT_PACKET_CHANNEL_CAPACITY
200            ),
201            1
202        );
203        assert_eq!(
204            parse_endpoint_packet_channel_capacity(
205                Some("8192"),
206                DEFAULT_ENDPOINT_PACKET_CHANNEL_CAPACITY
207            ),
208            8192
209        );
210        assert_eq!(
211            parse_endpoint_packet_channel_capacity(
212                Some("999999"),
213                DEFAULT_ENDPOINT_PACKET_CHANNEL_CAPACITY
214            ),
215            MAX_ENDPOINT_PACKET_CHANNEL_CAPACITY
216        );
217    }
218}