Skip to main content

rns_ctl/
remote.rs

1//! Remote management query helper.
2//!
3//! Connects as a shared client, creates a link to a remote management
4//! destination, sends a request, and returns the response data.
5
6use std::path::Path;
7use std::sync::mpsc;
8use std::sync::{Arc, Mutex};
9use std::time::Duration;
10
11use rns_net::shared_client::SharedClientConfig;
12use rns_net::{Callbacks, RnsNode};
13
14fn lock_response_data<'a>(
15    response_data: &'a Arc<Mutex<Option<Vec<u8>>>>,
16) -> std::sync::MutexGuard<'a, Option<Vec<u8>>> {
17    match response_data.lock() {
18        Ok(guard) => guard,
19        Err(poisoned) => {
20            log::error!("recovering from poisoned remote response buffer");
21            poisoned.into_inner()
22        }
23    }
24}
25
26/// Parse a 32-hex-char destination hash.
27pub fn parse_hex_hash(s: &str) -> Option<[u8; 16]> {
28    let s = s.trim();
29    if s.len() != 32 {
30        return None;
31    }
32    let bytes: Vec<u8> = (0..s.len())
33        .step_by(2)
34        .filter_map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok())
35        .collect();
36    if bytes.len() != 16 {
37        return None;
38    }
39    let mut result = [0u8; 16];
40    result.copy_from_slice(&bytes);
41    Some(result)
42}
43
44/// Result from a remote management query.
45pub struct RemoteQueryResult {
46    /// Raw response data from the management request.
47    pub data: Vec<u8>,
48}
49
50/// Callbacks that capture link establishment and response data.
51struct RemoteCallbacks {
52    link_established_tx: mpsc::Sender<rns_net::LinkId>,
53    response_data: Arc<Mutex<Option<Vec<u8>>>>,
54    response_tx: mpsc::Sender<()>,
55}
56
57impl Callbacks for RemoteCallbacks {
58    fn on_announce(&mut self, _announced: rns_net::AnnouncedIdentity) {}
59
60    fn on_path_updated(&mut self, _dest_hash: rns_net::DestHash, _hops: u8) {}
61
62    fn on_local_delivery(
63        &mut self,
64        _dest_hash: rns_net::DestHash,
65        _raw: Vec<u8>,
66        _packet_hash: rns_net::PacketHash,
67    ) {
68    }
69
70    fn on_link_established(
71        &mut self,
72        link_id: rns_net::LinkId,
73        _dest_hash: rns_net::DestHash,
74        _rtt: f64,
75        _is_initiator: bool,
76    ) {
77        let _ = self.link_established_tx.send(link_id);
78    }
79
80    fn on_response(&mut self, _link_id: rns_net::LinkId, _request_id: [u8; 16], data: Vec<u8>) {
81        *lock_response_data(&self.response_data) = Some(data);
82        let _ = self.response_tx.send(());
83    }
84}
85
86/// Perform a remote management query.
87///
88/// 1. Connects as a shared client
89/// 2. Creates a link to the management destination
90/// 3. Identifies on the link
91/// 4. Sends a request to the specified path
92/// 5. Returns the response data
93///
94/// Returns `None` if the query fails or times out.
95pub fn remote_query(
96    dest_hash: [u8; 16],
97    dest_sig_pub: [u8; 32],
98    identity_prv_key: [u8; 64],
99    path: &str,
100    data: &[u8],
101    config_path: Option<&Path>,
102    timeout: Duration,
103) -> Option<RemoteQueryResult> {
104    let (link_tx, link_rx) = mpsc::channel();
105    let (resp_tx, resp_rx) = mpsc::channel();
106    let response_data = Arc::new(Mutex::new(None));
107
108    let callbacks = RemoteCallbacks {
109        link_established_tx: link_tx,
110        response_data: response_data.clone(),
111        response_tx: resp_tx,
112    };
113
114    // Load config for shared instance connection
115    let config_dir = rns_net::storage::resolve_config_dir(config_path);
116    let config_file = config_dir.join("config");
117    let rns_config = if config_file.exists() {
118        rns_net::config::parse_file(&config_file).ok()?
119    } else {
120        rns_net::config::parse("").ok()?
121    };
122
123    let shared_config = SharedClientConfig {
124        instance_name: rns_config.reticulum.instance_name.clone(),
125        port: rns_config.reticulum.shared_instance_port,
126        rpc_port: rns_config.reticulum.instance_control_port,
127    };
128
129    let node = RnsNode::connect_shared(shared_config, Box::new(callbacks)).ok()?;
130
131    // Wait briefly for connection
132    std::thread::sleep(Duration::from_millis(500));
133
134    // Create link to management destination
135    let link_id = node.create_link(dest_hash, dest_sig_pub).ok()?;
136
137    // Wait for link establishment
138    let _established_link_id = link_rx.recv_timeout(timeout).ok()?;
139
140    // Identify on the link
141    node.identify_on_link(link_id, identity_prv_key).ok()?;
142    std::thread::sleep(Duration::from_millis(200));
143
144    // Send the request
145    node.send_request(link_id, path, data).ok()?;
146
147    // Wait for response
148    resp_rx.recv_timeout(timeout).ok()?;
149
150    let data = lock_response_data(&response_data).take()?;
151    node.shutdown();
152
153    Some(RemoteQueryResult { data })
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn parse_hex_hash_valid() {
162        let hash = parse_hex_hash("0123456789abcdef0123456789abcdef").unwrap();
163        assert_eq!(hash[0], 0x01);
164        assert_eq!(hash[15], 0xef);
165    }
166
167    #[test]
168    fn parse_hex_hash_invalid() {
169        assert!(parse_hex_hash("short").is_none());
170        assert!(parse_hex_hash("0123456789abcdef0123456789abcdef00").is_none());
171        assert!(parse_hex_hash("xyz3456789abcdef0123456789abcdef").is_none());
172    }
173
174    #[test]
175    fn parse_hex_hash_trimmed() {
176        let hash = parse_hex_hash("  0123456789abcdef0123456789abcdef  ").unwrap();
177        assert_eq!(hash[0], 0x01);
178    }
179}