Skip to main content

libnss_host4/
lib.rs

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