Skip to main content

rns_net/
shared_client.rs

1//! Shared instance client mode.
2//!
3//! Allows an RnsNode to connect as a client to an already-running Reticulum
4//! daemon, proxying operations through it. The client runs a minimal transport
5//! engine with `transport_enabled: false` — it does no routing of its own, but
6//! registers local destinations and sends/receives packets via the local
7//! connection.
8//!
9//! This matches Python's behavior when `share_instance = True` and a daemon
10//! is already running: the new process connects as a client rather than
11//! starting its own interfaces.
12
13use std::io;
14use std::path::Path;
15use std::sync::atomic::{AtomicU64, Ordering};
16use std::sync::Arc;
17use std::thread;
18use std::time::Duration;
19
20use rns_core::transport::types::TransportConfig;
21
22use crate::driver::{Callbacks, Driver};
23use crate::event;
24use crate::interface::local::LocalClientConfig;
25use crate::interface::{InterfaceEntry, InterfaceStats};
26use crate::node::RnsNode;
27use crate::storage;
28use crate::time;
29
30/// Configuration for connecting as a shared instance client.
31pub struct SharedClientConfig {
32    /// Instance name for Unix socket namespace (e.g. "default" → `\0rns/default`).
33    pub instance_name: String,
34    /// TCP port to try if Unix socket fails (default 37428).
35    pub port: u16,
36    /// RPC control port for queries (default 37429).
37    pub rpc_port: u16,
38}
39
40impl Default for SharedClientConfig {
41    fn default() -> Self {
42        SharedClientConfig {
43            instance_name: "default".into(),
44            port: 37428,
45            rpc_port: 37429,
46        }
47    }
48}
49
50impl RnsNode {
51    /// Connect to an existing shared instance as a client.
52    ///
53    /// The client runs `transport_enabled: false` — it does no routing,
54    /// but can register destinations and send/receive packets through
55    /// the daemon.
56    pub fn connect_shared(
57        config: SharedClientConfig,
58        callbacks: Box<dyn Callbacks>,
59    ) -> io::Result<Self> {
60        let transport_config = TransportConfig {
61            transport_enabled: false,
62            identity_hash: None,
63            prefer_shorter_path: false,
64            max_paths_per_destination: 1,
65        };
66
67        let (tx, rx) = event::channel();
68        let mut driver = Driver::new(transport_config, rx, tx.clone(), callbacks);
69
70        // Connect to the daemon via LocalClientInterface
71        let local_config = LocalClientConfig {
72            name: "Local shared instance".into(),
73            instance_name: config.instance_name.clone(),
74            port: config.port,
75            interface_id: rns_core::transport::types::InterfaceId(1),
76            reconnect_wait: Duration::from_secs(8),
77        };
78
79        let id = local_config.interface_id;
80        let info = rns_core::transport::types::InterfaceInfo {
81            id,
82            name: "LocalInterface".into(),
83            mode: rns_core::constants::MODE_FULL,
84            out_capable: true,
85            in_capable: true,
86            bitrate: Some(1_000_000_000),
87            announce_rate_target: None,
88            announce_rate_grace: 0,
89            announce_rate_penalty: 0.0,
90            announce_cap: rns_core::constants::ANNOUNCE_CAP,
91            is_local_client: true,
92            wants_tunnel: false,
93            tunnel_id: None,
94            mtu: 65535,
95            ia_freq: 0.0,
96            started: time::now(),
97            ingress_control: false,
98        };
99
100        let writer = crate::interface::local::start_client(local_config, tx.clone())?;
101
102        driver.engine.register_interface(info.clone());
103        driver.interfaces.insert(
104            id,
105            InterfaceEntry {
106                id,
107                info,
108                writer,
109                online: false,
110                dynamic: false,
111                ifac: None,
112                stats: InterfaceStats {
113                    started: time::now(),
114                    ..Default::default()
115                },
116                interface_type: "LocalClientInterface".to_string(),
117            },
118        );
119
120        // Spawn timer thread with configurable tick interval
121        let tick_interval_ms = Arc::new(AtomicU64::new(1000));
122        let timer_tx = tx.clone();
123        let timer_interval = Arc::clone(&tick_interval_ms);
124        thread::Builder::new()
125            .name("rns-timer-client".into())
126            .spawn(move || loop {
127                let ms = timer_interval.load(Ordering::Relaxed);
128                thread::sleep(Duration::from_millis(ms));
129                if timer_tx.send(event::Event::Tick).is_err() {
130                    break;
131                }
132            })?;
133
134        // Spawn driver thread
135        let driver_handle = thread::Builder::new()
136            .name("rns-driver-client".into())
137            .spawn(move || {
138                driver.run();
139            })?;
140
141        Ok(RnsNode::from_parts(
142            tx,
143            driver_handle,
144            None,
145            tick_interval_ms,
146        ))
147    }
148
149    /// Connect to a shared instance, with config loaded from a config directory.
150    ///
151    /// Reads the config file to determine instance_name and ports.
152    pub fn connect_shared_from_config(
153        config_path: Option<&Path>,
154        callbacks: Box<dyn Callbacks>,
155    ) -> io::Result<Self> {
156        let config_dir = storage::resolve_config_dir(config_path);
157
158        // Parse config file for instance settings
159        let config_file = config_dir.join("config");
160        let rns_config = if config_file.exists() {
161            crate::config::parse_file(&config_file)
162                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("{}", e)))?
163        } else {
164            crate::config::parse("")
165                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("{}", e)))?
166        };
167
168        let shared_config = SharedClientConfig {
169            instance_name: rns_config.reticulum.instance_name.clone(),
170            port: rns_config.reticulum.shared_instance_port,
171            rpc_port: rns_config.reticulum.instance_control_port,
172        };
173
174        Self::connect_shared(shared_config, callbacks)
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use std::sync::atomic::{AtomicU64, Ordering};
182    use std::sync::mpsc;
183    use std::sync::Arc;
184
185    use crate::interface::local::LocalServerConfig;
186
187    struct NoopCallbacks;
188    impl Callbacks for NoopCallbacks {
189        fn on_announce(&mut self, _: crate::destination::AnnouncedIdentity) {}
190        fn on_path_updated(&mut self, _: rns_core::types::DestHash, _: u8) {}
191        fn on_local_delivery(
192            &mut self,
193            _: rns_core::types::DestHash,
194            _: Vec<u8>,
195            _: rns_core::types::PacketHash,
196        ) {
197        }
198    }
199
200    fn find_free_port() -> u16 {
201        std::net::TcpListener::bind("127.0.0.1:0")
202            .unwrap()
203            .local_addr()
204            .unwrap()
205            .port()
206    }
207
208    #[test]
209    fn connect_shared_to_tcp_server() {
210        let port = find_free_port();
211        let next_id = Arc::new(AtomicU64::new(50000));
212        let (server_tx, server_rx) = mpsc::channel();
213
214        // Start a local server
215        let server_config = LocalServerConfig {
216            instance_name: "test-shared-connect".into(),
217            port,
218            interface_id: rns_core::transport::types::InterfaceId(99),
219        };
220
221        crate::interface::local::start_server(server_config, server_tx, next_id).unwrap();
222        thread::sleep(Duration::from_millis(50));
223
224        // Connect as shared client
225        let config = SharedClientConfig {
226            instance_name: "test-shared-connect".into(),
227            port,
228            rpc_port: 0,
229        };
230
231        let node = RnsNode::connect_shared(config, Box::new(NoopCallbacks)).unwrap();
232
233        // Server should see InterfaceUp for the client
234        let event = server_rx.recv_timeout(Duration::from_secs(2)).unwrap();
235        assert!(matches!(event, crate::event::Event::InterfaceUp(_, _, _)));
236
237        node.shutdown();
238    }
239
240    #[test]
241    fn shared_client_register_destination() {
242        let port = find_free_port();
243        let next_id = Arc::new(AtomicU64::new(51000));
244        let (server_tx, _server_rx) = mpsc::channel();
245
246        let server_config = LocalServerConfig {
247            instance_name: "test-shared-reg".into(),
248            port,
249            interface_id: rns_core::transport::types::InterfaceId(98),
250        };
251
252        crate::interface::local::start_server(server_config, server_tx, next_id).unwrap();
253        thread::sleep(Duration::from_millis(50));
254
255        let config = SharedClientConfig {
256            instance_name: "test-shared-reg".into(),
257            port,
258            rpc_port: 0,
259        };
260
261        let node = RnsNode::connect_shared(config, Box::new(NoopCallbacks)).unwrap();
262
263        // Register a destination
264        let dest_hash = [0xAA; 16];
265        node.register_destination(dest_hash, rns_core::constants::DESTINATION_SINGLE)
266            .unwrap();
267
268        // Give time for event processing
269        thread::sleep(Duration::from_millis(100));
270
271        node.shutdown();
272    }
273
274    #[test]
275    fn shared_client_send_packet() {
276        let port = find_free_port();
277        let next_id = Arc::new(AtomicU64::new(52000));
278        let (server_tx, server_rx) = mpsc::channel();
279
280        let server_config = LocalServerConfig {
281            instance_name: "test-shared-send".into(),
282            port,
283            interface_id: rns_core::transport::types::InterfaceId(97),
284        };
285
286        crate::interface::local::start_server(server_config, server_tx, next_id).unwrap();
287        thread::sleep(Duration::from_millis(50));
288
289        let config = SharedClientConfig {
290            instance_name: "test-shared-send".into(),
291            port,
292            rpc_port: 0,
293        };
294
295        let node = RnsNode::connect_shared(config, Box::new(NoopCallbacks)).unwrap();
296
297        // Build a minimal packet and send it
298        let raw = vec![0x00, 0x00, 0xAA, 0xBB, 0xCC, 0xDD]; // minimal raw packet
299        node.send_raw(raw, rns_core::constants::DESTINATION_PLAIN, None)
300            .unwrap();
301
302        // Server should receive a Frame event from the client
303        // (the packet will be HDLC-framed over the local connection)
304        let mut saw_frame = false;
305        for _ in 0..10 {
306            match server_rx.recv_timeout(Duration::from_secs(1)) {
307                Ok(crate::event::Event::Frame { .. }) => {
308                    saw_frame = true;
309                    break;
310                }
311                Ok(_) => continue,
312                Err(_) => break,
313            }
314        }
315        // The packet may or may not arrive as a Frame depending on transport
316        // routing, so we don't assert on it — the important thing is no crash.
317
318        node.shutdown();
319    }
320
321    #[test]
322    fn connect_shared_fails_no_server() {
323        let port = find_free_port();
324
325        let config = SharedClientConfig {
326            instance_name: "nonexistent-instance-12345".into(),
327            port,
328            rpc_port: 0,
329        };
330
331        // Should fail because no server is running
332        let result = RnsNode::connect_shared(config, Box::new(NoopCallbacks));
333        assert!(result.is_err());
334    }
335
336    #[test]
337    fn shared_config_defaults() {
338        let config = SharedClientConfig::default();
339        assert_eq!(config.instance_name, "default");
340        assert_eq!(config.port, 37428);
341        assert_eq!(config.rpc_port, 37429);
342    }
343}