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