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