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}