libnss_host4/lib.rs
1//! Implementing the trait and macro in this crate will publish a
2//! `gethostbyname4_r` NSS hook in your Rust `cdylib`.
3//!
4//! # Example
5//!
6//! ```
7//! use libnss_host4::{Addr, HostResolver, err::NssErr, impl_gethostbyname4_r};
8//! use std::net::Ipv6Addr;
9//!
10//! /// This resolver maps "localhost" to [::1%0].
11//! struct LocalDns;
12//! impl_gethostbyname4_r!(local, LocalDns);
13//!
14//! impl HostResolver for LocalDns {
15//! fn resolve_host(hostname: &str) -> Result<impl IntoIterator<Item = Addr>, NssErr> {
16//! if hostname == "localhost" {
17//! return Ok([Addr {
18//! ip: Ipv6Addr::LOCALHOST.into(),
19//! scope_id: 0,
20//! }]);
21//! }
22//! Err(NssErr::NO_RESULT)
23//! }
24//! }
25//! ```
26//!
27//! # Background
28//!
29//! glibc defines a Name Service Switch interface for querying hostnames.
30//!
31//! <https://sourceware.org/glibc/manual/2.43/html_mono/libc.html#Host-Names>
32//!
33//! This once-simple lookup API has unfortunately degenerated into a sedimentary
34//! chaos of numbered functions differentiated by reentrance and lack thereof.
35//! An early indication of which is the conspicuous absence of `gethostbyname3_r`
36//! and `gethostbyname4_r` in the docs linked above.
37//!
38//! However, as of writing, `gethostbyname4_r` is the only NSS host hook that
39//! can return IPv6 addresses with a `scope_id`, which makes it a big deal
40//! for the chosen few who care about such things.
41//!
42//! Other Rust NSS usage is already well supported by the [libnss crate](https://crates.io/crates/libnss).
43//! The motivating cause for this crate cannot accomodate its GPL license,
44//! which is why this is standalone. Presumably both crates can be used in
45//! the same `cdylib` to cover the full NSS host API.
46//!
47//! If the other hooks aren't needed, though, a cdylib with just `gethostbyname4_r`
48//! is sufficient for `getaddrinfo`-based resolution via `/etc/nsswitch.conf`.
49
50// This crate was previously `no_std`. It still could be if not for `std::panic::catch_unwind`.
51// But `catch_unwind` is warranted since uncaught panic across FFI is terrible,
52// especially in the types of apps that use NSS hooks. Panic is unavoidably possible
53// since this wraps unknown user code.
54
55mod buf;
56pub mod err;
57
58use core::ffi::CStr;
59use core::net::Ipv4Addr;
60use core::net::Ipv6Addr;
61use std::net::IpAddr;
62
63use crate::buf::Gaih4Buf;
64use crate::err::NssErr;
65use crate::err::NssStatus;
66
67#[doc(hidden)]
68pub mod _macro_internal {
69 pub use libc;
70 pub use paste;
71}
72
73/// This macro expands into an NSS-compatible hook for the `gethostbyname4_r`
74/// hostname resolution API.
75///
76/// # Safety
77///
78/// There must not be any other exported function named `_nss_{name}_gethostbyname4_r`
79/// in your `cdylib`.
80#[macro_export]
81macro_rules! impl_gethostbyname4_r {
82 ($nss_name:ident, $resolver:ident) => {
83 $crate::_macro_internal::paste::paste! {
84 #[unsafe(no_mangle)]
85 pub unsafe extern "C" fn [<_nss_ $nss_name _gethostbyname4_r>](
86 name: *const $crate::_macro_internal::libc::c_char,
87 pat: *mut *mut $crate::GaihAddrTuple,
88 buffer: *mut $crate::_macro_internal::libc::c_char,
89 buflen: $crate::_macro_internal::libc::size_t,
90 errnop: *mut $crate::_macro_internal::libc::c_int,
91 h_errnop: *mut $crate::_macro_internal::libc::c_int,
92 ttlp: *mut $crate::_macro_internal::libc::c_int,
93 ) -> $crate::_macro_internal::libc::c_int {
94 std::panic::catch_unwind(|| {
95 unsafe { $crate::gethostbyname4_r::<$resolver>(name, pat, buffer, buflen, errnop, h_errnop, ttlp) }
96 }).unwrap_or_else(|_| {
97 unsafe {
98 if !errnop.is_null() {
99 *errnop = $crate::_macro_internal::libc::EIO;
100 }
101 if !h_errnop.is_null() {
102 *h_errnop = $crate::err::HostStatus::NoRecovery as i32;
103 }
104 }
105 $crate::err::NssStatus::Unavailable as i32
106 })
107 }
108 }
109 };
110}
111
112/// An address that can be returned from gethostbyname4_r.
113#[derive(Debug, PartialEq, Eq, Clone, Copy)]
114pub struct Addr {
115 /// The IP address that was resolved.
116 pub ip: IpAddr,
117
118 /// This is typically only used for IPv6.
119 ///
120 /// Zero is a safe default if you're using IPv4 or don't know
121 /// what to put here.
122 //
123 // Leaving this as an option in IPv4 to enable whatever shenanigans
124 // the API user might be up to.
125 pub scope_id: u32,
126}
127
128/// Implement this trait with the actual address business logic
129/// that `gethostbyname4_r` should expose. The C interop layer
130/// simply wraps the resolution defined here.
131pub trait HostResolver {
132 /// Returns zero or more host addresses matching the hostname query
133 /// or an NSS-contextualized error on failure.
134 fn resolve_host(hostname: &str) -> Result<impl IntoIterator<Item = Addr>, NssErr>;
135
136 /// Optionally sets the "Time to Live Pointer" for the given
137 /// hostname's NSS result. This influences address cache lifespan.
138 ///
139 /// It is perfectly fine to ignore this. Only implement it if you
140 /// have a reason.
141 ///
142 /// This function is only invoked if the caller's TTLP is not null,
143 /// and returning None will skip writing to the pointer entirely.
144 fn set_ttlp(hostname: &str) -> Option<i32> {
145 let _ = hostname;
146 None
147 }
148}
149
150/// GETHOSTBYNAME4_R
151///
152/// This majestically-named function is used by glibc's `getaddrinfo`
153/// lookup when the "simple, old functions" are unsuitable. The motivating
154/// case is IPv6 scope IDs:
155///
156/// <https://github.com/lattera/glibc/blob/895ef79e04a953cac1493863bcae29ad85657ee1/sysdeps/posix/getaddrinfo.c#L563-L565>
157///
158/// Authoritative docs for implementing this API were elusive, so this
159/// effort is based largely on avahi nss-mdns source. My own understanding of
160/// this API is documented here in-excess with the hope that anything incorrect
161/// can be swiftly identified and fixed. If it is somehow fully correct, then
162/// it may also be a useful reference for others implementing NSS hooks.
163///
164/// # Safety
165///
166/// This function should never be called outside the NSS lookup path.
167/// Within glibc NSS, this implementation expects the following:
168///
169/// - `name` is a valid C string.
170/// - `*pat` is always a valid pointer. `**pat` may be either NULL or a valid
171/// `GaihAddrTuple` into which the first NSS result is written. The caller
172/// will only explore this list if it receives a success return value.
173/// - `buffer` + `buflen` are equivalent to a `&mut [u8]` with all the expectations
174/// byte slices carry in safe rust.
175/// - `errnop` and `h_errnop` are safe to dereference.
176/// - `ttlp` is either NULL or safe to dereference.
177///
178/// # Returns
179///
180/// Return value is an enum defined here:
181///
182/// <https://github.com/lattera/glibc/blob/895ef79e04a953cac1493863bcae29ad85657ee1/nss/nss.h#L30-L38>
183#[inline]
184#[doc(hidden)]
185pub unsafe fn gethostbyname4_r<R: HostResolver>(
186 // The hostname to be resolved. This is a null-terminated C-string and
187 // must not be used in the returned gaih_addrtuple. The gaih_addrtuple
188 // name should be stored within the given return buffer:
189 //
190 // https://github.com/avahi/nss-mdns/blob/3292b172ce0100a1aed8b67c381760bc3fb87f2e/src/util.c#L234-L236
191 name: *const libc::c_char,
192
193 // "Pointer to Address Tuple"
194 // Pointer to the linked list node in which this function's results are stored.
195 // Said list must live entirely within the given buffer.
196 //
197 // HOWEVER, if `*pat` is not null, then the first node in the list should
198 // be placed there, and all subsequent nodes should live in the buffer.
199 //
200 // https://github.com/avahi/nss-mdns/blob/3292b172ce0100a1aed8b67c381760bc3fb87f2e/src/util.c#L242-L255
201 pat: *mut *mut GaihAddrTuple,
202
203 // A buffer in which all results must be stored including the hostname.
204 buffer: *mut libc::c_char,
205
206 // The length of this buffer in bytes.
207 buflen: libc::size_t,
208
209 // A canonical linux error code.
210 errnop: *mut libc::c_int,
211
212 // "Host" lookup errno. Extends the standard errno.
213 //
214 // https://github.com/lattera/glibc/blob/895ef79e04a953cac1493863bcae29ad85657ee1/resolv/netdb.h#L62-L69
215 h_errnop: *mut libc::c_int,
216
217 // DNS time to live hint.
218 //
219 // NCSD initializes it to i32::MAX.
220 //
221 // https://github.com/lattera/glibc/blob/895ef79e04a953cac1493863bcae29ad85657ee1/nscd/aicache.c#L119
222 //
223 // And nss-mdns just ignores it.
224 //
225 // https://github.com/avahi/nss-mdns/blob/3292b172ce0100a1aed8b67c381760bc3fb87f2e/src/nss.c#L164
226 ttlp: *mut libc::c_int,
227) -> libc::c_int {
228 if name.is_null() || pat.is_null() || buffer.is_null() || errnop.is_null() || h_errnop.is_null()
229 // Allow null ttlp
230 {
231 return NssStatus::Unavailable as i32;
232 }
233
234 let (hostname, pat, errnop, h_errnop) = unsafe {
235 (
236 CStr::from_ptr(name),
237 &mut *pat,
238 &mut *errnop,
239 &mut *h_errnop,
240 )
241 };
242
243 let maybe_buf = unsafe { Gaih4Buf::try_new(hostname, pat, buffer, buflen) };
244 let mut buffer = match maybe_buf {
245 Ok(b) => b,
246 Err(e) => return e.bail(errnop, h_errnop),
247 };
248
249 let Ok(hostname) = hostname.to_str() else {
250 // Require a UTF-8 hostname.
251 return NssErr::INVALID_INPUT.bail(errnop, h_errnop);
252 };
253
254 let addrs = match R::resolve_host(hostname) {
255 Ok(res) => res,
256 Err(e) => return e.bail(errnop, h_errnop),
257 };
258
259 let mut found = false;
260 for addr in addrs {
261 if !buffer.push(addr) {
262 return NssErr::BUF_TOO_SMALL.bail(errnop, h_errnop);
263 }
264 found = true;
265 }
266
267 if !found {
268 return NssErr::NO_RESULT.bail(errnop, h_errnop);
269 }
270
271 if !ttlp.is_null()
272 && let Some(user_ttlp) = R::set_ttlp(hostname)
273 {
274 unsafe {
275 *ttlp = user_ttlp;
276 }
277 }
278
279 NssErr::SUCCESS.bail(errnop, h_errnop)
280}
281
282/// Recursive host object returned from `gethostbyname4`.
283///
284/// Defined in `nss.h`.
285///
286/// <https://github.com/lattera/glibc/blob/895ef79e04a953cac1493863bcae29ad85657ee1/nss/nss.h#L42-L49>
287#[repr(C)]
288#[derive(Debug)]
289#[doc(hidden)]
290pub struct GaihAddrTuple {
291 next: *mut GaihAddrTuple,
292 name: *const libc::c_char,
293 family: libc::c_int,
294
295 /// Stored big endian.
296 ///
297 /// <https://www.man7.org/linux/man-pages/man3/gethostbyname.3.html#:~:text=address%20in%20bytes.-,h_addr_list,-An%20array%20of>
298 addr: [libc::c_uint; 4],
299
300 /// Stored native endian.
301 ///
302 /// <https://sourceware.org/glibc/manual/2.41/html_node/Internet-Address-Formats.html#:~:text=The%20scope%20ID%20is%20stored%20in%20host%20byte%20order>
303 scope_id: libc::c_uint,
304}
305
306impl GaihAddrTuple {
307 fn new(hostname: *const libc::c_char) -> Self {
308 Self {
309 next: core::ptr::null_mut(),
310 name: hostname,
311 family: libc::AF_UNSPEC,
312 addr: [0u32; 4],
313 scope_id: 0,
314 }
315 }
316
317 /// Constructs a new node for the given address.
318 fn new_addr(hostname: *const libc::c_char, addr: Addr) -> Self {
319 let mut pat = match addr.ip {
320 IpAddr::V4(v4) => Self::new_v4(hostname, v4),
321 IpAddr::V6(v6) => Self::new_v6(hostname, v6),
322 };
323 pat.scope_id = addr.scope_id;
324 pat
325 }
326
327 /// Constructs a new IPv4 address node.
328 fn new_v4(hostname: *const libc::c_char, ipv4: Ipv4Addr) -> Self {
329 // This and `new_v6` are informed by avahi's use of inet_pton.
330 // https://github.com/avahi/nss-mdns/blob/3292b172ce0100a1aed8b67c381760bc3fb87f2e/src/avahi.c#L108
331 let mut pat = Self::new(hostname);
332 pat.family = libc::AF_INET;
333 pat.addr[0] = u32::from_ne_bytes(ipv4.octets());
334 pat
335 }
336
337 /// Constructs a new IPv6 address node.
338 fn new_v6(hostname: *const libc::c_char, ipv6: Ipv6Addr) -> Self {
339 let mut pat = Self::new(hostname);
340 pat.family = libc::AF_INET6;
341
342 ipv6.octets()
343 .chunks_exact(4)
344 .map(|bits| <[_; 4]>::try_from(bits).expect("exact chunk size is four"))
345 .map(u32::from_ne_bytes)
346 .zip(&mut pat.addr)
347 .for_each(|(val, slot)| *slot = val);
348
349 pat
350 }
351}
352
353#[cfg(test)]
354mod conversion_tests {
355 use core::net::Ipv4Addr;
356 use core::net::Ipv6Addr;
357
358 use crate::GaihAddrTuple;
359
360 /// NSS expects `gaih_addrtuple.addr` to hold the address in
361 /// big endian order. This test verifies with a direct conversion.
362 #[test]
363 fn ipv4_addr_is_network_byte_order() {
364 let t = GaihAddrTuple::new_v4(core::ptr::null(), Ipv4Addr::LOCALHOST);
365 let bytes: [u8; 16] = unsafe { core::mem::transmute(t.addr) };
366 assert_eq!(bytes[..4], Ipv4Addr::LOCALHOST.octets());
367 }
368
369 // IPv6 equivalent of the test above
370 #[test]
371 fn ipv6_addr_is_network_byte_order() {
372 let t = GaihAddrTuple::new_v6(core::ptr::null(), Ipv6Addr::LOCALHOST);
373 let bytes: [u8; 16] = unsafe { core::mem::transmute(t.addr) };
374 assert_eq!(bytes, Ipv6Addr::LOCALHOST.octets());
375 }
376}