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::io::Read;
15use std::path::Path;
16use std::sync::atomic::{AtomicU64, Ordering};
17use std::sync::Arc;
18use std::thread;
19use std::time::Duration;
20
21use rns_core::destination::{destination_hash, name_hash};
22use rns_core::packet::RawPacket;
23use rns_core::transport::types::TransportConfig;
24use rns_crypto::identity::Identity;
25
26use crate::driver::{Callbacks, Driver};
27use crate::event;
28use crate::event::Event;
29use crate::interface::local::LocalClientConfig;
30use crate::interface::{InterfaceEntry, InterfaceStats};
31use crate::node::RnsNode;
32use crate::storage;
33use crate::time;
34
35/// Configuration for connecting as a shared instance client.
36pub struct SharedClientConfig {
37    /// Instance name for Unix socket namespace (e.g. "default" → `\0rns/default`).
38    pub instance_name: String,
39    /// TCP port to try if Unix socket fails (default 37428).
40    pub port: u16,
41    /// RPC control port for queries (default 37429).
42    pub rpc_port: u16,
43}
44
45impl Default for SharedClientConfig {
46    fn default() -> Self {
47        SharedClientConfig {
48            instance_name: "default".into(),
49            port: 37428,
50            rpc_port: 37429,
51        }
52    }
53}
54
55impl RnsNode {
56    /// Connect to an existing shared instance as a client.
57    ///
58    /// The client runs `transport_enabled: false` — it does no routing,
59    /// but can register destinations and send/receive packets through
60    /// the daemon.
61    pub fn connect_shared(
62        config: SharedClientConfig,
63        callbacks: Box<dyn Callbacks>,
64    ) -> io::Result<Self> {
65        Self::connect_shared_with_reconnect_wait(config, callbacks, Duration::from_secs(8))
66    }
67
68    fn connect_shared_with_reconnect_wait(
69        config: SharedClientConfig,
70        callbacks: Box<dyn Callbacks>,
71        reconnect_wait: Duration,
72    ) -> io::Result<Self> {
73        let transport_config = TransportConfig {
74            transport_enabled: false,
75            identity_hash: None,
76            local_hops_delta: 0,
77            prefer_shorter_path: false,
78            max_paths_per_destination: 1,
79            packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
80            packet_hashlist_allocation: rns_core::transport::types::PacketHashlistAllocation::Eager,
81            max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
82            max_path_destinations: rns_core::transport::types::DEFAULT_MAX_PATH_DESTINATIONS,
83            max_tunnel_destinations_total: usize::MAX,
84            destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
85            announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
86            announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
87            announce_sig_cache_enabled: true,
88            announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
89            announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
90            announce_queue_max_entries: 256,
91            announce_queue_max_interfaces: 1024,
92        };
93
94        let (tx, rx) = event::channel();
95        let tick_interval_ms = Arc::new(AtomicU64::new(1000));
96        let mut driver = Driver::new(transport_config, rx, tx.clone(), callbacks);
97        driver.set_tick_interval_handle(Arc::clone(&tick_interval_ms));
98
99        // Connect to the daemon via LocalClientInterface
100        let local_config = LocalClientConfig {
101            name: "Local shared instance".into(),
102            instance_name: config.instance_name.clone(),
103            port: config.port,
104            interface_id: rns_core::transport::types::InterfaceId(1),
105            reconnect_wait,
106        };
107
108        let id = local_config.interface_id;
109        let info = rns_core::transport::types::InterfaceInfo {
110            id,
111            name: "LocalInterface".into(),
112            mode: rns_core::constants::MODE_FULL,
113            gravity: 0,
114            recursive_prs: false,
115            announces_from_internal: true,
116            announces_to_internal: None,
117            out_capable: true,
118            in_capable: true,
119            bitrate: Some(crate::interface::local::BITRATE),
120            airtime_profile: None,
121            announce_rate_target: None,
122            announce_rate_grace: 0,
123            announce_rate_penalty: 0.0,
124            announce_cap: rns_core::constants::ANNOUNCE_CAP,
125            is_local_client: true,
126            wants_tunnel: false,
127            tunnel_id: None,
128            mtu: crate::interface::local::HW_MTU,
129            ia_freq: 0.0,
130            ip_freq: 0.0,
131            op_freq: 0.0,
132            op_samples: 0,
133            started: time::now(),
134            ingress_control: rns_core::transport::types::IngressControlConfig::disabled(),
135        };
136
137        let writer = crate::interface::local::start_client(local_config, tx.clone())?;
138        let (writer, async_writer_metrics) = crate::interface::wrap_async_writer(
139            writer,
140            id,
141            &info.name,
142            tx.clone(),
143            driver.interface_writer_queue_capacity,
144        );
145
146        driver.engine.register_interface(info.clone());
147        driver.interfaces.insert(
148            id,
149            InterfaceEntry {
150                id,
151                info,
152                writer,
153                async_writer_metrics: Some(async_writer_metrics),
154                enabled: true,
155                online: false,
156                dynamic: false,
157                ifac: None,
158                stats: InterfaceStats {
159                    started: time::now(),
160                    ..Default::default()
161                },
162                interface_type: "LocalClientInterface".to_string(),
163                send_retry_at: None,
164                send_retry_backoff: Duration::ZERO,
165            },
166        );
167
168        // Spawn timer thread with configurable tick interval
169        let timer_tx = tx.clone();
170        let timer_interval = Arc::clone(&tick_interval_ms);
171        thread::Builder::new()
172            .name("rns-timer-client".into())
173            .spawn(move || loop {
174                let ms = timer_interval.load(Ordering::Relaxed);
175                thread::sleep(Duration::from_millis(ms));
176                if timer_tx.send(event::Event::Tick).is_err() {
177                    break;
178                }
179            })?;
180
181        // Spawn driver thread
182        let driver_handle = thread::Builder::new()
183            .name("rns-driver-client".into())
184            .spawn(move || {
185                driver.run();
186            })?;
187
188        Ok(RnsNode::from_parts(
189            tx,
190            driver_handle,
191            None,
192            tick_interval_ms,
193        ))
194    }
195
196    /// Connect to a shared instance, with config loaded from a config directory.
197    ///
198    /// Reads the config file to determine instance_name and ports.
199    pub fn connect_shared_from_config(
200        config_path: Option<&Path>,
201        callbacks: Box<dyn Callbacks>,
202    ) -> io::Result<Self> {
203        let config_dir = storage::resolve_config_dir(config_path);
204
205        // Parse config file for instance settings
206        let config_file = config_dir.join("config");
207        let rns_config = if config_file.exists() {
208            crate::config::parse_file(&config_file)
209                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("{}", e)))?
210        } else {
211            crate::config::parse("")
212                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("{}", e)))?
213        };
214
215        let shared_config = SharedClientConfig {
216            instance_name: rns_config.reticulum.instance_name.clone(),
217            port: rns_config.reticulum.shared_instance_port,
218            rpc_port: rns_config.reticulum.instance_control_port,
219        };
220
221        Self::connect_shared(shared_config, callbacks)
222    }
223}
224
225#[doc(hidden)]
226pub fn bench_shared_client_replay_once(
227    announce_count: usize,
228    reconnect_wait: Duration,
229) -> io::Result<usize> {
230    struct BenchNoopCallbacks;
231    impl Callbacks for BenchNoopCallbacks {
232        fn on_announce(&mut self, _: crate::destination::AnnouncedIdentity) {}
233        fn on_path_updated(&mut self, _: rns_core::types::DestHash, _: u8) {}
234        fn on_local_delivery(
235            &mut self,
236            _: rns_core::types::DestHash,
237            _: Vec<u8>,
238            _: rns_core::types::PacketHash,
239        ) {
240        }
241    }
242
243    fn build_shared_announce_raw(
244        dest_hash: &[u8; 16],
245        name_hash: &[u8; 10],
246        identity_prv_key: &[u8; 64],
247        app_data: Option<&[u8]>,
248        path_response: bool,
249    ) -> Vec<u8> {
250        let identity = Identity::from_private_key(identity_prv_key);
251        let mut random_hash = [0u8; 10];
252        random_hash[..5].copy_from_slice(&[0xA5; 5]);
253        random_hash[5..10].copy_from_slice(&[0, 0, 0, 0, 1]);
254
255        let (announce_data, _) = rns_core::announce::AnnounceData::pack(
256            &identity,
257            dest_hash,
258            name_hash,
259            &random_hash,
260            None,
261            app_data,
262        )
263        .unwrap();
264
265        let flags = rns_core::packet::PacketFlags {
266            header_type: rns_core::constants::HEADER_1,
267            context_flag: rns_core::constants::FLAG_UNSET,
268            transport_type: rns_core::constants::TRANSPORT_BROADCAST,
269            destination_type: rns_core::constants::DESTINATION_SINGLE,
270            packet_type: rns_core::constants::PACKET_TYPE_ANNOUNCE,
271        };
272        let context = if path_response {
273            rns_core::constants::CONTEXT_PATH_RESPONSE
274        } else {
275            rns_core::constants::CONTEXT_NONE
276        };
277
278        rns_core::packet::RawPacket::pack(flags, 0, dest_hash, None, context, &announce_data)
279            .unwrap()
280            .raw
281    }
282
283    fn read_until_frames(
284        stream: &mut std::net::TcpStream,
285        expected: usize,
286        expected_context: u8,
287    ) -> io::Result<Vec<Vec<u8>>> {
288        let mut decoder = crate::interface::local::frame_decoder(0);
289        let mut buf = [0u8; 4096];
290        let mut frames = Vec::new();
291        let deadline = std::time::Instant::now() + Duration::from_secs(2);
292        while frames.len() < expected {
293            let n = match stream.read(&mut buf) {
294                Ok(n) => n,
295                Err(e)
296                    if e.kind() == io::ErrorKind::WouldBlock
297                        || e.kind() == io::ErrorKind::TimedOut =>
298                {
299                    if std::time::Instant::now() >= deadline {
300                        return Err(io::Error::new(
301                            io::ErrorKind::TimedOut,
302                            format!(
303                                "timed out waiting for {} frames, got {}",
304                                expected,
305                                frames.len()
306                            ),
307                        ));
308                    }
309                    continue;
310                }
311                Err(e) => return Err(e),
312            };
313            for frame in decoder.feed(&buf[..n]) {
314                let Ok(packet) = RawPacket::unpack(&frame) else {
315                    log::debug!("skipping malformed shared-instance packet");
316                    continue;
317                };
318                if packet.context == expected_context {
319                    frames.push(frame);
320                }
321            }
322        }
323        Ok(frames)
324    }
325
326    let listener1 = std::net::TcpListener::bind("127.0.0.1:0")?;
327    let port = listener1.local_addr()?.port();
328    let instance_name = format!("bench-shared-replay-{port}");
329
330    let (accepted1_tx, accepted1_rx) = std::sync::mpsc::channel();
331    thread::spawn(move || {
332        let (stream, _) = listener1.accept().unwrap();
333        accepted1_tx.send(stream).unwrap();
334    });
335
336    let transport_config = TransportConfig {
337        transport_enabled: false,
338        identity_hash: None,
339        local_hops_delta: 0,
340        prefer_shorter_path: false,
341        max_paths_per_destination: 1,
342        packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
343        packet_hashlist_allocation: rns_core::transport::types::PacketHashlistAllocation::Eager,
344        max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
345        max_path_destinations: rns_core::transport::types::DEFAULT_MAX_PATH_DESTINATIONS,
346        max_tunnel_destinations_total: usize::MAX,
347        destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
348        announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
349        announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
350        announce_sig_cache_enabled: true,
351        announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
352        announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
353        announce_queue_max_entries: 256,
354        announce_queue_max_interfaces: 1024,
355    };
356
357    let (tx, rx) = event::channel();
358    let tick_interval_ms = Arc::new(AtomicU64::new(1000));
359    let mut driver = Driver::new(
360        transport_config,
361        rx,
362        tx.clone(),
363        Box::new(BenchNoopCallbacks),
364    );
365    driver.set_tick_interval_handle(Arc::clone(&tick_interval_ms));
366
367    let local_config = LocalClientConfig {
368        name: "Shared replay bench".into(),
369        instance_name: instance_name.clone(),
370        port,
371        interface_id: rns_core::transport::types::InterfaceId(1),
372        reconnect_wait,
373    };
374
375    let id = local_config.interface_id;
376    let info = rns_core::transport::types::InterfaceInfo {
377        id,
378        name: "LocalInterface".into(),
379        mode: rns_core::constants::MODE_FULL,
380        gravity: 0,
381        recursive_prs: false,
382        announces_from_internal: true,
383        announces_to_internal: None,
384        out_capable: true,
385        in_capable: true,
386        bitrate: Some(crate::interface::local::BITRATE),
387        airtime_profile: None,
388        announce_rate_target: None,
389        announce_rate_grace: 0,
390        announce_rate_penalty: 0.0,
391        announce_cap: rns_core::constants::ANNOUNCE_CAP,
392        is_local_client: true,
393        wants_tunnel: false,
394        tunnel_id: None,
395        mtu: crate::interface::local::HW_MTU,
396        ia_freq: 0.0,
397        ip_freq: 0.0,
398        op_freq: 0.0,
399        op_samples: 0,
400        started: time::now(),
401        ingress_control: rns_core::transport::types::IngressControlConfig::disabled(),
402    };
403
404    let writer = crate::interface::local::start_client(local_config, tx.clone())?;
405    let (writer, async_writer_metrics) = crate::interface::wrap_async_writer(
406        writer,
407        id,
408        &info.name,
409        tx.clone(),
410        driver.interface_writer_queue_capacity,
411    );
412    driver.engine.register_interface(info.clone());
413    driver.interfaces.insert(
414        id,
415        InterfaceEntry {
416            id,
417            info,
418            writer,
419            async_writer_metrics: Some(async_writer_metrics),
420            enabled: true,
421            online: false,
422            dynamic: false,
423            ifac: None,
424            stats: InterfaceStats {
425                started: time::now(),
426                ..Default::default()
427            },
428            interface_type: "LocalClientInterface".to_string(),
429            send_retry_at: None,
430            send_retry_backoff: Duration::ZERO,
431        },
432    );
433
434    let driver_handle = thread::Builder::new()
435        .name("rns-driver-bench-client".into())
436        .spawn(move || {
437            driver.run();
438        })?;
439
440    let mut stream1 = accepted1_rx
441        .recv_timeout(Duration::from_secs(2))
442        .map_err(|e| {
443            io::Error::new(
444                io::ErrorKind::TimedOut,
445                format!("shared bench initial accept failed: {e}"),
446            )
447        })?;
448    stream1.set_read_timeout(Some(Duration::from_secs(2)))?;
449
450    let mut records = Vec::new();
451    for i in 0..announce_count {
452        let mut prv_key = [0u8; 64];
453        for (j, byte) in prv_key.iter_mut().enumerate() {
454            *byte = (i as u8)
455                .wrapping_mul(23)
456                .wrapping_add(j as u8)
457                .wrapping_add(5);
458        }
459        let identity = Identity::from_private_key(&prv_key);
460        let aspect = format!("echo-{i}");
461        let name_hash = name_hash("shared-bench", &[&aspect]);
462        let dest_hash = destination_hash("shared-bench", &[&aspect], Some(identity.hash()));
463        let app_data = format!("hello-{i}").into_bytes();
464        records.push((dest_hash, name_hash, prv_key, app_data));
465    }
466
467    for (dest_hash, name_hash, prv_key, app_data) in &records {
468        let raw = build_shared_announce_raw(dest_hash, name_hash, prv_key, Some(app_data), false);
469        tx.send(Event::RegisterDestination {
470            dest_hash: *dest_hash,
471            dest_type: rns_core::constants::DESTINATION_SINGLE,
472        })
473        .map_err(|e| io::Error::new(io::ErrorKind::BrokenPipe, format!("{e}")))?;
474        tx.send(Event::StoreSharedAnnounce {
475            dest_hash: *dest_hash,
476            name_hash: *name_hash,
477            identity_prv_key: *prv_key,
478            app_data: Some(app_data.clone()),
479        })
480        .map_err(|e| io::Error::new(io::ErrorKind::BrokenPipe, format!("{e}")))?;
481        tx.send(Event::SendOutbound {
482            raw,
483            dest_type: rns_core::constants::DESTINATION_SINGLE,
484            attached_interface: None,
485        })
486        .map_err(|e| io::Error::new(io::ErrorKind::BrokenPipe, format!("{e}")))?;
487    }
488
489    let _ = read_until_frames(
490        &mut stream1,
491        announce_count,
492        rns_core::constants::CONTEXT_NONE,
493    )
494    .map_err(|e| io::Error::new(e.kind(), format!("initial announce delivery failed: {e}")))?;
495    drop(stream1);
496
497    let listener2 = std::net::TcpListener::bind(format!("127.0.0.1:{port}"))?;
498    let (accepted2_tx, accepted2_rx) = std::sync::mpsc::channel();
499    thread::spawn(move || {
500        let (stream, _) = listener2.accept().unwrap();
501        accepted2_tx.send(stream).unwrap();
502    });
503
504    let mut stream2 = accepted2_rx
505        .recv_timeout(Duration::from_secs(2))
506        .map_err(|e| {
507            io::Error::new(
508                io::ErrorKind::TimedOut,
509                format!("shared bench reconnect accept failed: {e}"),
510            )
511        })?;
512    stream2.set_read_timeout(Some(Duration::from_secs(2)))?;
513    let frames = read_until_frames(
514        &mut stream2,
515        announce_count,
516        rns_core::constants::CONTEXT_PATH_RESPONSE,
517    )
518    .map_err(|e| io::Error::new(e.kind(), format!("replayed announce delivery failed: {e}")))?;
519
520    tx.send(Event::Shutdown)
521        .map_err(|e| io::Error::new(io::ErrorKind::BrokenPipe, format!("{e}")))?;
522    let _ = driver_handle.join();
523
524    Ok(frames.len())
525}
526
527#[cfg(test)]
528mod tests {
529    use super::*;
530    use rns_core::packet::RawPacket;
531    use rns_core::types::IdentityHash;
532    use rns_crypto::identity::Identity;
533    use rns_crypto::OsRng;
534    use std::io::Read;
535    use std::sync::atomic::AtomicU64;
536    use std::sync::mpsc;
537    use std::sync::Arc;
538
539    use crate::interface::local::LocalServerConfig;
540
541    struct NoopCallbacks;
542    impl Callbacks for NoopCallbacks {
543        fn on_announce(&mut self, _: crate::destination::AnnouncedIdentity) {}
544        fn on_path_updated(&mut self, _: rns_core::types::DestHash, _: u8) {}
545        fn on_local_delivery(
546            &mut self,
547            _: rns_core::types::DestHash,
548            _: Vec<u8>,
549            _: rns_core::types::PacketHash,
550        ) {
551        }
552    }
553
554    fn find_free_port() -> u16 {
555        crate::test_support::port()
556    }
557
558    #[test]
559    fn connect_shared_to_tcp_server() {
560        let port = find_free_port();
561        let next_id = Arc::new(AtomicU64::new(50000));
562        let (server_tx, server_rx) = crate::event::channel();
563
564        // Start a local server
565        let server_config = LocalServerConfig {
566            instance_name: "test-shared-connect".into(),
567            port,
568            interface_id: rns_core::transport::types::InterfaceId(99),
569        };
570
571        crate::interface::local::start_server(server_config, server_tx, next_id).unwrap();
572        thread::sleep(Duration::from_millis(50));
573
574        // Connect as shared client
575        let config = SharedClientConfig {
576            instance_name: "test-shared-connect".into(),
577            port,
578            rpc_port: 0,
579        };
580
581        let node = RnsNode::connect_shared(config, Box::new(NoopCallbacks)).unwrap();
582
583        // Server should see InterfaceUp for the client
584        let event = server_rx.recv_timeout(Duration::from_secs(2)).unwrap();
585        assert!(matches!(event, crate::event::Event::InterfaceUp(_, _, _)));
586
587        node.shutdown();
588    }
589
590    #[test]
591    fn shared_client_register_destination() {
592        let port = find_free_port();
593        let next_id = Arc::new(AtomicU64::new(51000));
594        let (server_tx, _server_rx) = crate::event::channel();
595
596        let server_config = LocalServerConfig {
597            instance_name: "test-shared-reg".into(),
598            port,
599            interface_id: rns_core::transport::types::InterfaceId(98),
600        };
601
602        crate::interface::local::start_server(server_config, server_tx, next_id).unwrap();
603        thread::sleep(Duration::from_millis(50));
604
605        let config = SharedClientConfig {
606            instance_name: "test-shared-reg".into(),
607            port,
608            rpc_port: 0,
609        };
610
611        let node = RnsNode::connect_shared(config, Box::new(NoopCallbacks)).unwrap();
612
613        // Register a destination
614        let dest_hash = [0xAA; 16];
615        node.register_destination(dest_hash, rns_core::constants::DESTINATION_SINGLE)
616            .unwrap();
617
618        // Give time for event processing
619        thread::sleep(Duration::from_millis(100));
620
621        node.shutdown();
622    }
623
624    #[test]
625    fn shared_client_send_packet() {
626        let port = find_free_port();
627        let next_id = Arc::new(AtomicU64::new(52000));
628        let (server_tx, server_rx) = crate::event::channel();
629
630        let server_config = LocalServerConfig {
631            instance_name: "test-shared-send".into(),
632            port,
633            interface_id: rns_core::transport::types::InterfaceId(97),
634        };
635
636        crate::interface::local::start_server(server_config, server_tx, next_id).unwrap();
637        thread::sleep(Duration::from_millis(50));
638
639        let config = SharedClientConfig {
640            instance_name: "test-shared-send".into(),
641            port,
642            rpc_port: 0,
643        };
644
645        let node = RnsNode::connect_shared(config, Box::new(NoopCallbacks)).unwrap();
646
647        // Build a minimal packet and send it
648        let raw = vec![0x00, 0x00, 0xAA, 0xBB, 0xCC, 0xDD]; // minimal raw packet
649        node.send_raw(raw, rns_core::constants::DESTINATION_PLAIN, None)
650            .unwrap();
651
652        // Server should receive a Frame event from the client
653        // (the packet will be HDLC-framed over the local connection)
654        for _ in 0..10 {
655            match server_rx.recv_timeout(Duration::from_secs(1)) {
656                Ok(crate::event::Event::Frame { .. }) => {
657                    break;
658                }
659                Ok(_) => continue,
660                Err(_) => break,
661            }
662        }
663        // The packet may or may not arrive as a Frame depending on transport
664        // routing, so we don't assert on it — the important thing is no crash.
665
666        node.shutdown();
667    }
668
669    #[test]
670    fn shared_client_replays_single_announces_after_reconnect() {
671        let port = find_free_port();
672        let addr = format!("127.0.0.1:{}", port);
673        let instance_name = format!("test-shared-replay-{}", port);
674
675        let listener1 = std::net::TcpListener::bind(&addr).unwrap();
676        let (accepted1_tx, accepted1_rx) = mpsc::channel();
677        thread::spawn(move || {
678            let (stream, _) = listener1.accept().unwrap();
679            accepted1_tx.send(stream).unwrap();
680        });
681
682        let node = RnsNode::connect_shared(
683            SharedClientConfig {
684                instance_name,
685                port,
686                rpc_port: 0,
687            },
688            Box::new(NoopCallbacks),
689        )
690        .unwrap();
691
692        let identity = Identity::new(&mut OsRng);
693        let dest = crate::destination::Destination::single_in(
694            "shared-replay",
695            &["echo"],
696            IdentityHash(*identity.hash()),
697        );
698        node.register_destination(dest.hash.0, dest.dest_type.to_wire_constant())
699            .unwrap();
700        node.announce_queued(&dest, &identity, Some(b"hello"))
701            .unwrap();
702
703        let mut stream1 = accepted1_rx.recv_timeout(Duration::from_secs(2)).unwrap();
704        stream1
705            .set_read_timeout(Some(Duration::from_secs(2)))
706            .unwrap();
707
708        let mut decoder = crate::interface::local::frame_decoder(0);
709        let mut buf = [0u8; 4096];
710        let n = stream1.read(&mut buf).unwrap();
711        let frames = decoder.feed(&buf[..n]);
712        assert!(!frames.is_empty(), "expected initial announce frame");
713        let packet1 = RawPacket::unpack(&frames[0]).unwrap();
714        assert_eq!(packet1.destination_hash, dest.hash.0);
715        assert_eq!(packet1.context, rns_core::constants::CONTEXT_NONE);
716
717        drop(stream1);
718
719        let listener2 = std::net::TcpListener::bind(&addr).unwrap();
720        let (accepted2_tx, accepted2_rx) = mpsc::channel();
721        thread::spawn(move || {
722            let (stream, _) = listener2.accept().unwrap();
723            accepted2_tx.send(stream).unwrap();
724        });
725
726        let mut stream2 = accepted2_rx.recv_timeout(Duration::from_secs(15)).unwrap();
727        stream2
728            .set_read_timeout(Some(Duration::from_secs(15)))
729            .unwrap();
730
731        let mut decoder = crate::interface::local::frame_decoder(0);
732        let n = stream2.read(&mut buf).unwrap();
733        let frames = decoder.feed(&buf[..n]);
734        assert!(!frames.is_empty(), "expected replayed announce frame");
735        let packet2 = RawPacket::unpack(&frames[0]).unwrap();
736        assert_eq!(packet2.destination_hash, dest.hash.0);
737        assert_eq!(packet2.context, rns_core::constants::CONTEXT_PATH_RESPONSE);
738
739        node.shutdown();
740    }
741
742    #[test]
743    fn connect_shared_fails_no_server() {
744        let port = find_free_port();
745
746        let config = SharedClientConfig {
747            instance_name: "nonexistent-instance-12345".into(),
748            port,
749            rpc_port: 0,
750        };
751
752        // Should fail because no server is running
753        let result = RnsNode::connect_shared(config, Box::new(NoopCallbacks));
754        assert!(result.is_err());
755    }
756
757    #[test]
758    fn shared_config_defaults() {
759        let config = SharedClientConfig::default();
760        assert_eq!(config.instance_name, "default");
761        assert_eq!(config.port, 37428);
762        assert_eq!(config.rpc_port, 37429);
763    }
764}