Skip to content
This repository has been archived by the owner on Jan 25, 2024. It is now read-only.

Commit

Permalink
gethostbyname/gethostbyaddr: use proper glibc function
Browse files Browse the repository at this point in the history
We internally used getai to restpond to the
gethostbyname/gethostbyaddr operations. Sadly, it does not behave as
expected and breaks some tools (like hostname --fqdn, see
#4.

We FFI the right Glibc gethostbyname_2r/gethostbyaddr_2r (now
deprecated) functions and use it to back the GETHOSTBYNAME,
GETHOSTBYNAME6, GETHOSTBYADDR, and GETHOSTBYADDR6 Nscd interfaces.

Using sockburp, we realized the hostent serialization function was
bogus: we totally forgot to serialize the aliases. This commit fixes
this and makes sure we're producing bit-to-bit identical results with
Nscd for gethostbyname/getaddrinfo.

Took me three try to get this right. This is actually the third
full rewrite.

The Nscd behaviour for these two legacy functions is *really*
confusing. We're supposed to ignore the herrno (herrno != errno!!) and
set it to 0 if gethostbyaddr/name returns a non-null hostent. If we
end up with a null hostent, we return the herrno together with a dummy
hostent header.

I tried to keep things as safe as possible by extracting the glibc
hostent to a proper rust structure. This structure mirrors the libc
hostent in a Rust idiomatic way. We should probably try to upstream
the FFI part of this commit to the Nix crate at some point.

Fixes #4
  • Loading branch information
picnoir committed Oct 20, 2023
1 parent 94f54bb commit 66c79cd
Show file tree
Hide file tree
Showing 4 changed files with 365 additions and 158 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ authors = [
"Geoffrey Thomas <geoffrey.thomas@twosigma.com>",
"Leif Walsh <leif.walsh@twosigma.com>",
]
edition = "2018"
edition = "2021"
description = "The name service non-caching daemon"
readme = "README.md"
repository = "https://github.com/twosigma/nsncd"
Expand Down
256 changes: 255 additions & 1 deletion src/ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@
* limitations under the License.
*/

use nix::libc;
use anyhow::bail;
use nix::libc::{self};
use std::ffi::{CStr, CString};
use std::ptr;

#[allow(non_camel_case_types)]
type size_t = ::std::os::raw::c_ulonglong;
Expand All @@ -40,3 +43,254 @@ pub fn disable_internal_nscd() {
__nss_disable_nscd(do_nothing);
}
}

pub enum LibcIp {
V4([u8; 4]),
V6([u8; 16])
}

mod glibcffi {
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;

pub fn gethostbyaddr_r (
addr: *const libc::c_void,
len: libc::socklen_t,
af: libc::c_int,
ret: *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;
}
}

/// This structure is the Rust counterpart of the `libc::hostent` C
/// function the Libc hostent struct.
///
/// It's mostly used to perform the gethostbyaddr and gethostbyname
/// operations.
///
/// This struct can be serialized to the wire through the
/// `serialize` function or retrieved from the C boundary using the
/// TryFrom `libc:hostent` trait.
#[derive(Clone, Debug)]
pub struct Hostent {
pub name: String,
pub aliases: Vec<String>,
pub addr_type: i32,
pub addr_list: Vec<std::net::IpAddr>,
pub herrno: i32
}

fn from_libc_hostent(value: libc::hostent) -> Result<Hostent, anyhow::Error> {
// 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<String> = Vec::new();
let mut h_alias_ptr = value.h_aliases as *const *const libc::c_char;
while !(unsafe { *h_alias_ptr }).is_null() {
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<std::net::IpAddr> = Vec::new();

// copy the pointer into a private variable that we can mutate
// h_addr_list is a pointer to a list of pointers to addresses.
// h_addr_list[0] => ptr to first address
// h_addr_list[1] => null pointer (end of list)
let mut h_addr_ptr = value.h_addr_list as *const *const libc::c_void;
while !(unsafe { *h_addr_ptr }).is_null() {
if value.h_addrtype == libc::AF_INET {
let octets: [u8; 4] =
unsafe { std::ptr::read((*h_addr_ptr) as *const [u8; 4]) };
addr_list.push(std::net::IpAddr::V4(std::net::Ipv4Addr::from(octets)));
} else {
let octets: [u8; 16] =
unsafe { std::ptr::read((*h_addr_ptr) as *const [u8; 16]) };
addr_list.push(std::net::IpAddr::V6(std::net::Ipv6Addr::from(octets)));
}
unsafe { h_addr_ptr = h_addr_ptr.add(1) };
}

Hostent {
name,
aliases,
addr_type: value.h_addrtype,
addr_list,
herrno: 0
}
})
}

