From decb9e5e2f9d3b40607228a2476f3542ca56ea41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Baylac=20Jacqu=C3=A9?= Date: Sun, 8 Oct 2023 11:25:04 +0200 Subject: [PATCH] Gethostbyname: use proper glibc function We internally used getai to restpond to the gethostbyname operations. Sadly, it did not behave as expected and broke some tools (like hostname --fqdn, see https://github.com/nix-community/nsncd/issues/4. We FFI the right Glibc gethostbyname_2r (now deprecated) function and use it to back the GETHOSTBYNAME and GETHOSTBYNAME6 Nscd interfaces. Fixes https://github.com/nix-community/nsncd/issues/4 --- Cargo.toml | 2 +- src/ffi.rs | 187 +++++++++++++++++++++++++++++++++++++++++++++++- src/handlers.rs | 61 +++++----------- 3 files changed, 203 insertions(+), 47 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index daff366..497d2a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ authors = [ "Geoffrey Thomas ", "Leif Walsh ", ] -edition = "2018" +edition = "2021" description = "The name service non-caching daemon" readme = "README.md" repository = "https://github.com/twosigma/nsncd" diff --git a/src/ffi.rs b/src/ffi.rs index 78a549d..37fa519 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -14,7 +14,11 @@ * limitations under the License. */ -use nix::libc; +use anyhow::bail; +use nix::libc::{self}; +use nix::errno::Errno; +use std::ffi::{CStr, CString}; +use std::ptr; #[allow(non_camel_case_types)] type size_t = ::std::os::raw::c_ulonglong; @@ -40,3 +44,184 @@ pub fn disable_internal_nscd() { __nss_disable_nscd(do_nothing); } } + +mod ffi { + use nix::libc; + extern "C" { + pub fn gethostbyname2_r ( + name: *const libc::c_char, + af: libc::c_int, + result_buf: *mut libc::hostent, + buf: *mut libc::c_char, + buflen: libc::size_t, + result: *mut *mut libc::hostent, + h_errnop: *mut libc::c_int, + ) -> libc::c_int; + } +} + +#[derive(Clone, Debug)] +pub struct Hostent { + pub name: String, + pub aliases: Vec, + pub addr_type: i32, + pub addr_list: Vec, +} + +impl TryFrom for Hostent { + type Error = anyhow::Error; + + fn try_from(value: libc::hostent) -> Result { + // validate value.h_addtype, and bail out if it's unsupported + if value.h_addrtype != libc::AF_INET && value.h_addrtype != libc::AF_INET6 { + bail!("unsupported address type: {}", value.h_addrtype); + } + + // ensure value.h_length matches what we know from this address family + if value.h_addrtype == libc::AF_INET && value.h_length != 4 { + bail!("unsupported h_length for AF_INET: {}", value.h_length); + } + if value.h_addrtype == libc::AF_INET6 && value.h_length != 16 { + bail!("unsupported h_length for AF_INET6: {}", value.h_length); + } + + let name = unsafe { CStr::from_ptr(value.h_name).to_str().unwrap().to_string() }; + + Ok({ + // construct the list of aliases. keep adding to value.h_aliases until we encounter a null pointer. + let mut aliases: Vec = Vec::new(); + let mut h_alias_ptr = value.h_aliases; + loop { + if unsafe {(*h_alias_ptr).is_null()} { + break; + } else { + aliases.push(unsafe { + CStr::from_ptr(*h_alias_ptr).to_str().unwrap().to_string() + }); + // increment + unsafe { + h_alias_ptr = h_alias_ptr.add(1); + } + } + } + + // value.h_addrtype + + // construct the list of addresses. + let mut addr_list: Vec = Vec::new(); + + // copy the pointer into a private variable that we can mutate + let mut h_addr_ptr = value.h_addr_list; + loop { + if unsafe { (*h_addr_ptr).is_null() } { + break; + } else { + // read the value + let mut b: Vec = Vec::new(); + // h_length is the length of the address in bytes (4 or 16), + // which is ensured by our checks at the beginning of the function. + for _ in 0..value.h_length { + unsafe { + b.push(**h_addr_ptr as u8 ); + *h_addr_ptr = (*h_addr_ptr).add(1); + } + } + + if value.h_addrtype == libc::AF_INET { + addr_list.push({ + let octets: [u8; 4] = b.try_into().unwrap(); + std::net::IpAddr::V4(std::net::Ipv4Addr::from(octets)) + }); + } else { + addr_list.push({ + let octets: [u8; 16] = b.try_into().unwrap(); + std::net::IpAddr::V6(std::net::Ipv6Addr::from(octets)) + }); + } + + // increment the pointer + unsafe { h_addr_ptr = h_addr_ptr.add(1) }; + } + } + + Hostent { + name, + aliases, + addr_type: value.h_addrtype, + addr_list, + } + }) + } +} + +/// +/// +/// The stream is positioned at the first entry in the directory. +/// +/// af is nix::libc::AF_INET6 or nix::libc::AF_INET6 +pub fn gethostbyname2_r(name: String, af: libc::c_int) -> anyhow::Result { + let name = CString::new(name).unwrap(); + + // Prepare a libc::hostent and the pointer to the result list, + // which will passed to the ffi::gethostbyname2_r call. + let mut ret_hostent: libc::hostent = libc::hostent { + h_name: ptr::null_mut(), // <- points to buf + h_aliases: ptr::null_mut(), + h_addrtype: 0, + h_length: 0, + h_addr_list: ptr::null_mut(), + }; + let mut err: libc::c_int = 0; + let mut buf: Vec = Vec::with_capacity(200); + // We absolutely need to point hostent_result to the start of the + // result buffer. Glibc segfaults if we don't. + let mut hostent_result = ptr::null_mut(); + let ret: i32 = loop { + let ret = unsafe { + ffi::gethostbyname2_r( + name.as_ptr(), + af, + &mut ret_hostent, + buf.as_mut_ptr() as *mut i8, + (buf.capacity() as size_t).try_into().unwrap(), + &mut hostent_result, + &mut err, + ) + }; + if ret == libc::ERANGE { + // The buffer is too small. Let's x2 its capacity and retry. + buf.reserve(buf.capacity() * 2); + } else { + break ret; + } + }; + if err == 0 && ret == 0 && ! hostent_result.is_null() { + unsafe { + let res = (*hostent_result).try_into().unwrap(); + Ok(res) + } + } else { + // errno + Err(anyhow::anyhow!(Errno::from_i32(err))) + } +} + +#[test] +fn test_gethostbyname2_r() { + disable_internal_nscd(); + + let result: Result = gethostbyname2_r( + "localhost.".to_string(), + libc::AF_INET, + ); + + result.expect("Should resolve IPv4 localhost."); + + let result: Result = gethostbyname2_r( + "localhost.".to_string(), + libc::AF_INET6, + ); + + let res = result.expect("Should resolve IPv6 localhost."); + println!("{:?}", res); +} diff --git a/src/handlers.rs b/src/handlers.rs index 075f4b3..bc37d73 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -29,6 +29,7 @@ use nix::unistd::{getgrouplist, Gid, Group, Uid, User}; use slog::{debug, error, Logger}; use std::mem::size_of; +use crate::ffi::gethostbyname2_r; use crate::protocol::{AiResponse, AiResponseHeader}; use super::config::Config; @@ -180,6 +181,7 @@ pub fn handle_request( let address = IpAddr::from(address_bytes); let sock = SocketAddr::new(address, 0); + // TODO: why getnameinfo, replace with gethostbyaddr let host = match getnameinfo(&sock, NI_NUMERICSERV) { Ok((hostname, _service)) => Ok(Some(Host { addresses: vec![address], @@ -217,60 +219,29 @@ pub fn handle_request( RequestType::GETHOSTBYNAME => { let hostname = CStr::from_bytes_with_nul(request.key)?.to_str()?; - let hints = AddrInfoHints { - socktype: SOCK_STREAM, - ..AddrInfoHints::default() + let hostent = match gethostbyname2_r(hostname.to_string(), nix::libc::AF_INET) { + Ok(hostent) => hostent, + Err(e) => bail!("error during lookup: {:?}", e) }; - - let host = match getaddrinfo(Some(hostname), None, Some(hints)).map(|addrs| { - addrs - .filter_map(|r| r.ok()) - .filter(|r| r.sockaddr.is_ipv4()) - .map(|a| a.sockaddr.ip()) - .collect::>() - }) { - // no matches found - Ok(addresses) if addresses.len() == 0 => Ok(None), - Ok(addresses) => Ok(Some(Host { - addresses, - hostname: hostname.to_string(), - })), - Err(e) => match e.kind() { - dns_lookup::LookupErrorKind::NoName => Ok(None), - _ => bail!("error during lookup: {:?}", e), - }, + let host = Host { + addresses: hostent.addr_list, + hostname: hostent.name }; - Ok(serialize_host(log, host)) + host.serialize() } RequestType::GETHOSTBYNAMEv6 => { let hostname = CStr::from_bytes_with_nul(request.key)?.to_str()?; - let hints = AddrInfoHints { - socktype: SOCK_STREAM, - address: AF_INET6, // ai_family - ..AddrInfoHints::default() + let hostent = match gethostbyname2_r(hostname.to_string(), nix::libc::AF_INET6) { + Ok(hostent) => hostent, + Err(e) => bail!("error during lookup: {:?}", e) }; - - let host = match getaddrinfo(Some(hostname), None, Some(hints)).map(|addrs| { - addrs - .filter_map(|r| r.ok()) - .filter(|r| r.sockaddr.is_ipv6()) - .map(|a| a.sockaddr.ip()) - .collect::>() - }) { - // No matches found - Ok(addresses) if addresses.len() == 0 => Ok(None), - Ok(addresses) => Ok(Some(Host { - addresses, - hostname: hostname.to_string(), - })), - Err(e) => match e.kind() { - dns_lookup::LookupErrorKind::NoName => Ok(None), - _ => bail!("error during lookup: {:?}", e), - }, + let host = Host { + addresses: hostent.addr_list, + hostname: hostent.name }; - Ok(serialize_host(log, host)) + host.serialize() } // These will normally send an FD pointing to the internal cache structure,