Skip to main content

rns_net/common/
discovery.rs

1//! Interface Discovery protocol — pure types and parsing logic.
2//!
3//! Contains constants, data structures, parsing, and validation functions
4//! for the interface discovery protocol. No filesystem or threading I/O.
5//!
6//! Python reference: RNS/Discovery.py
7
8use rns_core::msgpack::{self, Value};
9use rns_core::stamp::{stamp_valid, stamp_value, stamp_workblock};
10use rns_crypto::sha256::sha256;
11
12use super::time;
13
14// ============================================================================
15// Constants (matching Python Discovery.py)
16// ============================================================================
17
18/// Discovery field IDs for msgpack encoding
19pub const NAME: u8 = 0xFF;
20pub const TRANSPORT_ID: u8 = 0xFE;
21pub const INTERFACE_TYPE: u8 = 0x00;
22pub const TRANSPORT: u8 = 0x01;
23pub const REACHABLE_ON: u8 = 0x02;
24pub const LATITUDE: u8 = 0x03;
25pub const LONGITUDE: u8 = 0x04;
26pub const HEIGHT: u8 = 0x05;
27pub const PORT: u8 = 0x06;
28pub const IFAC_NETNAME: u8 = 0x07;
29pub const IFAC_NETKEY: u8 = 0x08;
30pub const FREQUENCY: u8 = 0x09;
31pub const BANDWIDTH: u8 = 0x0A;
32pub const SPREADINGFACTOR: u8 = 0x0B;
33pub const CODINGRATE: u8 = 0x0C;
34pub const MODULATION: u8 = 0x0D;
35pub const CHANNEL: u8 = 0x0E;
36
37/// App name for discovery destination
38pub const APP_NAME: &str = "rnstransport";
39
40/// Default stamp value for interface discovery
41pub const DEFAULT_STAMP_VALUE: u8 = 14;
42
43/// Workblock expand rounds for interface discovery
44pub const WORKBLOCK_EXPAND_ROUNDS: u32 = 20;
45
46/// Stamp size in bytes
47pub const STAMP_SIZE: usize = 32;
48
49// Status thresholds (in seconds)
50/// 24 hours - status becomes "unknown"
51pub const THRESHOLD_UNKNOWN: f64 = 24.0 * 60.0 * 60.0;
52/// 3 days - status becomes "stale"
53pub const THRESHOLD_STALE: f64 = 3.0 * 24.0 * 60.0 * 60.0;
54/// 7 days - interface is removed
55pub const THRESHOLD_REMOVE: f64 = 7.0 * 24.0 * 60.0 * 60.0;
56
57// Status codes for sorting
58const STATUS_STALE: i32 = 0;
59const STATUS_UNKNOWN: i32 = 100;
60const STATUS_AVAILABLE: i32 = 1000;
61
62// ============================================================================
63// Per-interface discovery configuration
64// ============================================================================
65
66/// Per-interface discovery configuration parsed from config file.
67#[derive(Debug, Clone)]
68pub struct DiscoveryConfig {
69    /// Human-readable name to advertise (defaults to interface name).
70    pub discovery_name: String,
71    /// Announce interval in seconds (default 21600 = 6h, min 300 = 5min).
72    pub announce_interval: u64,
73    /// Stamp cost for discovery PoW (default 14).
74    pub stamp_value: u8,
75    /// IP/hostname this interface is reachable on.
76    pub reachable_on: Option<String>,
77    /// Interface type string (e.g. "BackboneInterface").
78    pub interface_type: String,
79    /// Listen port of the discoverable interface.
80    pub listen_port: Option<u16>,
81    /// Geographic latitude in decimal degrees.
82    pub latitude: Option<f64>,
83    /// Geographic longitude in decimal degrees.
84    pub longitude: Option<f64>,
85    /// Height/altitude in meters.
86    pub height: Option<f64>,
87}
88
89// ============================================================================
90// Data Structures
91// ============================================================================
92
93/// Status of a discovered interface
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum DiscoveredStatus {
96    Available,
97    Unknown,
98    Stale,
99}
100
101impl DiscoveredStatus {
102    /// Get numeric code for sorting (higher = better)
103    pub fn code(&self) -> i32 {
104        match self {
105            DiscoveredStatus::Available => STATUS_AVAILABLE,
106            DiscoveredStatus::Unknown => STATUS_UNKNOWN,
107            DiscoveredStatus::Stale => STATUS_STALE,
108        }
109    }
110
111    /// Convert to string
112    pub fn as_str(&self) -> &'static str {
113        match self {
114            DiscoveredStatus::Available => "available",
115            DiscoveredStatus::Unknown => "unknown",
116            DiscoveredStatus::Stale => "stale",
117        }
118    }
119}
120
121/// Information about a discovered interface
122#[derive(Debug, Clone)]
123pub struct DiscoveredInterface {
124    /// Interface type (e.g., "BackboneInterface", "TCPServerInterface", "RNodeInterface")
125    pub interface_type: String,
126    /// Whether the announcing node has transport enabled
127    pub transport: bool,
128    /// Human-readable name of the interface
129    pub name: String,
130    /// Timestamp when first discovered
131    pub discovered: f64,
132    /// Timestamp of last announcement
133    pub last_heard: f64,
134    /// Number of times heard
135    pub heard_count: u32,
136    /// Current status based on last_heard
137    pub status: DiscoveredStatus,
138    /// Raw stamp bytes
139    pub stamp: Vec<u8>,
140    /// Calculated stamp value (leading zeros)
141    pub stamp_value: u32,
142    /// Transport identity hash (truncated)
143    pub transport_id: [u8; 16],
144    /// Network identity hash (announcer)
145    pub network_id: [u8; 16],
146    /// Number of hops to reach this interface
147    pub hops: u8,
148
149    // Optional location info
150    pub latitude: Option<f64>,
151    pub longitude: Option<f64>,
152    pub height: Option<f64>,
153
154    // Connection info
155    pub reachable_on: Option<String>,
156    pub port: Option<u16>,
157
158    // RNode/RF specific
159    pub frequency: Option<u32>,
160    pub bandwidth: Option<u32>,
161    pub spreading_factor: Option<u8>,
162    pub coding_rate: Option<u8>,
163    pub modulation: Option<String>,
164    pub channel: Option<u8>,
165
166    // IFAC info
167    pub ifac_netname: Option<String>,
168    pub ifac_netkey: Option<String>,
169
170    // Auto-generated config entry
171    pub config_entry: Option<String>,
172
173    /// Hash for storage key (SHA256 of transport_id + name)
174    pub discovery_hash: [u8; 32],
175}
176
177impl DiscoveredInterface {
178    /// Compute the current status based on last_heard timestamp
179    pub fn compute_status(&self) -> DiscoveredStatus {
180        let delta = time::now() - self.last_heard;
181        if delta > THRESHOLD_STALE {
182            DiscoveredStatus::Stale
183        } else if delta > THRESHOLD_UNKNOWN {
184            DiscoveredStatus::Unknown
185        } else {
186            DiscoveredStatus::Available
187        }
188    }
189}
190
191// ============================================================================
192// Parsing and Validation
193// ============================================================================
194
195/// Parse an interface discovery announcement from app_data.
196///
197/// Returns None if:
198/// - Data is too short
199/// - Stamp is invalid
200/// - Required fields are missing
201pub fn parse_interface_announce(
202    app_data: &[u8],
203    announced_identity_hash: &[u8; 16],
204    hops: u8,
205    required_stamp_value: u8,
206) -> Option<DiscoveredInterface> {
207    // Need at least: 1 byte flags + some data + STAMP_SIZE
208    if app_data.len() <= STAMP_SIZE + 1 {
209        return None;
210    }
211
212    // Extract flags and payload
213    let flags = app_data[0];
214    let payload = &app_data[1..];
215
216    // Check encryption flag (we don't support encrypted discovery yet)
217    let encrypted = (flags & 0x02) != 0;
218    if encrypted {
219        log::debug!("Ignoring encrypted discovered interface (not supported)");
220        return None;
221    }
222
223    // Split stamp and packed info
224    let stamp = &payload[payload.len() - STAMP_SIZE..];
225    let packed = &payload[..payload.len() - STAMP_SIZE];
226
227    // Compute infohash and workblock
228    let infohash = sha256(packed);
229    let workblock = stamp_workblock(&infohash, WORKBLOCK_EXPAND_ROUNDS);
230
231    // Validate stamp
232    if !stamp_valid(stamp, required_stamp_value, &workblock) {
233        log::debug!("Ignoring discovered interface with invalid stamp");
234        return None;
235    }
236
237    // Calculate stamp value
238    let stamp_value = stamp_value(&workblock, stamp);
239
240    // Unpack the interface info
241    let (value, _) = msgpack::unpack(packed).ok()?;
242    let map = value.as_map()?;
243
244    // Helper to get a value from the map by integer key
245    let get_u8_val = |key: u8| -> Option<Value> {
246        for (k, v) in map {
247            if k.as_uint()? as u8 == key {
248                return Some(v.clone());
249            }
250        }
251        None
252    };
253
254    // Extract required fields
255    let interface_type = get_u8_val(INTERFACE_TYPE)?.as_str()?.to_string();
256    let transport = get_u8_val(TRANSPORT)?.as_bool()?;
257    let name = get_u8_val(NAME)?
258        .as_str()
259        .unwrap_or(&format!("Discovered {}", interface_type))
260        .to_string();
261
262    let transport_id_val = get_u8_val(TRANSPORT_ID)?;
263    let transport_id_bytes = transport_id_val.as_bin()?;
264    let mut transport_id = [0u8; 16];
265    if transport_id_bytes.len() >= 16 {
266        transport_id.copy_from_slice(&transport_id_bytes[..16]);
267    }
268
269    // Extract optional fields
270    let latitude = get_u8_val(LATITUDE).and_then(|v| v.as_float());
271    let longitude = get_u8_val(LONGITUDE).and_then(|v| v.as_float());
272    let height = get_u8_val(HEIGHT).and_then(|v| v.as_float());
273    let reachable_on = get_u8_val(REACHABLE_ON).and_then(|v| v.as_str().map(|s| s.to_string()));
274    let port = get_u8_val(PORT).and_then(|v| v.as_uint().map(|n| n as u16));
275    let frequency = get_u8_val(FREQUENCY).and_then(|v| v.as_uint().map(|n| n as u32));
276    let bandwidth = get_u8_val(BANDWIDTH).and_then(|v| v.as_uint().map(|n| n as u32));
277    let spreading_factor = get_u8_val(SPREADINGFACTOR).and_then(|v| v.as_uint().map(|n| n as u8));
278    let coding_rate = get_u8_val(CODINGRATE).and_then(|v| v.as_uint().map(|n| n as u8));
279    let modulation = get_u8_val(MODULATION).and_then(|v| v.as_str().map(|s| s.to_string()));
280    let channel = get_u8_val(CHANNEL).and_then(|v| v.as_uint().map(|n| n as u8));
281    let ifac_netname = get_u8_val(IFAC_NETNAME).and_then(|v| v.as_str().map(|s| s.to_string()));
282    let ifac_netkey = get_u8_val(IFAC_NETKEY).and_then(|v| v.as_str().map(|s| s.to_string()));
283
284    // Compute discovery hash
285    let discovery_hash = compute_discovery_hash(&transport_id, &name);
286
287    // Generate config entry
288    let config_entry = generate_config_entry(
289        &interface_type,
290        &name,
291        &transport_id,
292        reachable_on.as_deref(),
293        port,
294        frequency,
295        bandwidth,
296        spreading_factor,
297        coding_rate,
298        modulation.as_deref(),
299        ifac_netname.as_deref(),
300        ifac_netkey.as_deref(),
301    );
302
303    let now = time::now();
304
305    Some(DiscoveredInterface {
306        interface_type,
307        transport,
308        name,
309        discovered: now,
310        last_heard: now,
311        heard_count: 0,
312        status: DiscoveredStatus::Available,
313        stamp: stamp.to_vec(),
314        stamp_value,
315        transport_id,
316        network_id: *announced_identity_hash,
317        hops,
318        latitude,
319        longitude,
320        height,
321        reachable_on,
322        port,
323        frequency,
324        bandwidth,
325        spreading_factor,
326        coding_rate,
327        modulation,
328        channel,
329        ifac_netname,
330        ifac_netkey,
331        config_entry,
332        discovery_hash,
333    })
334}
335
336/// Compute the discovery hash for storage
337pub fn compute_discovery_hash(transport_id: &[u8; 16], name: &str) -> [u8; 32] {
338    let mut material = Vec::with_capacity(16 + name.len());
339    material.extend_from_slice(transport_id);
340    material.extend_from_slice(name.as_bytes());
341    sha256(&material)
342}
343
344/// Generate a config entry for auto-connecting to a discovered interface
345fn generate_config_entry(
346    interface_type: &str,
347    name: &str,
348    transport_id: &[u8; 16],
349    reachable_on: Option<&str>,
350    port: Option<u16>,
351    frequency: Option<u32>,
352    bandwidth: Option<u32>,
353    spreading_factor: Option<u8>,
354    coding_rate: Option<u8>,
355    modulation: Option<&str>,
356    ifac_netname: Option<&str>,
357    ifac_netkey: Option<&str>,
358) -> Option<String> {
359    let transport_id_hex = hex_encode(transport_id);
360    let netname_str = ifac_netname
361        .map(|n| format!("\n  network_name = {}", n))
362        .unwrap_or_default();
363    let netkey_str = ifac_netkey
364        .map(|k| format!("\n  passphrase = {}", k))
365        .unwrap_or_default();
366    let identity_str = format!("\n  transport_identity = {}", transport_id_hex);
367
368    match interface_type {
369        "BackboneInterface" | "TCPServerInterface" => {
370            let reachable = reachable_on.unwrap_or("unknown");
371            let port_val = port.unwrap_or(4242);
372            Some(format!(
373                "[[{}]]\n  type = BackboneInterface\n  enabled = yes\n  remote = {}\n  target_port = {}{}{}{}",
374                name, reachable, port_val, identity_str, netname_str, netkey_str
375            ))
376        }
377        "I2PInterface" => {
378            let reachable = reachable_on.unwrap_or("unknown");
379            Some(format!(
380                "[[{}]]\n  type = I2PInterface\n  enabled = yes\n  peers = {}{}{}{}",
381                name, reachable, identity_str, netname_str, netkey_str
382            ))
383        }
384        "RNodeInterface" => {
385            let freq_str = frequency
386                .map(|f| format!("\n  frequency = {}", f))
387                .unwrap_or_default();
388            let bw_str = bandwidth
389                .map(|b| format!("\n  bandwidth = {}", b))
390                .unwrap_or_default();
391            let sf_str = spreading_factor
392                .map(|s| format!("\n  spreadingfactor = {}", s))
393                .unwrap_or_default();
394            let cr_str = coding_rate
395                .map(|c| format!("\n  codingrate = {}", c))
396                .unwrap_or_default();
397            Some(format!(
398                "[[{}]]\n  type = RNodeInterface\n  enabled = yes\n  port = {}{}{}{}{}{}{}{}",
399                name, "", freq_str, bw_str, sf_str, cr_str, identity_str, netname_str, netkey_str
400            ))
401        }
402        "KISSInterface" => {
403            let freq_str = frequency
404                .map(|f| format!("\n  # Frequency: {}", f))
405                .unwrap_or_default();
406            let bw_str = bandwidth
407                .map(|b| format!("\n  # Bandwidth: {}", b))
408                .unwrap_or_default();
409            let mod_str = modulation
410                .map(|m| format!("\n  # Modulation: {}", m))
411                .unwrap_or_default();
412            Some(format!(
413                "[[{}]]\n  type = KISSInterface\n  enabled = yes\n  port = {}{}{}{}{}{}{}",
414                name, "", freq_str, bw_str, mod_str, identity_str, netname_str, netkey_str
415            ))
416        }
417        "WeaveInterface" => Some(format!(
418            "[[{}]]\n  type = WeaveInterface\n  enabled = yes\n  port = {}{}{}{}",
419            name, "", identity_str, netname_str, netkey_str
420        )),
421        _ => None,
422    }
423}
424
425// ============================================================================
426// Helper Functions
427// ============================================================================
428
429/// Encode bytes as hex string (no delimiters)
430pub fn hex_encode(bytes: &[u8]) -> String {
431    bytes.iter().map(|b| format!("{:02x}", b)).collect()
432}
433
434/// Check if a string is a valid IP address
435pub fn is_ip_address(s: &str) -> bool {
436    s.parse::<std::net::IpAddr>().is_ok()
437}
438
439/// Check if a string is a valid hostname
440pub fn is_hostname(s: &str) -> bool {
441    let s = s.strip_suffix('.').unwrap_or(s);
442    if s.len() > 253 {
443        return false;
444    }
445    let components: Vec<&str> = s.split('.').collect();
446    if components.is_empty() {
447        return false;
448    }
449    // Last component should not be all numeric
450    if components
451        .last()
452        .map(|c| c.chars().all(|ch| ch.is_ascii_digit()))
453        .unwrap_or(false)
454    {
455        return false;
456    }
457    components.iter().all(|c| {
458        !c.is_empty()
459            && c.len() <= 63
460            && !c.starts_with('-')
461            && !c.ends_with('-')
462            && c.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '-')
463    })
464}
465
466/// Filter and sort discovered interfaces
467pub fn filter_and_sort_interfaces(
468    interfaces: &mut Vec<DiscoveredInterface>,
469    only_available: bool,
470    only_transport: bool,
471) {
472    let now = time::now();
473
474    // Update status and filter
475    interfaces.retain(|iface| {
476        let delta = now - iface.last_heard;
477
478        // Check for removal threshold
479        if delta > THRESHOLD_REMOVE {
480            return false;
481        }
482
483        // Update status
484        let status = iface.compute_status();
485
486        // Apply filters
487        if only_available && status != DiscoveredStatus::Available {
488            return false;
489        }
490        if only_transport && !iface.transport {
491            return false;
492        }
493
494        true
495    });
496
497    // Sort by (status_code desc, value desc, last_heard desc)
498    interfaces.sort_by(|a, b| {
499        let status_cmp = b.compute_status().code().cmp(&a.compute_status().code());
500        if status_cmp != std::cmp::Ordering::Equal {
501            return status_cmp;
502        }
503        let value_cmp = b.stamp_value.cmp(&a.stamp_value);
504        if value_cmp != std::cmp::Ordering::Equal {
505            return value_cmp;
506        }
507        b.last_heard
508            .partial_cmp(&a.last_heard)
509            .unwrap_or(std::cmp::Ordering::Equal)
510    });
511}
512
513/// Compute the name hash for the discovery destination: `rnstransport.discovery.interface`.
514///
515/// Discovery is a SINGLE destination — its dest hash varies with the sender's identity.
516/// We match incoming announces by comparing their name_hash to this constant.
517pub fn discovery_name_hash() -> [u8; 10] {
518    rns_core::destination::name_hash(APP_NAME, &["discovery", "interface"])
519}