1use 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
14pub 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
37pub const APP_NAME: &str = "rnstransport";
39
40pub const DEFAULT_STAMP_VALUE: u8 = 14;
42
43pub const WORKBLOCK_EXPAND_ROUNDS: u32 = 20;
45
46pub const STAMP_SIZE: usize = 32;
48
49pub const DISCOVERABLE_TYPES: [&str; 6] = [
51 "BackboneInterface",
52 "TCPServerInterface",
53 "I2PInterface",
54 "RNodeInterface",
55 "WeaveInterface",
56 "KISSInterface",
57];
58
59pub const THRESHOLD_UNKNOWN: f64 = 24.0 * 60.0 * 60.0;
62pub const THRESHOLD_STALE: f64 = 3.0 * 24.0 * 60.0 * 60.0;
64pub const THRESHOLD_REMOVE: f64 = 7.0 * 24.0 * 60.0 * 60.0;
66
67const STATUS_STALE: i32 = 0;
69const STATUS_UNKNOWN: i32 = 100;
70const STATUS_AVAILABLE: i32 = 1000;
71
72#[derive(Debug, Clone)]
78pub struct DiscoveryConfig {
79 pub discovery_name: String,
81 pub announce_interval: u64,
83 pub stamp_value: u8,
85 pub reachable_on: Option<String>,
87 pub interface_type: String,
89 pub listen_port: Option<u16>,
91 pub latitude: Option<f64>,
93 pub longitude: Option<f64>,
95 pub height: Option<f64>,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum DiscoveredStatus {
106 Available,
107 Unknown,
108 Stale,
109}
110
111impl DiscoveredStatus {
112 pub fn code(&self) -> i32 {
114 match self {
115 DiscoveredStatus::Available => STATUS_AVAILABLE,
116 DiscoveredStatus::Unknown => STATUS_UNKNOWN,
117 DiscoveredStatus::Stale => STATUS_STALE,
118 }
119 }
120
121 pub fn as_str(&self) -> &'static str {
123 match self {
124 DiscoveredStatus::Available => "available",
125 DiscoveredStatus::Unknown => "unknown",
126 DiscoveredStatus::Stale => "stale",
127 }
128 }
129}
130
131#[derive(Debug, Clone)]
133pub struct DiscoveredInterface {
134 pub interface_type: String,
136 pub transport: bool,
138 pub name: String,
140 pub discovered: f64,
142 pub last_heard: f64,
144 pub heard_count: u32,
146 pub status: DiscoveredStatus,
148 pub stamp: Vec<u8>,
150 pub stamp_value: u32,
152 pub transport_id: [u8; 16],
154 pub network_id: [u8; 16],
156 pub hops: u8,
158
159 pub latitude: Option<f64>,
161 pub longitude: Option<f64>,
162 pub height: Option<f64>,
163
164 pub reachable_on: Option<String>,
166 pub port: Option<u16>,
167
168 pub frequency: Option<u32>,
170 pub bandwidth: Option<u32>,
171 pub spreading_factor: Option<u8>,
172 pub coding_rate: Option<u8>,
173 pub modulation: Option<String>,
174 pub channel: Option<u8>,
175
176 pub ifac_netname: Option<String>,
178 pub ifac_netkey: Option<String>,
179
180 pub config_entry: Option<String>,
182
183 pub discovery_hash: [u8; 32],
185}
186
187impl DiscoveredInterface {
188 pub fn compute_status(&self) -> DiscoveredStatus {
190 let delta = time::now() - self.last_heard;
191 if delta > THRESHOLD_STALE {
192 DiscoveredStatus::Stale
193 } else if delta > THRESHOLD_UNKNOWN {
194 DiscoveredStatus::Unknown
195 } else {
196 DiscoveredStatus::Available
197 }
198 }
199}
200
201pub fn parse_interface_announce(
212 app_data: &[u8],
213 announced_identity_hash: &[u8; 16],
214 hops: u8,
215 required_stamp_value: u8,
216) -> Option<DiscoveredInterface> {
217 if app_data.len() <= STAMP_SIZE + 1 {
219 return None;
220 }
221
222 let flags = app_data[0];
224 let payload = &app_data[1..];
225
226 let encrypted = (flags & 0x02) != 0;
228 if encrypted {
229 log::debug!("Ignoring encrypted discovered interface (not supported)");
230 return None;
231 }
232
233 let stamp = &payload[payload.len() - STAMP_SIZE..];
235 let packed = &payload[..payload.len() - STAMP_SIZE];
236
237 let infohash = sha256(packed);
239 let workblock = stamp_workblock(&infohash, WORKBLOCK_EXPAND_ROUNDS);
240
241 if !stamp_valid(stamp, required_stamp_value, &workblock) {
243 log::debug!("Ignoring discovered interface with invalid stamp");
244 return None;
245 }
246
247 let stamp_value = stamp_value(&workblock, stamp);
249
250 let (value, _) = msgpack::unpack(packed).ok()?;
252 let map = value.as_map()?;
253
254 let get_u8_val = |key: u8| -> Option<Value> {
256 for (k, v) in map {
257 if k.as_uint()? as u8 == key {
258 return Some(v.clone());
259 }
260 }
261 None
262 };
263
264 let interface_type = get_u8_val(INTERFACE_TYPE)?.as_str()?.to_string();
266 if !is_discoverable_type(&interface_type) {
267 log::debug!(
268 "Ignoring discovered interface with unsupported type '{}'",
269 interface_type
270 );
271 return None;
272 }
273
274 let transport = get_u8_val(TRANSPORT)?.as_bool()?;
275 let name = get_u8_val(NAME)?
276 .as_str()
277 .unwrap_or(&format!("Discovered {}", interface_type))
278 .to_string();
279
280 let transport_id_val = get_u8_val(TRANSPORT_ID)?;
281 let transport_id_bytes = transport_id_val.as_bin()?;
282 let mut transport_id = [0u8; 16];
283 if transport_id_bytes.len() >= 16 {
284 transport_id.copy_from_slice(&transport_id_bytes[..16]);
285 }
286
287 let latitude = get_u8_val(LATITUDE).and_then(|v| v.as_float());
289 let longitude = get_u8_val(LONGITUDE).and_then(|v| v.as_float());
290 let height = get_u8_val(HEIGHT).and_then(|v| v.as_float());
291 let reachable_on = get_u8_val(REACHABLE_ON).and_then(|v| v.as_str().map(|s| s.to_string()));
292 if let Some(ref reachable_on) = reachable_on {
293 if !(is_ip_address(reachable_on) || is_hostname(reachable_on)) {
294 log::debug!(
295 "Ignoring discovered interface with invalid reachable_on '{}'",
296 reachable_on
297 );
298 return None;
299 }
300 }
301
302 let port = get_u8_val(PORT).and_then(|v| v.as_uint().map(|n| n as u16));
303 let frequency = get_u8_val(FREQUENCY).and_then(|v| v.as_uint().map(|n| n as u32));
304 let bandwidth = get_u8_val(BANDWIDTH).and_then(|v| v.as_uint().map(|n| n as u32));
305 let spreading_factor = get_u8_val(SPREADINGFACTOR).and_then(|v| v.as_uint().map(|n| n as u8));
306 let coding_rate = get_u8_val(CODINGRATE).and_then(|v| v.as_uint().map(|n| n as u8));
307 let modulation = get_u8_val(MODULATION).and_then(|v| v.as_str().map(|s| s.to_string()));
308 let channel = get_u8_val(CHANNEL).and_then(|v| v.as_uint().map(|n| n as u8));
309 let ifac_netname = get_u8_val(IFAC_NETNAME).and_then(|v| v.as_str().map(|s| s.to_string()));
310 let ifac_netkey = get_u8_val(IFAC_NETKEY).and_then(|v| v.as_str().map(|s| s.to_string()));
311
312 let discovery_hash = compute_discovery_hash(&transport_id, &name);
314
315 let config_entry = generate_config_entry(
317 &interface_type,
318 &name,
319 &transport_id,
320 reachable_on.as_deref(),
321 port,
322 frequency,
323 bandwidth,
324 spreading_factor,
325 coding_rate,
326 modulation.as_deref(),
327 ifac_netname.as_deref(),
328 ifac_netkey.as_deref(),
329 );
330
331 let now = time::now();
332
333 Some(DiscoveredInterface {
334 interface_type,
335 transport,
336 name,
337 discovered: now,
338 last_heard: now,
339 heard_count: 0,
340 status: DiscoveredStatus::Available,
341 stamp: stamp.to_vec(),
342 stamp_value,
343 transport_id,
344 network_id: *announced_identity_hash,
345 hops,
346 latitude,
347 longitude,
348 height,
349 reachable_on,
350 port,
351 frequency,
352 bandwidth,
353 spreading_factor,
354 coding_rate,
355 modulation,
356 channel,
357 ifac_netname,
358 ifac_netkey,
359 config_entry,
360 discovery_hash,
361 })
362}
363
364pub fn compute_discovery_hash(transport_id: &[u8; 16], name: &str) -> [u8; 32] {
366 let mut material = Vec::with_capacity(16 + name.len());
367 material.extend_from_slice(transport_id);
368 material.extend_from_slice(name.as_bytes());
369 sha256(&material)
370}
371
372fn generate_config_entry(
374 interface_type: &str,
375 name: &str,
376 transport_id: &[u8; 16],
377 reachable_on: Option<&str>,
378 port: Option<u16>,
379 frequency: Option<u32>,
380 bandwidth: Option<u32>,
381 spreading_factor: Option<u8>,
382 coding_rate: Option<u8>,
383 modulation: Option<&str>,
384 ifac_netname: Option<&str>,
385 ifac_netkey: Option<&str>,
386) -> Option<String> {
387 let transport_id_hex = hex_encode(transport_id);
388 let netname_str = ifac_netname
389 .map(|n| format!("\n network_name = {}", n))
390 .unwrap_or_default();
391 let netkey_str = ifac_netkey
392 .map(|k| format!("\n passphrase = {}", k))
393 .unwrap_or_default();
394 let identity_str = format!("\n transport_identity = {}", transport_id_hex);
395
396 match interface_type {
397 "BackboneInterface" | "TCPServerInterface" => {
398 let reachable = reachable_on.unwrap_or("unknown");
399 let port_val = port.unwrap_or(4242);
400 Some(format!(
401 "[[{}]]\n type = BackboneInterface\n enabled = yes\n remote = {}\n target_port = {}{}{}{}",
402 name, reachable, port_val, identity_str, netname_str, netkey_str
403 ))
404 }
405 "I2PInterface" => {
406 let reachable = reachable_on.unwrap_or("unknown");
407 Some(format!(
408 "[[{}]]\n type = I2PInterface\n enabled = yes\n peers = {}{}{}{}",
409 name, reachable, identity_str, netname_str, netkey_str
410 ))
411 }
412 "RNodeInterface" => {
413 let freq_str = frequency
414 .map(|f| format!("\n frequency = {}", f))
415 .unwrap_or_default();
416 let bw_str = bandwidth
417 .map(|b| format!("\n bandwidth = {}", b))
418 .unwrap_or_default();
419 let sf_str = spreading_factor
420 .map(|s| format!("\n spreadingfactor = {}", s))
421 .unwrap_or_default();
422 let cr_str = coding_rate
423 .map(|c| format!("\n codingrate = {}", c))
424 .unwrap_or_default();
425 Some(format!(
426 "[[{}]]\n type = RNodeInterface\n enabled = yes\n port = {}{}{}{}{}{}{}{}",
427 name, "", freq_str, bw_str, sf_str, cr_str, identity_str, netname_str, netkey_str
428 ))
429 }
430 "KISSInterface" => {
431 let freq_str = frequency
432 .map(|f| format!("\n # Frequency: {}", f))
433 .unwrap_or_default();
434 let bw_str = bandwidth
435 .map(|b| format!("\n # Bandwidth: {}", b))
436 .unwrap_or_default();
437 let mod_str = modulation
438 .map(|m| format!("\n # Modulation: {}", m))
439 .unwrap_or_default();
440 Some(format!(
441 "[[{}]]\n type = KISSInterface\n enabled = yes\n port = {}{}{}{}{}{}{}",
442 name, "", freq_str, bw_str, mod_str, identity_str, netname_str, netkey_str
443 ))
444 }
445 "WeaveInterface" => Some(format!(
446 "[[{}]]\n type = WeaveInterface\n enabled = yes\n port = {}{}{}{}",
447 name, "", identity_str, netname_str, netkey_str
448 )),
449 _ => None,
450 }
451}
452
453pub fn hex_encode(bytes: &[u8]) -> String {
459 bytes.iter().map(|b| format!("{:02x}", b)).collect()
460}
461
462pub fn is_ip_address(s: &str) -> bool {
464 s.parse::<std::net::IpAddr>().is_ok()
465}
466
467pub fn is_hostname(s: &str) -> bool {
469 let s = s.strip_suffix('.').unwrap_or(s);
470 if s.len() > 253 {
471 return false;
472 }
473 let components: Vec<&str> = s.split('.').collect();
474 if components.is_empty() {
475 return false;
476 }
477 if components
479 .last()
480 .map(|c| c.chars().all(|ch| ch.is_ascii_digit()))
481 .unwrap_or(false)
482 {
483 return false;
484 }
485 components.iter().all(|c| {
486 !c.is_empty()
487 && c.len() <= 63
488 && !c.starts_with('-')
489 && !c.ends_with('-')
490 && c.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '-')
491 })
492}
493
494pub fn is_discoverable_type(interface_type: &str) -> bool {
496 DISCOVERABLE_TYPES.contains(&interface_type)
497}
498
499pub fn filter_and_sort_interfaces(
501 interfaces: &mut Vec<DiscoveredInterface>,
502 only_available: bool,
503 only_transport: bool,
504) {
505 let now = time::now();
506
507 interfaces.retain(|iface| {
509 if !is_discoverable_type(&iface.interface_type) {
510 return false;
511 }
512 if let Some(ref reachable_on) = iface.reachable_on {
513 if !(is_ip_address(reachable_on) || is_hostname(reachable_on)) {
514 return false;
515 }
516 }
517
518 let delta = now - iface.last_heard;
519
520 if delta > THRESHOLD_REMOVE {
522 return false;
523 }
524
525 let status = iface.compute_status();
527
528 if only_available && status != DiscoveredStatus::Available {
530 return false;
531 }
532 if only_transport && !iface.transport {
533 return false;
534 }
535
536 true
537 });
538
539 interfaces.sort_by(|a, b| {
541 let status_cmp = b.compute_status().code().cmp(&a.compute_status().code());
542 if status_cmp != std::cmp::Ordering::Equal {
543 return status_cmp;
544 }
545 let value_cmp = b.stamp_value.cmp(&a.stamp_value);
546 if value_cmp != std::cmp::Ordering::Equal {
547 return value_cmp;
548 }
549 b.last_heard
550 .partial_cmp(&a.last_heard)
551 .unwrap_or(std::cmp::Ordering::Equal)
552 });
553}
554
555pub fn discovery_name_hash() -> [u8; 10] {
560 rns_core::destination::name_hash(APP_NAME, &["discovery", "interface"])
561}
562
563#[cfg(test)]
564mod tests {
565 use super::*;
566
567 fn build_discovery_app_data(interface_type: &str, reachable_on: Option<&str>) -> Vec<u8> {
568 let mut entries = vec![
569 (
570 Value::UInt(INTERFACE_TYPE as u64),
571 Value::Str(interface_type.to_string()),
572 ),
573 (Value::UInt(TRANSPORT as u64), Value::Bool(true)),
574 (
575 Value::UInt(NAME as u64),
576 Value::Str(format!("test-{interface_type}")),
577 ),
578 (Value::UInt(TRANSPORT_ID as u64), Value::Bin(vec![0x42; 16])),
579 ];
580
581 if let Some(reachable_on) = reachable_on {
582 entries.push((
583 Value::UInt(REACHABLE_ON as u64),
584 Value::Str(reachable_on.to_string()),
585 ));
586 }
587
588 let packed = msgpack::pack(&Value::Map(entries));
589 let mut app_data = Vec::with_capacity(1 + packed.len() + STAMP_SIZE);
590 app_data.push(0x00);
591 app_data.extend_from_slice(&packed);
592 app_data.extend_from_slice(&[0u8; STAMP_SIZE]);
593 app_data
594 }
595
596 #[test]
597 fn parse_rejects_unsupported_discovered_interface_type() {
598 let app_data = build_discovery_app_data("BogusInterface", None);
599
600 let parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0);
601
602 assert!(
603 parsed.is_none(),
604 "unsupported discovered interface types must be ignored"
605 );
606 }
607
608 #[test]
609 fn parse_rejects_invalid_reachable_on_address() {
610 let app_data = build_discovery_app_data("BackboneInterface", Some("-not a host-"));
611
612 let parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0);
613
614 assert!(
615 parsed.is_none(),
616 "discovered interfaces with invalid reachable_on values must be ignored"
617 );
618 }
619
620 #[test]
621 fn parse_accepts_supported_discovered_interface_types() {
622 for interface_type in [
623 "BackboneInterface",
624 "TCPServerInterface",
625 "I2PInterface",
626 "RNodeInterface",
627 "WeaveInterface",
628 "KISSInterface",
629 ] {
630 let app_data = build_discovery_app_data(interface_type, Some("example.com"));
631
632 let parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0);
633
634 assert!(
635 parsed.is_some(),
636 "{interface_type} should be accepted as a discoverable interface type"
637 );
638 }
639 }
640}