use rns_core::msgpack::{self, Value};
use rns_core::stamp::{stamp_valid, stamp_value, stamp_workblock};
use rns_crypto::sha256::sha256;
use super::time;
pub const NAME: u8 = 0xFF;
pub const TRANSPORT_ID: u8 = 0xFE;
pub const INTERFACE_TYPE: u8 = 0x00;
pub const TRANSPORT: u8 = 0x01;
pub const REACHABLE_ON: u8 = 0x02;
pub const LATITUDE: u8 = 0x03;
pub const LONGITUDE: u8 = 0x04;
pub const HEIGHT: u8 = 0x05;
pub const PORT: u8 = 0x06;
pub const IFAC_NETNAME: u8 = 0x07;
pub const IFAC_NETKEY: u8 = 0x08;
pub const FREQUENCY: u8 = 0x09;
pub const BANDWIDTH: u8 = 0x0A;
pub const SPREADINGFACTOR: u8 = 0x0B;
pub const CODINGRATE: u8 = 0x0C;
pub const MODULATION: u8 = 0x0D;
pub const CHANNEL: u8 = 0x0E;
pub const APP_NAME: &str = "rnstransport";
pub const DEFAULT_STAMP_VALUE: u8 = 14;
pub const WORKBLOCK_EXPAND_ROUNDS: u32 = 20;
pub const STAMP_SIZE: usize = 32;
pub const DISCOVERABLE_TYPES: [&str; 6] = [
"BackboneInterface",
"TCPServerInterface",
"I2PInterface",
"RNodeInterface",
"WeaveInterface",
"KISSInterface",
];
pub const THRESHOLD_UNKNOWN: f64 = 24.0 * 60.0 * 60.0;
pub const THRESHOLD_STALE: f64 = 3.0 * 24.0 * 60.0 * 60.0;
pub const THRESHOLD_REMOVE: f64 = 7.0 * 24.0 * 60.0 * 60.0;
const STATUS_STALE: i32 = 0;
const STATUS_UNKNOWN: i32 = 100;
const STATUS_AVAILABLE: i32 = 1000;
#[derive(Debug, Clone)]
pub struct DiscoveryConfig {
pub discovery_name: String,
pub announce_interval: u64,
pub stamp_value: u8,
pub reachable_on: Option<String>,
pub interface_type: String,
pub listen_port: Option<u16>,
pub latitude: Option<f64>,
pub longitude: Option<f64>,
pub height: Option<f64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiscoveredStatus {
Available,
Unknown,
Stale,
}
impl DiscoveredStatus {
pub fn code(&self) -> i32 {
match self {
DiscoveredStatus::Available => STATUS_AVAILABLE,
DiscoveredStatus::Unknown => STATUS_UNKNOWN,
DiscoveredStatus::Stale => STATUS_STALE,
}
}
pub fn as_str(&self) -> &'static str {
match self {
DiscoveredStatus::Available => "available",
DiscoveredStatus::Unknown => "unknown",
DiscoveredStatus::Stale => "stale",
}
}
}
#[derive(Debug, Clone)]
pub struct DiscoveredInterface {
pub interface_type: String,
pub transport: bool,
pub name: String,
pub discovered: f64,
pub last_heard: f64,
pub heard_count: u32,
pub status: DiscoveredStatus,
pub stamp: Vec<u8>,
pub stamp_value: u32,
pub transport_id: [u8; 16],
pub network_id: [u8; 16],
pub hops: u8,
pub latitude: Option<f64>,
pub longitude: Option<f64>,
pub height: Option<f64>,
pub reachable_on: Option<String>,
pub port: Option<u16>,
pub frequency: Option<u32>,
pub bandwidth: Option<u32>,
pub spreading_factor: Option<u8>,
pub coding_rate: Option<u8>,
pub modulation: Option<String>,
pub channel: Option<u8>,
pub ifac_netname: Option<String>,
pub ifac_netkey: Option<String>,
pub config_entry: Option<String>,
pub discovery_hash: [u8; 32],
}
impl DiscoveredInterface {
pub fn compute_status(&self) -> DiscoveredStatus {
let delta = time::now() - self.last_heard;
if delta > THRESHOLD_STALE {
DiscoveredStatus::Stale
} else if delta > THRESHOLD_UNKNOWN {
DiscoveredStatus::Unknown
} else {
DiscoveredStatus::Available
}
}
}
pub fn parse_interface_announce(
app_data: &[u8],
announced_identity_hash: &[u8; 16],
hops: u8,
required_stamp_value: u8,
) -> Option<DiscoveredInterface> {
if app_data.len() <= STAMP_SIZE + 1 {
return None;
}
let flags = app_data[0];
let payload = &app_data[1..];
let encrypted = (flags & 0x02) != 0;
if encrypted {
log::debug!("Ignoring encrypted discovered interface (not supported)");
return None;
}
let stamp = &payload[payload.len() - STAMP_SIZE..];
let packed = &payload[..payload.len() - STAMP_SIZE];
let infohash = sha256(packed);
let workblock = stamp_workblock(&infohash, WORKBLOCK_EXPAND_ROUNDS);
if !stamp_valid(stamp, required_stamp_value, &workblock) {
log::debug!("Ignoring discovered interface with invalid stamp");
return None;
}
let stamp_value = stamp_value(&workblock, stamp);
let (value, _) = msgpack::unpack(packed).ok()?;
let map = value.as_map()?;
let get_u8_val = |key: u8| -> Option<Value> {
for (k, v) in map {
if k.as_uint()? as u8 == key {
return Some(v.clone());
}
}
None
};
let interface_type = get_u8_val(INTERFACE_TYPE)?.as_str()?.to_string();
if !is_discoverable_type(&interface_type) {
log::debug!(
"Ignoring discovered interface with unsupported type '{}'",
interface_type
);
return None;
}
let transport = get_u8_val(TRANSPORT)?.as_bool()?;
let name = get_u8_val(NAME)?
.as_str()
.unwrap_or(&format!("Discovered {}", interface_type))
.to_string();
let transport_id_val = get_u8_val(TRANSPORT_ID)?;
let transport_id_bytes = transport_id_val.as_bin()?;
let mut transport_id = [0u8; 16];
if transport_id_bytes.len() >= 16 {
transport_id.copy_from_slice(&transport_id_bytes[..16]);
}
let latitude = get_u8_val(LATITUDE).and_then(|v| v.as_float());
let longitude = get_u8_val(LONGITUDE).and_then(|v| v.as_float());
let height = get_u8_val(HEIGHT).and_then(|v| v.as_float());
let reachable_on = get_u8_val(REACHABLE_ON).and_then(|v| v.as_str().map(|s| s.to_string()));
if let Some(ref reachable_on) = reachable_on {
if !(is_ip_address(reachable_on) || is_hostname(reachable_on)) {
log::debug!(
"Ignoring discovered interface with invalid reachable_on '{}'",
reachable_on
);
return None;
}
}
let port = get_u8_val(PORT).and_then(|v| v.as_uint().map(|n| n as u16));
let frequency = get_u8_val(FREQUENCY).and_then(|v| v.as_uint().map(|n| n as u32));
let bandwidth = get_u8_val(BANDWIDTH).and_then(|v| v.as_uint().map(|n| n as u32));
let spreading_factor = get_u8_val(SPREADINGFACTOR).and_then(|v| v.as_uint().map(|n| n as u8));
let coding_rate = get_u8_val(CODINGRATE).and_then(|v| v.as_uint().map(|n| n as u8));
let modulation = get_u8_val(MODULATION).and_then(|v| v.as_str().map(|s| s.to_string()));
let channel = get_u8_val(CHANNEL).and_then(|v| v.as_uint().map(|n| n as u8));
let ifac_netname = get_u8_val(IFAC_NETNAME).and_then(|v| v.as_str().map(|s| s.to_string()));
let ifac_netkey = get_u8_val(IFAC_NETKEY).and_then(|v| v.as_str().map(|s| s.to_string()));
let discovery_hash = compute_discovery_hash(&transport_id, &name);
let config_entry = generate_config_entry(
&interface_type,
&name,
&transport_id,
reachable_on.as_deref(),
port,
frequency,
bandwidth,
spreading_factor,
coding_rate,
modulation.as_deref(),
ifac_netname.as_deref(),
ifac_netkey.as_deref(),
);
let now = time::now();
Some(DiscoveredInterface {
interface_type,
transport,
name,
discovered: now,
last_heard: now,
heard_count: 0,
status: DiscoveredStatus::Available,
stamp: stamp.to_vec(),
stamp_value,
transport_id,
network_id: *announced_identity_hash,
hops,
latitude,
longitude,
height,
reachable_on,
port,
frequency,
bandwidth,
spreading_factor,
coding_rate,
modulation,
channel,
ifac_netname,
ifac_netkey,
config_entry,
discovery_hash,
})
}
pub fn compute_discovery_hash(transport_id: &[u8; 16], name: &str) -> [u8; 32] {
let mut material = Vec::with_capacity(16 + name.len());
material.extend_from_slice(transport_id);
material.extend_from_slice(name.as_bytes());
sha256(&material)
}
fn generate_config_entry(
interface_type: &str,
name: &str,
transport_id: &[u8; 16],
reachable_on: Option<&str>,
port: Option<u16>,
frequency: Option<u32>,
bandwidth: Option<u32>,
spreading_factor: Option<u8>,
coding_rate: Option<u8>,
modulation: Option<&str>,
ifac_netname: Option<&str>,
ifac_netkey: Option<&str>,
) -> Option<String> {
let transport_id_hex = hex_encode(transport_id);
let netname_str = ifac_netname
.map(|n| format!("\n network_name = {}", n))
.unwrap_or_default();
let netkey_str = ifac_netkey
.map(|k| format!("\n passphrase = {}", k))
.unwrap_or_default();
let identity_str = format!("\n transport_identity = {}", transport_id_hex);
match interface_type {
"BackboneInterface" | "TCPServerInterface" => {
let reachable = reachable_on.unwrap_or("unknown");
let port_val = port.unwrap_or(4242);
Some(format!(
"[[{}]]\n type = BackboneInterface\n enabled = yes\n remote = {}\n target_port = {}{}{}{}",
name, reachable, port_val, identity_str, netname_str, netkey_str
))
}
"I2PInterface" => {
let reachable = reachable_on.unwrap_or("unknown");
Some(format!(
"[[{}]]\n type = I2PInterface\n enabled = yes\n peers = {}{}{}{}",
name, reachable, identity_str, netname_str, netkey_str
))
}
"RNodeInterface" => {
let freq_str = frequency
.map(|f| format!("\n frequency = {}", f))
.unwrap_or_default();
let bw_str = bandwidth
.map(|b| format!("\n bandwidth = {}", b))
.unwrap_or_default();
let sf_str = spreading_factor
.map(|s| format!("\n spreadingfactor = {}", s))
.unwrap_or_default();
let cr_str = coding_rate
.map(|c| format!("\n codingrate = {}", c))
.unwrap_or_default();
Some(format!(
"[[{}]]\n type = RNodeInterface\n enabled = yes\n port = {}{}{}{}{}{}{}{}",
name, "", freq_str, bw_str, sf_str, cr_str, identity_str, netname_str, netkey_str
))
}
"KISSInterface" => {
let freq_str = frequency
.map(|f| format!("\n # Frequency: {}", f))
.unwrap_or_default();
let bw_str = bandwidth
.map(|b| format!("\n # Bandwidth: {}", b))
.unwrap_or_default();
let mod_str = modulation
.map(|m| format!("\n # Modulation: {}", m))
.unwrap_or_default();
Some(format!(
"[[{}]]\n type = KISSInterface\n enabled = yes\n port = {}{}{}{}{}{}{}",
name, "", freq_str, bw_str, mod_str, identity_str, netname_str, netkey_str
))
}
"WeaveInterface" => Some(format!(
"[[{}]]\n type = WeaveInterface\n enabled = yes\n port = {}{}{}{}",
name, "", identity_str, netname_str, netkey_str
)),
_ => None,
}
}
pub fn hex_encode(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{:02x}", b)).collect()
}
pub fn is_ip_address(s: &str) -> bool {
s.parse::<std::net::IpAddr>().is_ok()
}
pub fn is_hostname(s: &str) -> bool {
let s = s.strip_suffix('.').unwrap_or(s);
if s.len() > 253 {
return false;
}
let components: Vec<&str> = s.split('.').collect();
if components.is_empty() {
return false;
}
if components
.last()
.map(|c| c.chars().all(|ch| ch.is_ascii_digit()))
.unwrap_or(false)
{
return false;
}
components.iter().all(|c| {
!c.is_empty()
&& c.len() <= 63
&& !c.starts_with('-')
&& !c.ends_with('-')
&& c.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '-')
})
}
pub fn is_discoverable_type(interface_type: &str) -> bool {
DISCOVERABLE_TYPES.contains(&interface_type)
}
pub fn filter_and_sort_interfaces(
interfaces: &mut Vec<DiscoveredInterface>,
only_available: bool,
only_transport: bool,
) {
let now = time::now();
interfaces.retain(|iface| {
if !is_discoverable_type(&iface.interface_type) {
return false;
}
if let Some(ref reachable_on) = iface.reachable_on {
if !(is_ip_address(reachable_on) || is_hostname(reachable_on)) {
return false;
}
}
let delta = now - iface.last_heard;
if delta > THRESHOLD_REMOVE {
return false;
}
let status = iface.compute_status();
if only_available && status != DiscoveredStatus::Available {
return false;
}
if only_transport && !iface.transport {
return false;
}
true
});
interfaces.sort_by(|a, b| {
let status_cmp = b.compute_status().code().cmp(&a.compute_status().code());
if status_cmp != std::cmp::Ordering::Equal {
return status_cmp;
}
let value_cmp = b.stamp_value.cmp(&a.stamp_value);
if value_cmp != std::cmp::Ordering::Equal {
return value_cmp;
}
b.last_heard
.partial_cmp(&a.last_heard)
.unwrap_or(std::cmp::Ordering::Equal)
});
}
pub fn discovery_name_hash() -> [u8; 10] {
rns_core::destination::name_hash(APP_NAME, &["discovery", "interface"])
}
#[cfg(test)]
mod tests {
use super::*;
fn build_discovery_app_data(interface_type: &str, reachable_on: Option<&str>) -> Vec<u8> {
let mut entries = vec![
(
Value::UInt(INTERFACE_TYPE as u64),
Value::Str(interface_type.to_string()),
),
(Value::UInt(TRANSPORT as u64), Value::Bool(true)),
(
Value::UInt(NAME as u64),
Value::Str(format!("test-{interface_type}")),
),
(Value::UInt(TRANSPORT_ID as u64), Value::Bin(vec![0x42; 16])),
];
if let Some(reachable_on) = reachable_on {
entries.push((
Value::UInt(REACHABLE_ON as u64),
Value::Str(reachable_on.to_string()),
));
}
let packed = msgpack::pack(&Value::Map(entries));
let mut app_data = Vec::with_capacity(1 + packed.len() + STAMP_SIZE);
app_data.push(0x00);
app_data.extend_from_slice(&packed);
app_data.extend_from_slice(&[0u8; STAMP_SIZE]);
app_data
}
#[test]
fn parse_rejects_unsupported_discovered_interface_type() {
let app_data = build_discovery_app_data("BogusInterface", None);
let parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0);
assert!(
parsed.is_none(),
"unsupported discovered interface types must be ignored"
);
}
#[test]
fn parse_rejects_invalid_reachable_on_address() {
let app_data = build_discovery_app_data("BackboneInterface", Some("-not a host-"));
let parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0);
assert!(
parsed.is_none(),
"discovered interfaces with invalid reachable_on values must be ignored"
);
}
#[test]
fn parse_accepts_supported_discovered_interface_types() {
for interface_type in [
"BackboneInterface",
"TCPServerInterface",
"I2PInterface",
"RNodeInterface",
"WeaveInterface",
"KISSInterface",
] {
let app_data = build_discovery_app_data(interface_type, Some("example.com"));
let parsed = parse_interface_announce(&app_data, &[0x11; 16], 1, 0);
assert!(
parsed.is_some(),
"{interface_type} should be accepted as a discoverable interface type"
);
}
}
}