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 };
64
65 let (tx, rx) = event::channel();
66 let mut driver = Driver::new(transport_config, rx, tx.clone(), callbacks);
67
68 let local_config = LocalClientConfig {
70 name: "Local shared instance".into(),
71 instance_name: config.instance_name.clone(),
72 port: config.port,
73 interface_id: rns_core::transport::types::InterfaceId(1),
74 reconnect_wait: Duration::from_secs(8),
75 };
76
77 let id = local_config.interface_id;
78 let info = rns_core::transport::types::InterfaceInfo {
79 id,
80 name: "LocalInterface".into(),
81 mode: rns_core::constants::MODE_FULL,
82 out_capable: true,
83 in_capable: true,
84 bitrate: Some(1_000_000_000),
85 announce_rate_target: None,
86 announce_rate_grace: 0,
87 announce_rate_penalty: 0.0,
88 announce_cap: rns_core::constants::ANNOUNCE_CAP,
89 is_local_client: true,
90 wants_tunnel: false,
91 tunnel_id: None,
92 mtu: 65535,
93 ia_freq: 0.0,
94 started: time::now(),
95 ingress_control: false,
96 };
97
98 let writer = crate::interface::local::start_client(local_config, tx.clone())?;
99
100 driver.engine.register_interface(info.clone());
101 driver.interfaces.insert(
102 id,
103 InterfaceEntry {
104 id,
105 info,
106 writer,
107 online: false,
108 dynamic: false,
109 ifac: None,
110 stats: InterfaceStats {
111 started: time::now(),
112 ..Default::default()
113 },
114 interface_type: "LocalClientInterface".to_string(),
115 },
116 );
117
118 let tick_interval_ms = Arc::new(AtomicU64::new(1000));
120 let timer_tx = tx.clone();
121 let timer_interval = Arc::clone(&tick_interval_ms);
122 thread::Builder::new()
123 .name("rns-timer-client".into())
124 .spawn(move || {
125 loop {
126 let ms = timer_interval.load(Ordering::Relaxed);
127 thread::sleep(Duration::from_millis(ms));
128 if timer_tx.send(event::Event::Tick).is_err() {
129 break;
130 }
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(tx, driver_handle, None, tick_interval_ms))
142 }
143
144 pub fn connect_shared_from_config(
148 config_path: Option<&Path>,
149 callbacks: Box<dyn Callbacks>,
150 ) -> io::Result<Self> {
151 let config_dir = storage::resolve_config_dir(config_path);
152
153 let config_file = config_dir.join("config");
155 let rns_config = if config_file.exists() {
156 crate::config::parse_file(&config_file).map_err(|e| {
157 io::Error::new(io::ErrorKind::InvalidData, format!("{}", e))
158 })?
159 } else {
160 crate::config::parse("").map_err(|e| {
161 io::Error::new(io::ErrorKind::InvalidData, format!("{}", e))
162 })?
163 };
164
165 let shared_config = SharedClientConfig {
166 instance_name: rns_config.reticulum.instance_name.clone(),
167 port: rns_config.reticulum.shared_instance_port,
168 rpc_port: rns_config.reticulum.instance_control_port,
169 };
170
171 Self::connect_shared(shared_config, callbacks)
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use std::sync::atomic::{AtomicU64, Ordering};
179 use std::sync::mpsc;
180 use std::sync::Arc;
181
182 use crate::interface::local::LocalServerConfig;
183
184 struct NoopCallbacks;
185 impl Callbacks for NoopCallbacks {
186 fn on_announce(&mut self, _: crate::destination::AnnouncedIdentity) {}
187 fn on_path_updated(&mut self, _: rns_core::types::DestHash, _: u8) {}
188 fn on_local_delivery(&mut self, _: rns_core::types::DestHash, _: Vec<u8>, _: rns_core::types::PacketHash) {}
189 }
190
191 fn find_free_port() -> u16 {
192 std::net::TcpListener::bind("127.0.0.1:0")
193 .unwrap()
194 .local_addr()
195 .unwrap()
196 .port()
197 }
198
199 #[test]
200 fn connect_shared_to_tcp_server() {
201 let port = find_free_port();
202 let next_id = Arc::new(AtomicU64::new(50000));
203 let (server_tx, server_rx) = mpsc::channel();
204
205 let server_config = LocalServerConfig {
207 instance_name: "test-shared-connect".into(),
208 port,
209 interface_id: rns_core::transport::types::InterfaceId(99),
210 };
211
212 crate::interface::local::start_server(server_config, server_tx, next_id).unwrap();
213 thread::sleep(Duration::from_millis(50));
214
215 let config = SharedClientConfig {
217 instance_name: "test-shared-connect".into(),
218 port,
219 rpc_port: 0,
220 };
221
222 let node = RnsNode::connect_shared(config, Box::new(NoopCallbacks)).unwrap();
223
224 let event = server_rx.recv_timeout(Duration::from_secs(2)).unwrap();
226 assert!(matches!(event, crate::event::Event::InterfaceUp(_, _, _)));
227
228 node.shutdown();
229 }
230
231 #[test]
232 fn shared_client_register_destination() {
233 let port = find_free_port();
234 let next_id = Arc::new(AtomicU64::new(51000));
235 let (server_tx, _server_rx) = mpsc::channel();
236
237 let server_config = LocalServerConfig {
238 instance_name: "test-shared-reg".into(),
239 port,
240 interface_id: rns_core::transport::types::InterfaceId(98),
241 };
242
243 crate::interface::local::start_server(server_config, server_tx, next_id).unwrap();
244 thread::sleep(Duration::from_millis(50));
245
246 let config = SharedClientConfig {
247 instance_name: "test-shared-reg".into(),
248 port,
249 rpc_port: 0,
250 };
251
252 let node = RnsNode::connect_shared(config, Box::new(NoopCallbacks)).unwrap();
253
254 let dest_hash = [0xAA; 16];
256 node.register_destination(
257 dest_hash,
258 rns_core::constants::DESTINATION_SINGLE,
259 )
260 .unwrap();
261
262 thread::sleep(Duration::from_millis(100));
264
265 node.shutdown();
266 }
267
268 #[test]
269 fn shared_client_send_packet() {
270 let port = find_free_port();
271 let next_id = Arc::new(AtomicU64::new(52000));
272 let (server_tx, server_rx) = mpsc::channel();
273
274 let server_config = LocalServerConfig {
275 instance_name: "test-shared-send".into(),
276 port,
277 interface_id: rns_core::transport::types::InterfaceId(97),
278 };
279
280 crate::interface::local::start_server(server_config, server_tx, next_id).unwrap();
281 thread::sleep(Duration::from_millis(50));
282
283 let config = SharedClientConfig {
284 instance_name: "test-shared-send".into(),
285 port,
286 rpc_port: 0,
287 };
288
289 let node = RnsNode::connect_shared(config, Box::new(NoopCallbacks)).unwrap();
290
291 let raw = vec![0x00, 0x00, 0xAA, 0xBB, 0xCC, 0xDD]; node.send_raw(raw, rns_core::constants::DESTINATION_PLAIN, None)
294 .unwrap();
295
296 let mut saw_frame = false;
299 for _ in 0..10 {
300 match server_rx.recv_timeout(Duration::from_secs(1)) {
301 Ok(crate::event::Event::Frame { .. }) => {
302 saw_frame = true;
303 break;
304 }
305 Ok(_) => continue,
306 Err(_) => break,
307 }
308 }
309 node.shutdown();
313 }
314
315 #[test]
316 fn connect_shared_fails_no_server() {
317 let port = find_free_port();
318
319 let config = SharedClientConfig {
320 instance_name: "nonexistent-instance-12345".into(),
321 port,
322 rpc_port: 0,
323 };
324
325 let result = RnsNode::connect_shared(config, Box::new(NoopCallbacks));
327 assert!(result.is_err());
328 }
329
330 #[test]
331 fn shared_config_defaults() {
332 let config = SharedClientConfig::default();
333 assert_eq!(config.instance_name, "default");
334 assert_eq!(config.port, 37428);
335 assert_eq!(config.rpc_port, 37429);
336 }
337}