1use 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
30pub struct SharedClientConfig {
32 pub instance_name: String,
34 pub port: u16,
36 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 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 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 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 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 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 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 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 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 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 let dest_hash = [0xAA; 16];
265 node.register_destination(dest_hash, rns_core::constants::DESTINATION_SINGLE)
266 .unwrap();
267
268 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 let raw = vec![0x00, 0x00, 0xAA, 0xBB, 0xCC, 0xDD]; node.send_raw(raw, rns_core::constants::DESTINATION_PLAIN, None)
300 .unwrap();
301
302 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 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 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}