1use 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
28pub struct SharedClientConfig {
30 pub instance_name: String,
32 pub port: u16,
34 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 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 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 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 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 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 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 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 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 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 let dest_hash = [0xAA; 16];
247 node.register_destination(
248 dest_hash,
249 rns_core::constants::DESTINATION_SINGLE,
250 )
251 .unwrap();
252
253 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 let raw = vec![0x00, 0x00, 0xAA, 0xBB, 0xCC, 0xDD]; node.send_raw(raw, rns_core::constants::DESTINATION_PLAIN, None)
285 .unwrap();
286
287 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 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 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}