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    /// Notifies the peer, removes it locally, and suppresses auto-reconnect.
54    pub(crate) async 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        self.send_disconnect_to_peer(&node_addr, DisconnectReason::ConfigurationChange)
64            .await;
65
66        // Remove the peer (full cleanup: sessions, indices, links, tree, bloom)
67        self.remove_active_peer(&node_addr);
68
69        // Suppress any pending auto-reconnect
70        self.retry_pending.remove(&node_addr);
71
72        info!(npub = %npub, "API disconnect completed");
73
74        Ok(serde_json::json!({
75            "npub": npub,
76            "disconnected": true,
77        }))
78    }
79
80    /// Adopt an already-established UDP traversal and start the normal FIPS
81    /// Noise handshake over it.
82    ///
83    /// This is intended for integration with an external rendezvous runtime
84    /// that has already completed relay signaling, STUN observation, and UDP
85    /// hole punching. After handoff, the adopted socket is owned by FIPS.
86    pub async fn adopt_established_traversal(
87        &mut self,
88        traversal: EstablishedTraversal,
89    ) -> Result<BootstrapHandoffResult, NodeError> {
90        debug!(
91            peer_npub = %traversal.peer_npub,
92            session_id = %traversal.session_id,
93            remote_addr = %traversal.remote_addr,
94            "adopting established traversal socket"
95        );
96
97        if !self.state.is_operational() {
98            return Err(NodeError::NotStarted);
99        }
100
101        let packet_tx = self.packet_tx.clone().ok_or(NodeError::NotStarted)?;
102        let peer_identity = PeerIdentity::from_npub(&traversal.peer_npub).map_err(|e| {
103            NodeError::InvalidPeerNpub {
104                npub: traversal.peer_npub.clone(),
105                reason: e.to_string(),
106            }
107        })?;
108        let peer_node_addr = *peer_identity.node_addr();
109        if self.peers.contains_key(&peer_node_addr) {
110            debug!(
111                peer_npub = %traversal.peer_npub,
112                "Adopting NAT traversal handoff as alternate path for already-connected peer"
113            );
114        }
115
116        self.peer_aliases
117            .insert(peer_node_addr, peer_identity.short_npub());
118        self.register_identity(peer_node_addr, peer_identity.pubkey_full());
119
120        let transport_id = self.allocate_transport_id();
121        // Adopted ephemeral UDP transports inherit MTU + socket-buffer sizing
122        // (and accept_connections / advertise flags) from the operator's
123        // configured [transports.udp] when the bootstrap runtime doesn't
124        // pass an explicit override. Lookup tries `transport_name` first
125        // (covers the `Named` multi-listener variant) and falls back to the
126        // unnamed `Single` listener, so single- and named-listener configs
127        // both inherit cleanly.
128        //
129        // Tradeoff: `UdpConfig::default()` sets MTU 1280 (IPv6 minimum), the
130        // only value guaranteed to survive arbitrary middlebox paths.
131        // Inheriting a higher operator-chosen MTU means NAT-traversed flows
132        // initially attempt that MTU and may black-hole on tighter paths
133        // until reactive `MtuExceeded` recovery kicks in. Operators who
134        // raise the primary MTU based on known-clean topology accept that
135        // tradeoff; the silent drop on a too-low default was strictly
136        // worse for the common case where the primary MTU is reachable.
137        //
138        // Bind / external address fields are cleared since the socket is
139        // already bound.
140        let inherited_config = traversal.transport_config.clone().unwrap_or_else(|| {
141            let mut cfg = self
142                .lookup_udp_config(traversal.transport_name.as_deref())
143                .or_else(|| self.lookup_udp_config(None))
144                .cloned()
145                .unwrap_or_default();
146            cfg.bind_addr = None;
147            cfg.external_addr = None;
148            cfg
149        });
150        let mut transport = crate::transport::udp::UdpTransport::new(
151            transport_id,
152            traversal.transport_name.clone(),
153            inherited_config,
154            packet_tx,
155        );
156
157        transport
158            .adopt_socket_async(traversal.socket)
159            .await
160            .map_err(|e| NodeError::BootstrapHandoff(e.to_string()))?;
161
162        let local_addr = transport.local_addr().ok_or_else(|| {
163            NodeError::BootstrapHandoff("adopted UDP transport has no local address".into())
164        })?;
165
166        self.transports.insert(
167            transport_id,
168            crate::transport::TransportHandle::Udp(transport),
169        );
170        self.bootstrap_transports
171            .register(transport_id, traversal.peer_npub.clone());
172
173        let remote_addr = TransportAddr::from_string(&traversal.remote_addr.to_string());
174        if let Err(err) = self
175            .initiate_connection(transport_id, remote_addr.clone(), peer_identity)
176            .await
177        {
178            self.bootstrap_transports.remove(&transport_id);
179            if let Some(mut handle) = self.transports.remove(&transport_id) {
180                let _ = handle.stop().await;
181            }
182            return Err(err);
183        }
184
185        info!(
186            peer = %self.peer_display_name(&peer_node_addr),
187            transport_id = %transport_id,
188            local_addr = %local_addr,
189            remote_addr = %traversal.remote_addr,
190            session_id = %traversal.session_id,
191            "adopted NAT traversal socket; handshake initiated"
192        );
193
194        Ok(BootstrapHandoffResult {
195            transport_id,
196            local_addr,
197            remote_addr: traversal.remote_addr,
198            peer_node_addr,
199            session_id: traversal.session_id,
200        })
201    }
202}