Skip to main content

fips_core/node/lifecycle/
control.rs

1use super::*;
2
3impl Node {
4    // === Control API methods ===
5
6    /// Connect to a peer via the control API.
7    ///
8    /// Creates an ephemeral peer connection (not persisted to config, no
9    /// auto-reconnect). Reuses the same connection path as auto-connect
10    /// peers. Returns JSON data on success or an error message.
11    pub(crate) async fn api_connect(
12        &mut self,
13        npub: &str,
14        address: &str,
15        transport: &str,
16    ) -> Result<serde_json::Value, String> {
17        let peer_config = PeerConfig {
18            npub: npub.to_string(),
19            alias: None,
20            addresses: vec![PeerAddress::new(transport, address)],
21            connect_policy: ConnectPolicy::Manual,
22            auto_reconnect: false,
23            discovery_fallback_transit: true,
24        };
25
26        // Pre-seed identity cache (same as initiate_peer_connections does)
27        if let Ok(identity) = PeerIdentity::from_npub(npub) {
28            self.peer_aliases
29                .insert(*identity.node_addr(), identity.short_npub());
30            self.register_identity(*identity.node_addr(), identity.pubkey_full());
31        }
32
33        self.initiate_peer_connection(&peer_config)
34            .await
35            .map(|()| {
36                info!(
37                    npub = %npub,
38                    address = %address,
39                    transport = %transport,
40                    "API connect initiated"
41                );
42                serde_json::json!({
43                    "npub": npub,
44                    "address": address,
45                    "transport": transport,
46                })
47            })
48            .map_err(|e| e.to_string())
49    }
50
51    /// Disconnect a peer via the control API.
52    ///
53    /// Removes the peer and suppresses auto-reconnect.
54    pub(crate) fn api_disconnect(&mut self, npub: &str) -> Result<serde_json::Value, String> {
55        let peer_identity =
56            PeerIdentity::from_npub(npub).map_err(|e| format!("invalid npub '{npub}': {e}"))?;
57        let node_addr = *peer_identity.node_addr();
58
59        if !self.peers.contains_key(&node_addr) {
60            return Err(format!("peer not found: {npub}"));
61        }
62
63        // Remove the peer (full cleanup: sessions, indices, links, tree, bloom)
64        self.remove_active_peer(&node_addr);
65
66        // Suppress any pending auto-reconnect
67        self.retry_pending.remove(&node_addr);
68
69        info!(npub = %npub, "API disconnect completed");
70
71        Ok(serde_json::json!({
72            "npub": npub,
73            "disconnected": true,
74        }))
75    }
76
77    /// Adopt an already-established UDP traversal and start the normal FIPS
78    /// Noise handshake over it.
79    ///
80    /// This is intended for integration with an external rendezvous runtime
81    /// that has already completed relay signaling, STUN observation, and UDP
82    /// hole punching. After handoff, the adopted socket is owned by FIPS.
83    pub async fn adopt_established_traversal(
84        &mut self,
85        traversal: EstablishedTraversal,
86    ) -> Result<BootstrapHandoffResult, NodeError> {
87        debug!(
88            peer_npub = %traversal.peer_npub,
89            session_id = %traversal.session_id,
90            remote_addr = %traversal.remote_addr,
91            "adopting established traversal socket"
92        );
93
94        if !self.state.is_operational() {
95            return Err(NodeError::NotStarted);
96        }
97
98        let packet_tx = self.packet_tx.clone().ok_or(NodeError::NotStarted)?;
99        let peer_identity = PeerIdentity::from_npub(&traversal.peer_npub).map_err(|e| {
100            NodeError::InvalidPeerNpub {
101                npub: traversal.peer_npub.clone(),
102                reason: e.to_string(),
103            }
104        })?;
105        let peer_node_addr = *peer_identity.node_addr();
106        if self.peers.contains_key(&peer_node_addr) {
107            debug!(
108                peer_npub = %traversal.peer_npub,
109                "Adopting NAT traversal handoff as alternate path for already-connected peer"
110            );
111        }
112
113        self.peer_aliases
114            .insert(peer_node_addr, peer_identity.short_npub());
115        self.register_identity(peer_node_addr, peer_identity.pubkey_full());
116
117        let transport_id = self.allocate_transport_id();
118        // Adopted ephemeral UDP transports inherit MTU + socket-buffer sizing
119        // (and accept_connections / advertise flags) from the operator's
120        // configured [transports.udp] when the bootstrap runtime doesn't
121        // pass an explicit override. Lookup tries `transport_name` first
122        // (covers the `Named` multi-listener variant) and falls back to the
123        // unnamed `Single` listener, so single- and named-listener configs
124        // both inherit cleanly.
125        //
126        // Tradeoff: `UdpConfig::default()` sets MTU 1280 (IPv6 minimum), the
127        // only value guaranteed to survive arbitrary middlebox paths.
128        // Inheriting a higher operator-chosen MTU means NAT-traversed flows
129        // initially attempt that MTU and may black-hole on tighter paths
130        // until reactive `MtuExceeded` recovery kicks in. Operators who
131        // raise the primary MTU based on known-clean topology accept that
132        // tradeoff; the silent drop on a too-low default was strictly
133        // worse for the common case where the primary MTU is reachable.
134        //
135        // Bind / external address fields are cleared since the socket is
136        // already bound.
137        let inherited_config = traversal.transport_config.clone().unwrap_or_else(|| {
138            let mut cfg = self
139                .lookup_udp_config(traversal.transport_name.as_deref())
140                .or_else(|| self.lookup_udp_config(None))
141                .cloned()
142                .unwrap_or_default();
143            cfg.bind_addr = None;
144            cfg.external_addr = None;
145            cfg
146        });
147        let mut transport = crate::transport::udp::UdpTransport::new(
148            transport_id,
149            traversal.transport_name.clone(),
150            inherited_config,
151            packet_tx,
152        );
153
154        transport
155            .adopt_socket_async(traversal.socket)
156            .await
157            .map_err(|e| NodeError::BootstrapHandoff(e.to_string()))?;
158
159        let local_addr = transport.local_addr().ok_or_else(|| {
160            NodeError::BootstrapHandoff("adopted UDP transport has no local address".into())
161        })?;
162
163        self.transports.insert(
164            transport_id,
165            crate::transport::TransportHandle::Udp(transport),
166        );
167        self.bootstrap_transports
168            .register(transport_id, traversal.peer_npub.clone());
169
170        let remote_addr = TransportAddr::from_string(&traversal.remote_addr.to_string());
171        if let Err(err) = self
172            .initiate_connection(transport_id, remote_addr.clone(), peer_identity)
173            .await
174        {
175            self.bootstrap_transports.remove(&transport_id);
176            if let Some(mut handle) = self.transports.remove(&transport_id) {
177                let _ = handle.stop().await;
178            }
179            return Err(err);
180        }
181
182        info!(
183            peer = %self.peer_display_name(&peer_node_addr),
184            transport_id = %transport_id,
185            local_addr = %local_addr,
186            remote_addr = %traversal.remote_addr,
187            session_id = %traversal.session_id,
188            "adopted NAT traversal socket; handshake initiated"
189        );
190
191        Ok(BootstrapHandoffResult {
192            transport_id,
193            local_addr,
194            remote_addr: traversal.remote_addr,
195            peer_node_addr,
196            session_id: traversal.session_id,
197        })
198    }
199}