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