syd 3.52.0

rock-solid application kernel
Documentation
//
// Syd: rock-solid application kernel
// src/proc.rs: DNS utilities
//
// Copyright (c) 2024, 2025 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::{
    ffi::{CStr, OsString},
    net::{IpAddr, SocketAddr, SocketAddrV4, SocketAddrV6, ToSocketAddrs},
    os::unix::ffi::OsStringExt,
    ptr,
};

use libc::{
    c_char, getnameinfo, AF_INET, AF_INET6, AF_UNSPEC, EAI_AGAIN, EAI_BADFLAGS, EAI_FAIL,
    EAI_FAMILY, EAI_MEMORY, EAI_NONAME, EAI_SERVICE, EAI_SOCKTYPE, EAI_SYSTEM, NI_MAXHOST,
    NI_NAMEREQD, NI_NUMERICSERV,
};
use nix::{
    errno::Errno,
    sys::socket::{SockaddrLike, SockaddrStorage},
};

use crate::{err::err2no, hash::SydIndexSet, rng::fillrandom};

/// Resolves a hostname to a single IP address.
/// In case of multiple responses, an IP is selected randomly.
/// Randomness is provided by getrandom(2).
pub fn resolve_rand(name: &str, family: Option<i32>) -> Result<IpAddr, Errno> {
    // Read random bytes with getrandom(2) and convert to usize.
    // Note, getrandom(2) is soon to be in the VDSO!
    let mut buf = [0u8; 4];
    if fillrandom(&mut buf).is_err() {
        return Err(Errno::EIO); // Input/output error.
    }
    // Convert bytes to a usize.
    let cookie = usize::try_from(u32::from_ne_bytes(buf)).or(Err(Errno::EOVERFLOW))?;

    // Resolve hostname.
    let addrs = resolve_host(name, family)?;

    // Select a random IP address from the list.
    // Calculate random index within the bounds of the addresses vector.
    #[expect(clippy::arithmetic_side_effects)]
    Ok(addrs[cookie.wrapping_rem(addrs.len())])
}

/// Resolves a hostname using the system DNS resolver.
pub fn resolve_host(name: &str, family: Option<i32>) -> Result<Vec<IpAddr>, Errno> {
    let ai_family = match family {
        Some(AF_INET) => AF_INET,
        Some(AF_INET6) => AF_INET6,
        Some(_) => return Err(Errno::EINVAL),
        None => AF_UNSPEC, // Allow IPv4 or IPv6.
    };

    // Create an SydIndexSet to store unique IPs
    // while preserving insertion order.
    let addrs: SydIndexSet<IpAddr> = SydIndexSet::from_iter(
        (name, 22)
            .to_socket_addrs()
            .map_err(|err| err2no(&err))?
            .filter(|addr| {
                matches!(
                    (ai_family, addr),
                    (AF_UNSPEC, _) | (AF_INET, SocketAddr::V4(_)) | (AF_INET6, SocketAddr::V6(_))
                )
            })
            .map(|addr| addr.ip()),
    );

    if addrs.is_empty() {
        // No addresses were found.
        return Err(Errno::ENOENT);
    }

    Ok(addrs.iter().copied().collect())
}

/// Performs a reverse DNS lookup for the given IP address, returning a hostname or an error.
#[expect(clippy::cast_possible_truncation)]
pub fn lookup_addr(addr: IpAddr) -> Result<OsString, Errno> {
    let addr = match addr {
        IpAddr::V4(v4) => SockaddrStorage::from(SocketAddrV4::new(v4, 0)),
        IpAddr::V6(v6) => SockaddrStorage::from(SocketAddrV6::new(v6, 0, 0, 0)),
    };
    let mut host_buf = [0 as c_char; NI_MAXHOST as usize];

    // SAFETY: We call a system function (getnameinfo) with valid pointers for the address
    // and buffer, and we check the return value to ensure success before using `host_buf`.
    let ret = unsafe {
        #[cfg(target_os = "android")]
        {
            getnameinfo(
                addr.as_ptr(),
                addr.len(),
                host_buf.as_mut_ptr(),
                host_buf.len() as usize,
                ptr::null_mut(),
                0,
                NI_NAMEREQD | NI_NUMERICSERV,
            )
        }
        #[cfg(not(target_os = "android"))]
        {
            getnameinfo(
                addr.as_ptr(),
                addr.len(),
                host_buf.as_mut_ptr(),
                host_buf.len() as libc::socklen_t,
                ptr::null_mut(),
                0,
                NI_NAMEREQD | NI_NUMERICSERV,
            )
        }
    };

    match ret {
        0 => {
            // SAFETY: On success, `host_buf` contains a valid null-terminated string.
            let cstr = unsafe { CStr::from_ptr(host_buf.as_ptr()) };
            Ok(OsString::from_vec(cstr.to_bytes().into()))
        }
        EAI_SYSTEM => Err(Errno::last()),
        EAI_AGAIN => Err(Errno::EAGAIN),
        EAI_BADFLAGS => Err(Errno::EINVAL),
        EAI_FAIL => Err(Errno::EIO),
        EAI_FAMILY => Err(Errno::EAFNOSUPPORT),
        EAI_MEMORY => Err(Errno::ENOMEM),
        EAI_NONAME => Err(Errno::ENOENT),
        EAI_SERVICE => Err(Errno::EPROTONOSUPPORT),
        EAI_SOCKTYPE => Err(Errno::ESOCKTNOSUPPORT),
        _ => Err(Errno::EIO),
    }
}

#[cfg(test)]
mod tests {
    use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};

    use super::*;

    #[test]
    fn test_resolve_host_1() {
        let result = resolve_host("localhost", None);
        assert!(result.is_ok(), "resolve_host(localhost) failed: {result:?}");
        let addrs = result.unwrap();
        assert!(!addrs.is_empty());
    }

    #[test]
    fn test_resolve_host_2() {
        let result = resolve_host("localhost", Some(AF_INET));
        if let Ok(addrs) = result {
            for addr in &addrs {
                assert!(addr.is_ipv4(), "expected IPv4 only");
            }
        }
    }

    #[test]
    fn test_resolve_host_3() {
        let result = resolve_host("localhost", Some(AF_INET6));
        if let Ok(addrs) = result {
            for addr in &addrs {
                assert!(addr.is_ipv6(), "expected IPv6 only");
            }
        }
    }

    #[test]
    fn test_resolve_host_4() {
        let result = resolve_host("localhost", Some(999));
        assert_eq!(result, Err(Errno::EINVAL));
    }

    #[test]
    fn test_resolve_host_5() {
        let result = resolve_host("this.host.definitely.does.not.exist.invalid", None);
        assert!(result.is_err());
    }

    #[test]
    fn test_resolve_rand_1() {
        let result = resolve_rand("localhost", None);
        assert!(result.is_ok() || result.is_err());
        if let Ok(addr) = result {
            assert!(addr.is_ipv4() || addr.is_ipv6());
        }
    }

    #[test]
    fn test_lookup_addr_1() {
        let addr = IpAddr::V4(Ipv4Addr::LOCALHOST);
        let result = lookup_addr(addr);
        if let Ok(name) = result {
            assert!(!name.is_empty());
        }
    }

    #[test]
    fn test_lookup_addr_2() {
        let addr = IpAddr::V6(Ipv6Addr::LOCALHOST);
        let result = lookup_addr(addr);
        if let Ok(name) = result {
            assert!(!name.is_empty());
        }
    }
}