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};
pub fn resolve_rand(name: &str, family: Option<i32>) -> Result<IpAddr, Errno> {
let mut buf = [0u8; 4];
if fillrandom(&mut buf).is_err() {
return Err(Errno::EIO); }
let cookie = usize::try_from(u32::from_ne_bytes(buf)).or(Err(Errno::EOVERFLOW))?;
let addrs = resolve_host(name, family)?;
#[expect(clippy::arithmetic_side_effects)]
Ok(addrs[cookie.wrapping_rem(addrs.len())])
}
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, };
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() {
return Err(Errno::ENOENT);
}
Ok(addrs.iter().copied().collect())
}
#[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];
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 => {
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());
}
}
}