aegis-tool 0.4.4

Aegis SSH client and managed host agent.
Documentation
use std::net::IpAddr;
use std::process::Command;

pub fn preferred_wireguard_endpoint_ip(endpoints: &[String]) -> anyhow::Result<Option<String>> {
    Ok(preferred_wireguard_endpoint_ip_with_ipv6_support(
        endpoints,
        system_supports_public_ipv6()?,
    ))
}

pub fn wireguard_endpoint_ipv4(endpoints: &[String]) -> Option<String> {
    endpoints.iter().find_map(|endpoint| {
        endpoint
            .parse::<IpAddr>()
            .ok()
            .filter(IpAddr::is_ipv4)
            .map(|_| endpoint.clone())
    })
}

pub fn system_supports_public_ipv6() -> anyhow::Result<bool> {
    #[cfg(target_os = "linux")]
    let output = crate::command::require_success(
        "inspect public IPv6 route",
        Command::new("ip").args(["-6", "route", "show", "default"]),
    )?;
    #[cfg(target_os = "macos")]
    let output = crate::command::require_success(
        "inspect public IPv6 route",
        Command::new("/usr/sbin/netstat").args(["-rn", "-f", "inet6"]),
    )?;
    Ok(output
        .stdout
        .lines()
        .any(|line| line.split_whitespace().next() == Some("default")))
}

pub fn preferred_wireguard_endpoint_ip_with_ipv6_support(
    endpoints: &[String],
    ipv6_supported: bool,
) -> Option<String> {
    let mut ipv4 = None;
    let mut ipv6 = None;

    for endpoint in endpoints {
        match endpoint.parse::<IpAddr>().ok()? {
            IpAddr::V4(_) if ipv4.is_none() => ipv4 = Some(endpoint.clone()),
            IpAddr::V6(_) if ipv6.is_none() => ipv6 = Some(endpoint.clone()),
            _ => {}
        }
    }

    if ipv6_supported {
        ipv6.or(ipv4)
    } else {
        ipv4.or(ipv6)
    }
}

#[cfg(test)]
mod tests {
    use super::{preferred_wireguard_endpoint_ip_with_ipv6_support, wireguard_endpoint_ipv4};

    #[test]
    fn ipv4_only_selection_never_falls_back_to_ipv6() {
        let dual_stack = vec!["2600:1900:4000:fec::1".to_string(), "34.1.2.3".to_string()];
        assert_eq!(
            Some("34.1.2.3".to_string()),
            wireguard_endpoint_ipv4(&dual_stack)
        );
        assert_eq!(
            None,
            wireguard_endpoint_ipv4(&["2600:1900:4000:fec::1".to_string()])
        );
    }

    #[test]
    fn prefers_ipv6_when_supported() {
        let endpoints = vec!["34.1.2.3".to_string(), "2600:1900:4000:fec::1".to_string()];
        assert_eq!(
            Some("2600:1900:4000:fec::1".to_string()),
            preferred_wireguard_endpoint_ip_with_ipv6_support(&endpoints, true)
        );
    }

    #[test]
    fn falls_back_to_ipv4_when_ipv6_is_not_supported() {
        let endpoints = vec!["2600:1900:4000:fec::1".to_string(), "34.1.2.3".to_string()];
        assert_eq!(
            Some("34.1.2.3".to_string()),
            preferred_wireguard_endpoint_ip_with_ipv6_support(&endpoints, false)
        );
    }

    #[test]
    fn still_returns_ipv6_when_it_is_the_only_option() {
        let endpoints = vec!["2600:1900:4000:fec::1".to_string()];
        assert_eq!(
            Some("2600:1900:4000:fec::1".to_string()),
            preferred_wireguard_endpoint_ip_with_ipv6_support(&endpoints, false)
        );
    }
}