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