/// Decodes the result of a gethostbyname/addr call into a `Hostent`
/// Rust struct.
///
/// This decoding algorithm is quite confusing, but that's how it's
/// implemented in Nscd and what the client Glibc expects. We
/// basically always ignore `herrno` except if the resulting
/// `libc::hostent` is set to null by glibc.
fn unmarshal_gethostbyxx(hostent: *mut libc::hostent, herrno: libc::c_int) -> anyhow::Result<Hostent> {
if !hostent.is_null() {
unsafe {
let res = from_libc_hostent(*hostent)?;
Ok(res)
}
} else {
Ok(
// This is a default hostent header we're supposed to use
// to convey a lookup error. This is a glibc quirk, I have
// nothing to do with that, don't blame me :)
Hostent {
name: "".to_string(),
aliases: Vec::new(),
addr_type: -1,
addr_list: Vec::new(),
herrno
}
)
}
}

pub fn gethostbyaddr_r(addr: LibcIp) -> anyhow::Result<Hostent> {

let (addr, len, af) = match addr {
LibcIp::V4(ref ipv4) => (ipv4 as &[u8], 4, libc::AF_INET),
LibcIp::V6(ref ipv6) => (ipv6 as &[u8], 16, libc::AF_INET6)
};

let mut ret_hostent: libc::hostent = libc::hostent {
h_name: ptr::null_mut(),
h_aliases: ptr::null_mut(),
h_addrtype: 0,
h_length: 0,
h_addr_list: ptr::null_mut(),
};
let mut herrno: libc::c_int = 0;
let mut hostent_result = ptr::null_mut();
let mut buf: Vec<u8> = Vec::with_capacity(200);
loop {
let ret = unsafe {
glibcffi::gethostbyaddr_r(
addr.as_ptr() as *const libc::c_void,
len,
af,
&mut ret_hostent,
buf.as_mut_ptr() as *mut libc::c_char,
(buf.capacity() as size_t).try_into().unwrap(),
&mut hostent_result,
&mut herrno
)
};

if ret == libc::ERANGE {
buf.reserve(buf.capacity() * 2);
} else {
break;
}
};
unmarshal_gethostbyxx(hostent_result, herrno)
}

///
///
/// 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<Hostent> {
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 herrno: libc::c_int = 0;
let mut buf: Vec<u8> = 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 _: i32 = loop {
let ret = unsafe {
glibcffi::gethostbyname2_r(
name.as_ptr(),
af,
&mut ret_hostent,
buf.as_mut_ptr() as *mut libc::c_char,
(buf.capacity() as size_t).try_into().unwrap(),
&mut hostent_result,
&mut herrno,
)
};
if ret == libc::ERANGE {
// The buffer is too small. Let's x2 its capacity and retry.
buf.reserve(buf.capacity() * 2);
} else {
break ret;
}
};
unmarshal_gethostbyxx(hostent_result, herrno)
}

#[test]
fn test_gethostbyname2_r() {
disable_internal_nscd();

let result: Result<Hostent, anyhow::Error> = gethostbyname2_r(
"localhost.".to_string(),
libc::AF_INET,
);

result.expect("Should resolve IPv4 localhost.");

let result: Result<Hostent, anyhow::Error> = gethostbyname2_r(
"localhost.".to_string(),
libc::AF_INET6,
);
result.expect("Should resolve IPv6 localhost.");
}

#[test]
fn test_gethostbyaddr_r() {
disable_internal_nscd();

let v4test = LibcIp::V4([127,0,0,1]);
let _ = gethostbyaddr_r(v4test).expect("Should resolve IPv4 localhost with gethostbyaddr");

let v6test = LibcIp::V6([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1]);
let _ = gethostbyaddr_r(v6test).expect("Should resolve IPv6 localhost with gethostbyaddr");
}
Loading

0 comments on commit 66c79cd

Please sign in to comment.