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::{AtomicBool, 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, QueryRequest, QueryResponse};
29use crate::interface::local::LocalClientConfig;
30use crate::interface::{InterfaceEntry, InterfaceStats};
31use crate::node::RnsNode;
32use crate::pickle::PickleValue;
33use crate::rpc::{RpcAddr, RpcClient};
34use crate::storage;
35use crate::time;
36
37/// Configuration for connecting as a shared instance client.
38pub struct SharedClientConfig {
39    /// Instance name for Unix socket namespace (e.g. "default" → `\0rns/default`).
40    pub instance_name: String,
41    /// TCP port to try if Unix socket fails (default 37428).
42    pub port: u16,
43    /// RPC control port for queries (default 37429).
44    pub rpc_port: u16,
45}
46
47impl Default for SharedClientConfig {
48    fn default() -> Self {
49        SharedClientConfig {
50            instance_name: "default".into(),
51            port: 37428,
52            rpc_port: 37429,
53        }
54    }
55}
56
57/// The destination types an application needs announces of (upstream
58/// 825c2e19). A shared instance with `[filter] required_only` drops
59/// announces of any other type, so a client registers what it needs there.
60pub(crate) struct DestinationTypeRequester {
61    /// `Transport.application_identity.hash`: identifies this application's
62    /// registrations, so they can be withdrawn together.
63    requester: [u8; 16],
64    /// Whether this node is a shared instance client.
65    shared_client: bool,
66    /// Address and key of the shared instance's RPC server, when known.
67    shared_rpc: Option<(RpcAddr, [u8; 32])>,
68    /// Whether a type was registered with the shared instance, so the
69    /// registrations are withdrawn on shutdown.
70    registered: AtomicBool,
71}
72
73impl Default for DestinationTypeRequester {
74    fn default() -> Self {
75        DestinationTypeRequester {
76            requester: *Identity::new(&mut rns_crypto::OsRng).hash(),
77            shared_client: false,
78            shared_rpc: None,
79            registered: AtomicBool::new(false),
80        }
81    }
82}
83
84impl DestinationTypeRequester {
85    /// Send one destination type call to the shared instance. A failed call
86    /// or one the instance does not understand reports `false`.
87    fn call_shared_instance(&self, key: &str, value: PickleValue) -> bool {
88        let Some((addr, auth_key)) = &self.shared_rpc else {
89            return false;
90        };
91        let request = PickleValue::Dict(vec![
92            (PickleValue::String(key.into()), value),
93            (
94                PickleValue::String("requester".into()),
95                PickleValue::Bytes(self.requester.to_vec()),
96            ),
97            (PickleValue::String("signature".into()), PickleValue::None),
98        ]);
99        match RpcClient::connect(addr, auth_key).and_then(|mut client| client.call(&request)) {
100            Ok(response) => response.as_bool() == Some(true),
101            // Debug level: a Python shared instance on Linux serves RPC on
102            // an abstract Unix socket, which this TCP client cannot reach,
103            // and registration only matters when it filters announces.
104            Err(e) => {
105                log::debug!(
106                    "Shared instance RPC failed while setting destination type filters: {e}"
107                );
108                false
109            }
110        }
111    }
112
113    /// Withdraw this application's registrations from the shared instance,
114    /// as the Python exit handler does.
115    pub(crate) fn withdraw_from_shared_instance(&self) {
116        if self.registered.swap(false, Ordering::Relaxed) {
117            self.call_shared_instance("unwants_all_destination_types", PickleValue::Bool(true));
118        }
119    }
120}
121
122impl RnsNode {
123    /// `Reticulum.wants_destination_type`: this application needs announces
124    /// of the destination type `full_name`, such as `rncp.receive`.
125    ///
126    /// The type is registered with the local transport and, on a shared
127    /// instance client, with the shared instance, whose answer is returned.
128    /// Returns `false` for a blocked type.
129    pub fn wants_destination_type(&self, full_name: &str) -> bool {
130        let local = self.destination_type_query(QueryRequest::WantsDestinationType {
131            full_name: full_name.into(),
132            requester: self.destination_types.requester.to_vec(),
133        });
134        if !self.destination_types.shared_client || self.destination_types.shared_rpc.is_none() {
135            return local;
136        }
137        let registered = self.destination_types.call_shared_instance(
138            "wants_destination_type",
139            PickleValue::String(full_name.into()),
140        );
141        if registered {
142            self.destination_types
143                .registered
144                .store(true, Ordering::Relaxed);
145        }
146        registered
147    }
148
149    /// `Reticulum.unwants_destination_type`: withdraw a need registered with
150    /// [`Self::wants_destination_type`]. Returns whether it was registered.
151    pub fn unwants_destination_type(&self, full_name: &str) -> bool {
152        let local = self.destination_type_query(QueryRequest::UnwantsDestinationType {
153            full_name: full_name.into(),
154            requester: self.destination_types.requester.to_vec(),
155        });
156        if !self.destination_types.shared_client || self.destination_types.shared_rpc.is_none() {
157            return local;
158        }
159        self.destination_types.call_shared_instance(
160            "unwants_destination_type",
161            PickleValue::String(full_name.into()),
162        )
163    }
164
165    /// `Reticulum.unwants_all_destination_types`: withdraw every destination
166    /// type this application registered.
167    pub fn unwants_all_destination_types(&self) -> bool {
168        let local = self.destination_type_query(QueryRequest::UnwantsAllDestinationTypes {
169            requester: self.destination_types.requester.to_vec(),
170        });
171        if !self.destination_types.shared_client || self.destination_types.shared_rpc.is_none() {
172            return local;
173        }
174        self.destination_types
175            .registered
176            .store(false, Ordering::Relaxed);
177        self.destination_types
178            .call_shared_instance("unwants_all_destination_types", PickleValue::Bool(true))
179    }
180
181    /// `Reticulum.required_announces_only`: on a shared instance client, let
182    /// through only announces of the types this application registered and
183    /// the shared instance's own destinations. Other nodes return `false`,
184    /// so an application cannot squelch announces for the node as a whole.
185    pub fn required_announces_only(&self, enabled: bool) -> bool {
186        if !self.destination_types.shared_client {
187            return false;
188        }
189        self.destination_type_query(QueryRequest::SetRequiredDestinationsOnly { enabled })
190    }
191
192    fn destination_type_query(&self, request: QueryRequest) -> bool {
193        matches!(
194            self.query(request),
195            Ok(QueryResponse::DestinationTypeRegistration(true))
196        )
197    }
198
199    /// Connect to an existing shared instance as a client.
200    ///
201    /// The client runs `transport_enabled: false` — it does no routing,
202    /// but can register destinations and send/receive packets through
203    /// the daemon.
204    pub fn connect_shared(
205        config: SharedClientConfig,
206        callbacks: Box<dyn Callbacks>,
207    ) -> io::Result<Self> {
208        Self::connect_shared_with_reconnect_wait(config, callbacks, Duration::from_secs(8))
209    }
210
211    fn connect_shared_with_reconnect_wait(
212        config: SharedClientConfig,
213        callbacks: Box<dyn Callbacks>,
214        reconnect_wait: Duration,
215    ) -> io::Result<Self> {
216        let transport_config = TransportConfig {
217            transport_enabled: false,
218            identity_hash: None,
219            local_hops_delta: 0,
220            prefer_shorter_path: false,
221            max_paths_per_destination: 1,
222            packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
223            packet_hashlist_allocation: rns_core::transport::types::PacketHashlistAllocation::Lazy,
224            max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
225            max_path_destinations: rns_core::transport::types::DEFAULT_MAX_PATH_DESTINATIONS,
226            max_tunnel_destinations_total: usize::MAX,
227            destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
228            announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
229            announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
230            announce_sig_cache_enabled: true,
231            announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
232            announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
233            announce_queue_max_entries: 256,
234            announce_queue_max_interfaces: 1024,
235        };
236
237        let (tx, rx) = event::channel();
238        let tick_interval_ms = Arc::new(AtomicU64::new(1000));
239        let mut driver = Driver::new(transport_config, rx, tx.clone(), callbacks);
240        driver.connected_to_shared_instance = true;
241        driver.set_tick_interval_handle(Arc::clone(&tick_interval_ms));
242        let accepting_work = driver.accepting_work_handle();
243
244        // Connect to the daemon via LocalClientInterface
245        let local_config = LocalClientConfig {
246            name: "Local shared instance".into(),
247            instance_name: config.instance_name.clone(),
248            port: config.port,
249            interface_id: rns_core::transport::types::InterfaceId(1),
250            reconnect_wait,
251        };
252
253        let id = local_config.interface_id;
254        let info = rns_core::transport::types::InterfaceInfo {
255            stamp_costs: Default::default(),
256            id,
257            name: "LocalInterface".into(),
258            mode: rns_core::constants::MODE_FULL,
259            gravity: 0,
260            recursive_prs: false,
261            announces_from_internal: true,
262            announces_to_internal: None,
263            out_capable: true,
264            in_capable: true,
265            bitrate: Some(crate::interface::local::BITRATE),
266            airtime_profile: None,
267            announce_rate_target: None,
268            announce_rate_grace: 0,
269            announce_rate_penalty: 0.0,
270            announce_cap: rns_core::constants::ANNOUNCE_CAP,
271            is_local_client: true,
272            wants_tunnel: false,
273            tunnel_id: None,
274            mtu: crate::interface::local::HW_MTU,
275            ia_freq: 0.0,
276            ip_freq: 0.0,
277            op_freq: 0.0,
278            op_samples: 0,
279            started: time::now(),
280            ingress_control: rns_core::transport::types::IngressControlConfig::disabled(),
281        };
282
283        let writer = crate::interface::local::start_client(local_config, tx.clone())?;
284        let (writer, async_writer_metrics) = crate::interface::wrap_async_writer(
285            writer,
286            id,
287            &info.name,
288            tx.clone(),
289            driver.interface_writer_queue_capacity,
290        );
291
292        driver.engine.register_interface(info.clone());
293        driver.interfaces.insert(
294            id,
295            InterfaceEntry {
296                id,
297                info,
298                writer,
299                async_writer_metrics: Some(async_writer_metrics),
300                enabled: true,
301                online: false,
302                dynamic: false,
303                ifac: None,
304                stats: InterfaceStats {
305                    started: time::now(),
306                    ..Default::default()
307                },
308                interface_type: "LocalClientInterface".to_string(),
309                send_retry_at: None,
310                send_retry_backoff: Duration::ZERO,
311            },
312        );
313
314        // Spawn timer thread with configurable tick interval, unless the
315        // driver keeps the tick itself (compact engine).
316        if !crate::engine::FOLD_TIMERS {
317            let timer_tx = tx.clone();
318            let timer_interval = Arc::clone(&tick_interval_ms);
319            thread::Builder::new()
320                .name("rns-timer-client".into())
321                .spawn(move || loop {
322                    let ms = timer_interval.load(Ordering::Relaxed);
323                    thread::sleep(Duration::from_millis(ms));
324                    if timer_tx.send(event::Event::Tick).is_err() {
325                        break;
326                    }
327                })?;
328        }
329
330        // Spawn driver thread
331        let driver_handle = thread::Builder::new()
332            .name("rns-driver-client".into())
333            .spawn(move || {
334                driver.run();
335            })?;
336
337        let mut node =
338            RnsNode::from_parts(tx, driver_handle, None, tick_interval_ms, accepting_work);
339        node.destination_types.shared_client = true;
340        Ok(node)
341    }
342
343    /// Connect to a shared instance, with config loaded from a config directory.
344    ///
345    /// Reads the config file to determine instance_name and ports.
346    pub fn connect_shared_from_config(
347        config_path: Option<&Path>,
348        callbacks: Box<dyn Callbacks>,
349    ) -> io::Result<Self> {
350        let config_dir = storage::resolve_config_dir(config_path);
351
352        // Parse config file for instance settings
353        let config_file = config_dir.join("config");
354        let rns_config = if config_file.exists() {
355            crate::config::parse_file(&config_file)
356                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("{}", e)))?
357        } else {
358            crate::config::parse("")
359                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("{}", e)))?
360        };
361
362        let shared_config = SharedClientConfig {
363            instance_name: rns_config.reticulum.instance_name.clone(),
364            port: rns_config.reticulum.shared_instance_port,
365            rpc_port: rns_config.reticulum.instance_control_port,
366        };
367        let rpc_port = shared_config.rpc_port;
368
369        let mut node = Self::connect_shared(shared_config, callbacks)?;
370        node.use_shared_instance_rpc(&config_dir, rpc_port);
371        Ok(node)
372    }
373
374    /// Reach the shared instance's RPC server for destination type
375    /// registrations. Its key derives from the shared instance's transport
376    /// identity, which clients read from the same storage directory; without
377    /// one, registrations stay local.
378    pub(crate) fn use_shared_instance_rpc(&mut self, config_dir: &Path, rpc_port: u16) {
379        match storage::load_identity(&config_dir.join("storage").join("transport_identity")) {
380            Ok(identity) => {
381                let auth_key =
382                    crate::rpc::derive_auth_key(&identity.get_private_key().unwrap_or([0u8; 64]));
383                self.destination_types.shared_rpc =
384                    Some((RpcAddr::Tcp("127.0.0.1".into(), rpc_port), auth_key));
385            }
386            Err(e) => log::debug!("No shared instance RPC key for destination type filters: {e}"),
387        }
388    }
389}
390
391#[doc(hidden)]
392pub fn bench_shared_client_replay_once(
393    announce_count: usize,
394    reconnect_wait: Duration,
395) -> io::Result<usize> {
396    struct BenchNoopCallbacks;
397    impl Callbacks for BenchNoopCallbacks {
398        fn on_announce(&mut self, _: crate::destination::AnnouncedIdentity) {}
399        fn on_path_updated(&mut self, _: rns_core::types::DestHash, _: u8) {}
400        fn on_local_delivery(
401            &mut self,
402            _: rns_core::types::DestHash,
403            _: Vec<u8>,
404            _: rns_core::types::PacketHash,
405        ) {
406        }
407    }
408
409    fn build_shared_announce_raw(
410        dest_hash: &[u8; 16],
411        name_hash: &[u8; 10],
412        identity_prv_key: &[u8; 64],
413        app_data: Option<&[u8]>,
414        path_response: bool,
415    ) -> Vec<u8> {
416        let identity = Identity::from_private_key(identity_prv_key);
417        let mut random_hash = [0u8; 10];
418        random_hash[..5].copy_from_slice(&[0xA5; 5]);
419        random_hash[5..10].copy_from_slice(&[0, 0, 0, 0, 1]);
420
421        let (announce_data, _) = rns_core::announce::AnnounceData::pack(
422            &identity,
423            dest_hash,
424            name_hash,
425            &random_hash,
426            None,
427            app_data,
428        )
429        .unwrap();
430
431        let flags = rns_core::packet::PacketFlags {
432            header_type: rns_core::constants::HEADER_1,
433            context_flag: rns_core::constants::FLAG_UNSET,
434            transport_type: rns_core::constants::TRANSPORT_BROADCAST,
435            destination_type: rns_core::constants::DESTINATION_SINGLE,
436            packet_type: rns_core::constants::PACKET_TYPE_ANNOUNCE,
437        };
438        let context = if path_response {
439            rns_core::constants::CONTEXT_PATH_RESPONSE
440        } else {
441            rns_core::constants::CONTEXT_NONE
442        };
443
444        rns_core::packet::RawPacket::pack(flags, 0, dest_hash, None, context, &announce_data)
445            .unwrap()
446            .raw
447    }
448
449    fn read_until_frames(
450        stream: &mut std::net::TcpStream,
451        expected: usize,
452        expected_context: u8,
453    ) -> io::Result<Vec<Vec<u8>>> {
454        let mut decoder = crate::interface::local::frame_decoder(0);
455        let mut buf = [0u8; 4096];
456        let mut frames = Vec::new();
457        let deadline = std::time::Instant::now() + Duration::from_secs(2);
458        while frames.len() < expected {
459            let n = match stream.read(&mut buf) {
460                Ok(n) => n,
461                Err(e)
462                    if e.kind() == io::ErrorKind::WouldBlock
463                        || e.kind() == io::ErrorKind::TimedOut =>
464                {
465                    if std::time::Instant::now() >= deadline {
466                        return Err(io::Error::new(
467                            io::ErrorKind::TimedOut,
468                            format!(
469                                "timed out waiting for {} frames, got {}",
470                                expected,
471                                frames.len()
472                            ),
473                        ));
474                    }
475                    continue;
476                }
477                Err(e) => return Err(e),
478            };
479            for frame in decoder.feed(&buf[..n]) {
480                let Ok(packet) = RawPacket::unpack(&frame) else {
481                    log::debug!("skipping malformed shared-instance packet");
482                    continue;
483                };
484                if packet.context == expected_context {
485                    frames.push(frame);
486                }
487            }
488        }
489        Ok(frames)
490    }
491
492    let listener1 = std::net::TcpListener::bind("127.0.0.1:0")?;
493    let port = listener1.local_addr()?.port();
494    let instance_name = format!("bench-shared-replay-{port}");
495
496    let (accepted1_tx, accepted1_rx) = std::sync::mpsc::channel();
497    thread::spawn(move || {
498        let (stream, _) = listener1.accept().unwrap();
499        accepted1_tx.send(stream).unwrap();
500    });
501
502    let transport_config = TransportConfig {
503        transport_enabled: false,
504        identity_hash: None,
505        local_hops_delta: 0,
506        prefer_shorter_path: false,
507        max_paths_per_destination: 1,
508        packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
509        packet_hashlist_allocation: rns_core::transport::types::PacketHashlistAllocation::Lazy,
510        max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
511        max_path_destinations: rns_core::transport::types::DEFAULT_MAX_PATH_DESTINATIONS,
512        max_tunnel_destinations_total: usize::MAX,
513        destination_timeout_secs: rns_core::constants::DESTINATION_TIMEOUT,
514        announce_table_ttl_secs: rns_core::constants::ANNOUNCE_TABLE_TTL,
515        announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
516        announce_sig_cache_enabled: true,
517        announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
518        announce_sig_cache_ttl_secs: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL,
519        announce_queue_max_entries: 256,
520        announce_queue_max_interfaces: 1024,
521    };
522
523    let (tx, rx) = event::channel();
524    let tick_interval_ms = Arc::new(AtomicU64::new(1000));
525    let mut driver = Driver::new(
526        transport_config,
527        rx,
528        tx.clone(),
529        Box::new(BenchNoopCallbacks),
530    );
531    driver.set_tick_interval_handle(Arc::clone(&tick_interval_ms));
532
533    let local_config = LocalClientConfig {
534        name: "Shared replay bench".into(),
535        instance_name: instance_name.clone(),
536        port,
537        interface_id: rns_core::transport::types::InterfaceId(1),
538        reconnect_wait,
539    };
540
541    let id = local_config.interface_id;
542    let info = rns_core::transport::types::InterfaceInfo {
543        stamp_costs: Default::default(),
544        id,
545        name: "LocalInterface".into(),
546        mode: rns_core::constants::MODE_FULL,
547        gravity: 0,
548        recursive_prs: false,
549        announces_from_internal: true,
550        announces_to_internal: None,
551        out_capable: true,
552        in_capable: true,
553        bitrate: Some(crate::interface::local::BITRATE),
554        airtime_profile: None,
555        announce_rate_target: None,
556        announce_rate_grace: 0,
557        announce_rate_penalty: 0.0,
558        announce_cap: rns_core::constants::ANNOUNCE_CAP,
559        is_local_client: true,
560        wants_tunnel: false,
561        tunnel_id: None,
562        mtu: crate::interface::local::HW_MTU,
563        ia_freq: 0.0,
564        ip_freq: 0.0,
565        op_freq: 0.0,
566        op_samples: 0,
567        started: time::now(),
568        ingress_control: rns_core::transport::types::IngressControlConfig::disabled(),
569    };
570
571    let writer = crate::interface::local::start_client(local_config, tx.clone())?;
572    let (writer, async_writer_metrics) = crate::interface::wrap_async_writer(
573        writer,
574        id,
575        &info.name,
576        tx.clone(),
577        driver.interface_writer_queue_capacity,
578    );
579    driver.engine.register_interface(info.clone());
580    driver.interfaces.insert(
581        id,
582        InterfaceEntry {
583            id,
584            info,
585            writer,
586            async_writer_metrics: Some(async_writer_metrics),
587            enabled: true,
588            online: false,
589            dynamic: false,
590            ifac: None,
591            stats: InterfaceStats {
592                started: time::now(),
593                ..Default::default()
594            },
595            interface_type: "LocalClientInterface".to_string(),
596            send_retry_at: None,
597            send_retry_backoff: Duration::ZERO,
598        },
599    );
600
601    let driver_handle = thread::Builder::new()
602        .name("rns-driver-bench-client".into())
603        .spawn(move || {
604            driver.run();
605        })?;
606
607    let mut stream1 = accepted1_rx
608        .recv_timeout(Duration::from_secs(2))
609        .map_err(|e| {
610            io::Error::new(
611                io::ErrorKind::TimedOut,
612                format!("shared bench initial accept failed: {e}"),
613            )
614        })?;
615    stream1.set_read_timeout(Some(Duration::from_secs(2)))?;
616
617    let mut records = Vec::new();
618    for i in 0..announce_count {
619        let mut prv_key = [0u8; 64];
620        for (j, byte) in prv_key.iter_mut().enumerate() {
621            *byte = (i as u8)
622                .wrapping_mul(23)
623                .wrapping_add(j as u8)
624                .wrapping_add(5);
625        }
626        let identity = Identity::from_private_key(&prv_key);
627        let aspect = format!("echo-{i}");
628        let name_hash = name_hash("shared-bench", &[&aspect]);
629        let dest_hash = destination_hash("shared-bench", &[&aspect], Some(identity.hash()));
630        let app_data = format!("hello-{i}").into_bytes();
631        records.push((dest_hash, name_hash, prv_key, app_data));
632    }
633
634    for (dest_hash, name_hash, prv_key, app_data) in &records {
635        let raw = build_shared_announce_raw(dest_hash, name_hash, prv_key, Some(app_data), false);
636        tx.send(Event::RegisterDestination {
637            dest_hash: *dest_hash,
638            dest_type: rns_core::constants::DESTINATION_SINGLE,
639        })
640        .map_err(|e| io::Error::new(io::ErrorKind::BrokenPipe, format!("{e}")))?;
641        tx.send(Event::StoreSharedAnnounce {
642            dest_hash: *dest_hash,
643            name_hash: *name_hash,
644            identity_prv_key: *prv_key,
645            app_data: Some(app_data.clone()),
646        })
647        .map_err(|e| io::Error::new(io::ErrorKind::BrokenPipe, format!("{e}")))?;
648        tx.send(Event::SendOutbound {
649            raw,
650            dest_type: rns_core::constants::DESTINATION_SINGLE,
651            attached_interface: None,
652        })
653        .map_err(|e| io::Error::new(io::ErrorKind::BrokenPipe, format!("{e}")))?;
654    }
655
656    let _ = read_until_frames(
657        &mut stream1,
658        announce_count,
659        rns_core::constants::CONTEXT_NONE,
660    )
661    .map_err(|e| io::Error::new(e.kind(), format!("initial announce delivery failed: {e}")))?;
662    drop(stream1);
663
664    let listener2 = std::net::TcpListener::bind(format!("127.0.0.1:{port}"))?;
665    let (accepted2_tx, accepted2_rx) = std::sync::mpsc::channel();
666    thread::spawn(move || {
667        let (stream, _) = listener2.accept().unwrap();
668        accepted2_tx.send(stream).unwrap();
669    });
670
671    let mut stream2 = accepted2_rx
672        .recv_timeout(Duration::from_secs(2))
673        .map_err(|e| {
674            io::Error::new(
675                io::ErrorKind::TimedOut,
676                format!("shared bench reconnect accept failed: {e}"),
677            )
678        })?;
679    stream2.set_read_timeout(Some(Duration::from_secs(2)))?;
680    let frames = read_until_frames(
681        &mut stream2,
682        announce_count,
683        rns_core::constants::CONTEXT_PATH_RESPONSE,
684    )
685    .map_err(|e| io::Error::new(e.kind(), format!("replayed announce delivery failed: {e}")))?;
686
687    tx.send(Event::Shutdown)
688        .map_err(|e| io::Error::new(io::ErrorKind::BrokenPipe, format!("{e}")))?;
689    let _ = driver_handle.join();
690
691    Ok(frames.len())
692}
693
694#[cfg(test)]
695mod tests {
696    use super::*;
697    use rns_core::packet::RawPacket;
698    use rns_core::types::IdentityHash;
699    use rns_crypto::identity::Identity;
700    use rns_crypto::OsRng;
701    use std::io::Read;
702    use std::sync::atomic::AtomicU64;
703    use std::sync::mpsc;
704    use std::sync::Arc;
705
706    use crate::interface::local::LocalServerConfig;
707
708    struct NoopCallbacks;
709    impl Callbacks for NoopCallbacks {
710        fn on_announce(&mut self, _: crate::destination::AnnouncedIdentity) {}
711        fn on_path_updated(&mut self, _: rns_core::types::DestHash, _: u8) {}
712        fn on_local_delivery(
713            &mut self,
714            _: rns_core::types::DestHash,
715            _: Vec<u8>,
716            _: rns_core::types::PacketHash,
717        ) {
718        }
719    }
720
721    fn find_free_port() -> u16 {
722        crate::test_support::port()
723    }
724
725    #[test]
726    fn shared_client_registers_destination_types_with_the_shared_instance() {
727        // Upstream 825c2e19: clients send their needed destination types to
728        // the shared instance over RPC and withdraw them on exit.
729        let dir = tempfile::tempdir().unwrap();
730        let rpc_port = find_free_port();
731        let shared_port = find_free_port();
732        std::fs::write(
733            dir.path().join("config"),
734            format!(
735                "[reticulum]\nshare_instance = yes\ninstance_control_port = {rpc_port}\n\
736                 shared_instance_port = {shared_port}\n\n[filter]\n\
737                 required_only = yes\nignored_types = nomadnetwork.node\n"
738            ),
739        )
740        .unwrap();
741        let daemon = RnsNode::from_config(Some(dir.path()), Box::new(NoopCallbacks)).unwrap();
742        let client =
743            RnsNode::connect_shared_from_config(Some(dir.path()), Box::new(NoopCallbacks)).unwrap();
744
745        // Only the shared instance knows the type is blocked, so `false`
746        // shows the call reached it.
747        assert!(client.wants_destination_type("rncp.receive"));
748        assert!(!client.wants_destination_type("nomadnetwork.node"));
749        assert!(!client.unwants_destination_type("rnx.execute"));
750        assert!(client.unwants_destination_type("rncp.receive"));
751        assert!(client.wants_destination_type("rnx.execute"));
752
753        assert!(client.required_announces_only(true));
754        assert!(!daemon.required_announces_only(true));
755
756        // Shutting the client down withdraws its registrations.
757        let auth_key = client.destination_types.shared_rpc.as_ref().unwrap().1;
758        let requester = client.destination_types.requester;
759        client.shutdown();
760        let mut rpc =
761            RpcClient::connect(&RpcAddr::Tcp("127.0.0.1".into(), rpc_port), &auth_key).unwrap();
762        let response = rpc
763            .call(&PickleValue::Dict(vec![
764                (
765                    PickleValue::String("unwants_destination_type".into()),
766                    PickleValue::String("rnx.execute".into()),
767                ),
768                (
769                    PickleValue::String("requester".into()),
770                    PickleValue::Bytes(requester.to_vec()),
771                ),
772            ]))
773            .unwrap();
774        assert_eq!(response, PickleValue::Bool(false));
775        daemon.shutdown();
776    }
777
778    #[test]
779    fn connect_shared_to_tcp_server() {
780        let port = find_free_port();
781        let next_id = Arc::new(AtomicU64::new(50000));
782        let (server_tx, server_rx) = crate::event::channel();
783
784        // Start a local server
785        let server_config = LocalServerConfig {
786            instance_name: crate::test_support::instance_name("test-shared-connect"),
787            port,
788            interface_id: rns_core::transport::types::InterfaceId(99),
789        };
790
791        crate::interface::local::start_server(server_config, server_tx, next_id).unwrap();
792        thread::sleep(Duration::from_millis(50));
793
794        // Connect as shared client
795        let config = SharedClientConfig {
796            instance_name: crate::test_support::instance_name("test-shared-connect"),
797            port,
798            rpc_port: 0,
799        };
800
801        let node = RnsNode::connect_shared(config, Box::new(NoopCallbacks)).unwrap();
802
803        // Server should see InterfaceUp for the client
804        let event = server_rx.recv_timeout(Duration::from_secs(2)).unwrap();
805        assert!(matches!(event, crate::event::Event::InterfaceUp(_, _, _)));
806
807        node.shutdown();
808    }
809
810    #[test]
811    fn shared_client_register_destination() {
812        let port = find_free_port();
813        let next_id = Arc::new(AtomicU64::new(51000));
814        let (server_tx, _server_rx) = crate::event::channel();
815
816        let server_config = LocalServerConfig {
817            instance_name: crate::test_support::instance_name("test-shared-reg"),
818            port,
819            interface_id: rns_core::transport::types::InterfaceId(98),
820        };
821
822        crate::interface::local::start_server(server_config, server_tx, next_id).unwrap();
823        thread::sleep(Duration::from_millis(50));
824
825        let config = SharedClientConfig {
826            instance_name: crate::test_support::instance_name("test-shared-reg"),
827            port,
828            rpc_port: 0,
829        };
830
831        let node = RnsNode::connect_shared(config, Box::new(NoopCallbacks)).unwrap();
832
833        // Register a destination
834        let dest_hash = [0xAA; 16];
835        node.register_destination(dest_hash, rns_core::constants::DESTINATION_SINGLE)
836            .unwrap();
837
838        // Give time for event processing
839        thread::sleep(Duration::from_millis(100));
840
841        node.shutdown();
842    }
843
844    #[test]
845    fn shared_client_send_packet() {
846        let port = find_free_port();
847        let next_id = Arc::new(AtomicU64::new(52000));
848        let (server_tx, server_rx) = crate::event::channel();
849
850        let server_config = LocalServerConfig {
851            instance_name: crate::test_support::instance_name("test-shared-send"),
852            port,
853            interface_id: rns_core::transport::types::InterfaceId(97),
854        };
855
856        crate::interface::local::start_server(server_config, server_tx, next_id).unwrap();
857        thread::sleep(Duration::from_millis(50));
858
859        let config = SharedClientConfig {
860            instance_name: crate::test_support::instance_name("test-shared-send"),
861            port,
862            rpc_port: 0,
863        };
864
865        let node = RnsNode::connect_shared(config, Box::new(NoopCallbacks)).unwrap();
866
867        // Build a minimal packet and send it
868        let raw = vec![0x00, 0x00, 0xAA, 0xBB, 0xCC, 0xDD]; // minimal raw packet
869        node.send_raw(raw, rns_core::constants::DESTINATION_PLAIN, None)
870            .unwrap();
871
872        // Server should receive a Frame event from the client
873        // (the packet will be HDLC-framed over the local connection)
874        for _ in 0..10 {
875            match server_rx.recv_timeout(Duration::from_secs(1)) {
876                Ok(crate::event::Event::Frame { .. }) => {
877                    break;
878                }
879                Ok(_) => continue,
880                Err(_) => break,
881            }
882        }
883        // The packet may or may not arrive as a Frame depending on transport
884        // routing, so we don't assert on it — the important thing is no crash.
885
886        node.shutdown();
887    }
888
889    #[test]
890    fn shared_client_replays_single_announces_after_reconnect() {
891        let port = find_free_port();
892        let addr = format!("127.0.0.1:{}", port);
893        let instance_name = format!("test-shared-replay-{}", port);
894
895        let listener1 = std::net::TcpListener::bind(&addr).unwrap();
896        let (accepted1_tx, accepted1_rx) = mpsc::channel();
897        thread::spawn(move || {
898            let (stream, _) = listener1.accept().unwrap();
899            accepted1_tx.send(stream).unwrap();
900        });
901
902        let node = RnsNode::connect_shared(
903            SharedClientConfig {
904                instance_name,
905                port,
906                rpc_port: 0,
907            },
908            Box::new(NoopCallbacks),
909        )
910        .unwrap();
911
912        let identity = Identity::new(&mut OsRng);
913        let dest = crate::destination::Destination::single_in(
914            "shared-replay",
915            &["echo"],
916            IdentityHash(*identity.hash()),
917        );
918        node.register_destination(dest.hash.0, dest.dest_type.to_wire_constant())
919            .unwrap();
920        node.announce_queued(&dest, &identity, Some(b"hello"))
921            .unwrap();
922
923        let mut stream1 = accepted1_rx.recv_timeout(Duration::from_secs(2)).unwrap();
924        stream1
925            .set_read_timeout(Some(Duration::from_secs(2)))
926            .unwrap();
927
928        let mut decoder = crate::interface::local::frame_decoder(0);
929        let mut buf = [0u8; 4096];
930        let n = stream1.read(&mut buf).unwrap();
931        let frames = decoder.feed(&buf[..n]);
932        assert!(!frames.is_empty(), "expected initial announce frame");
933        let packet1 = RawPacket::unpack(&frames[0]).unwrap();
934        assert_eq!(packet1.destination_hash, dest.hash.0);
935        assert_eq!(packet1.context, rns_core::constants::CONTEXT_NONE);
936
937        drop(stream1);
938
939        let listener2 = std::net::TcpListener::bind(&addr).unwrap();
940        let (accepted2_tx, accepted2_rx) = mpsc::channel();
941        thread::spawn(move || {
942            let (stream, _) = listener2.accept().unwrap();
943            accepted2_tx.send(stream).unwrap();
944        });
945
946        let mut stream2 = accepted2_rx.recv_timeout(Duration::from_secs(15)).unwrap();
947        stream2
948            .set_read_timeout(Some(Duration::from_secs(15)))
949            .unwrap();
950
951        let mut decoder = crate::interface::local::frame_decoder(0);
952        let n = stream2.read(&mut buf).unwrap();
953        let frames = decoder.feed(&buf[..n]);
954        assert!(!frames.is_empty(), "expected replayed announce frame");
955        let packet2 = RawPacket::unpack(&frames[0]).unwrap();
956        assert_eq!(packet2.destination_hash, dest.hash.0);
957        assert_eq!(packet2.context, rns_core::constants::CONTEXT_PATH_RESPONSE);
958
959        node.shutdown();
960    }
961
962    #[test]
963    fn connect_shared_fails_no_server() {
964        let port = find_free_port();
965
966        let config = SharedClientConfig {
967            instance_name: "nonexistent-instance-12345".into(),
968            port,
969            rpc_port: 0,
970        };
971
972        // Should fail because no server is running
973        let result = RnsNode::connect_shared(config, Box::new(NoopCallbacks));
974        assert!(result.is_err());
975    }
976
977    #[test]
978    fn shared_config_defaults() {
979        let config = SharedClientConfig::default();
980        assert_eq!(config.instance_name, "default");
981        assert_eq!(config.port, 37428);
982        assert_eq!(config.rpc_port, 37429);
983    }
984}