nvpn 4.1.3

CLI and daemon for Nostr VPN private mesh networks
Documentation
#[cfg(any(target_os = "linux", test))]
#[derive(serde::Deserialize)]
struct LinuxIpInterfaceState {
    #[serde(default)]
    flags: Vec<String>,
    mtu: u16,
    #[serde(default)]
    txqlen: Option<usize>,
    #[serde(default)]
    addr_info: Vec<LinuxIpAddressState>,
}

#[cfg(any(target_os = "linux", test))]
#[derive(serde::Deserialize)]
struct LinuxIpAddressState {
    local: String,
    prefixlen: u8,
}

#[cfg(any(target_os = "linux", test))]
fn linux_interface_state_matches_json(
    raw: &str,
    addresses: &[String],
    mtu: u16,
    tx_queue_len: Option<usize>,
) -> bool {
    let Ok(interfaces) = serde_json::from_str::<Vec<LinuxIpInterfaceState>>(raw) else {
        return false;
    };
    let [interface] = interfaces.as_slice() else {
        return false;
    };
    if interface.mtu != mtu
        || !interface.flags.iter().any(|flag| flag == "UP")
        || tx_queue_len.is_some_and(|expected| interface.txqlen != Some(expected))
    {
        return false;
    }

    let actual = interface
        .addr_info
        .iter()
        .filter_map(|address| {
            address
                .local
                .parse::<IpAddr>()
                .ok()
                .map(|ip| (ip, address.prefixlen))
        })
        .collect::<HashSet<_>>();
    addresses.iter().all(|address| {
        let (ip, prefix_len) = address.split_once('/').map_or(
            (address.as_str(), None),
            |(ip, prefix_len)| (ip, Some(prefix_len)),
        );
        let Ok(ip) = ip.parse::<IpAddr>() else {
            return false;
        };
        let prefix_len = match prefix_len {
            Some(prefix_len) => match prefix_len.parse::<u8>() {
                Ok(prefix_len)
                    if (ip.is_ipv4() && prefix_len <= 32)
                        || (ip.is_ipv6() && prefix_len <= 128) =>
                {
                    prefix_len
                }
                _ => return false,
            },
            None if ip.is_ipv4() => 32,
            None => 128,
        };
        actual.contains(&(ip, prefix_len))
    })
}

#[cfg(target_os = "linux")]
fn linux_interface_state_matches(
    iface: &str,
    addresses: &[String],
    mtu: u16,
    tx_queue_len: Option<usize>,
) -> bool {
    let output = ProcessCommand::new("ip")
        .arg("-j")
        .arg("address")
        .arg("show")
        .arg("dev")
        .arg(iface)
        .output();
    let Ok(output) = output else {
        return false;
    };
    output.status.success()
        && std::str::from_utf8(&output.stdout).is_ok_and(|raw| {
            linux_interface_state_matches_json(raw, addresses, mtu, tx_queue_len)
        })
}