Branch data Line data Source code
1 : : /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
2 : : *
3 : : * Copyright 2025 GNOME Foundation, Inc.
4 : : *
5 : : * SPDX-License-Identifier: LGPL-2.1-or-later
6 : : *
7 : : * This library is free software; you can redistribute it and/or
8 : : * modify it under the terms of the GNU Lesser General Public
9 : : * License as published by the Free Software Foundation; either
10 : : * version 2.1 of the License, or (at your option) any later version.
11 : : *
12 : : * This library is distributed in the hope that it will be useful,
13 : : * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 : : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 : : * Lesser General Public License for more details.
16 : : *
17 : : * You should have received a copy of the GNU Lesser General Public
18 : : * License along with this library; if not, write to the Free Software
19 : : * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
20 : : *
21 : : * Authors:
22 : : * - Philip Withnall <pwithnall@gnome.org>
23 : : */
24 : :
25 : : #include "config.h"
26 : :
27 : : #include <arpa/inet.h>
28 : : #include <cdb.h>
29 : : #include <dlfcn.h>
30 : : #include <fcntl.h>
31 : : #include <glib.h>
32 : : #include <glib/gstdio.h>
33 : : #include <gio/gio.h>
34 : : #include <locale.h>
35 : : #include <netdb.h>
36 : : #include <nss.h>
37 : : #include <pwd.h>
38 : : #include <stdio.h>
39 : :
40 : : static int (*real_open) (const char *, int, ...);
41 : : static char *mock_filter_list_path = NULL;
42 : : static enum
43 : : {
44 : : GETPWUID_VALID_USER,
45 : : GETPWUID_EMPTY_USERNAME,
46 : : GETPWUID_USER_NOT_FOUND,
47 : : GETPWUID_ERROR_EIO,
48 : : } mock_getpwuid_behaviour = GETPWUID_VALID_USER;
49 : : static enum
50 : : {
51 : : OPEN_PASSTHROUGH,
52 : : OPEN_ERROR_ENOENT,
53 : : OPEN_ERROR_EACCES,
54 : : } mock_open_behaviour = OPEN_PASSTHROUGH;
55 : :
56 : : int
57 : : open (const char *pathname,
58 : : int flags,
59 : : ...)
60 : : {
61 : : va_list va;
62 : 55 : mode_t mode = 0;
63 : :
64 [ + + ]: 55 : if (real_open == NULL)
65 : 1 : real_open = dlsym (RTLD_NEXT, "open");
66 : :
67 : 55 : va_start (va, flags);
68 [ + + ]: 55 : if (flags & (O_CREAT | O_TMPFILE))
69 : 9 : mode = va_arg (va, mode_t);
70 : 55 : va_end (va);
71 : :
72 : : /* Override the path for the compiled filter list file */
73 [ + + ]: 55 : if (mock_filter_list_path != NULL &&
74 [ + + ]: 28 : strcmp (pathname, "/var/lib/malcontent-nss/filter-lists/test-user") == 0)
75 : 25 : pathname = mock_filter_list_path;
76 : :
77 : : /* Allow overriding the behaviour. */
78 [ + + + - ]: 55 : switch (mock_open_behaviour)
79 : : {
80 : 51 : case OPEN_PASSTHROUGH:
81 : 51 : return real_open (pathname, flags, mode);
82 : 2 : case OPEN_ERROR_ENOENT:
83 : 2 : errno = ENOENT;
84 : 2 : return -1;
85 : 2 : case OPEN_ERROR_EACCES:
86 : 2 : errno = EACCES;
87 : 2 : return -1;
88 : 0 : default:
89 : : g_assert_not_reached ();
90 : : }
91 : : }
92 : :
93 : : int
94 : : getpwuid_r (uid_t uid,
95 : : struct passwd *restrict pwd,
96 : : char buf[],
97 : : size_t buflen,
98 : : struct passwd **restrict result)
99 : : {
100 : 49 : const char *test_user_username = "test-user";
101 : :
102 : : /* We expect the current user */
103 : 49 : g_assert (uid == getuid ());
104 : :
105 : : /* Override the username lookup for the compiled filter list file */
106 [ - + ]: 49 : if (buflen <= strlen (test_user_username))
107 : : {
108 : 0 : *result = NULL;
109 : 0 : return ERANGE;
110 : : }
111 : :
112 [ + + + + : 49 : switch (mock_getpwuid_behaviour)
- ]
113 : : {
114 : 43 : case GETPWUID_VALID_USER:
115 : 43 : strncpy (buf, test_user_username, buflen);
116 : 43 : pwd->pw_name = buf;
117 : 43 : pwd->pw_uid = uid;
118 : : /* don’t bother with the other fields for now */
119 : 43 : *result = pwd;
120 : :
121 : 43 : return 0;
122 : 2 : case GETPWUID_EMPTY_USERNAME:
123 : 2 : strncpy (buf, "", buflen);
124 : 2 : pwd->pw_name = buf;
125 : 2 : pwd->pw_uid = uid;
126 : : /* don’t bother with the other fields for now */
127 : 2 : *result = pwd;
128 : :
129 : 2 : return 0;
130 : 2 : case GETPWUID_USER_NOT_FOUND:
131 : 2 : *result = NULL;
132 : 2 : return 0;
133 : 2 : case GETPWUID_ERROR_EIO:
134 : 2 : *result = NULL;
135 : 2 : return EIO;
136 : 0 : default:
137 : : g_assert_not_reached ();
138 : : }
139 : : }
140 : :
141 : : static void
142 : 4 : assert_lookup_is_sinkhole (const char *hostname)
143 : : {
144 : 4 : const struct in_addr sinkhole_addr = { .s_addr = 0 };
145 : 4 : const struct in6_addr sinkhole_addr6 = { .s6_addr = { 0, } };
146 : 4 : struct addrinfo *res = NULL;
147 : : struct addrinfo hints;
148 : :
149 : 4 : memset (&hints, 0, sizeof (hints));
150 : 4 : hints.ai_family = AF_UNSPEC; /* IPv4 or IPv6 */
151 : :
152 : 4 : g_assert_cmpint (getaddrinfo (hostname, NULL, &hints, &res), ==, 0);
153 : :
154 [ + + ]: 28 : for (struct addrinfo *i = res; i != NULL; i = i->ai_next)
155 : : {
156 [ + + ]: 24 : if (i->ai_addr->sa_family == AF_INET)
157 : : {
158 : 12 : const struct sockaddr_in *p = (const struct sockaddr_in *) i->ai_addr;
159 : 12 : g_assert_cmpmem (&(p->sin_addr), sizeof (p->sin_addr), &sinkhole_addr, sizeof (sinkhole_addr));
160 : : }
161 [ + - ]: 12 : else if (i->ai_addr->sa_family == AF_INET6)
162 : : {
163 : 12 : const struct sockaddr_in6 *p = (const struct sockaddr_in6 *) i->ai_addr;
164 : 12 : g_assert_cmpmem (&(p->sin6_addr), sizeof (p->sin6_addr), &sinkhole_addr6, sizeof (sinkhole_addr6));
165 : : }
166 : : }
167 : :
168 : 4 : freeaddrinfo (res);
169 : :
170 : : /* And query again with AI_CANONNAME set, to test that. */
171 : 4 : hints.ai_flags |= AI_CANONNAME;
172 : 4 : g_assert_cmpint (getaddrinfo (hostname, NULL, &hints, &res), ==, 0);
173 : 4 : g_assert_cmpstr (res->ai_canonname, ==, hostname);
174 : 4 : freeaddrinfo (res);
175 : 4 : }
176 : :
177 : : static void
178 : 3 : assert_lookup_is_not_sinkhole (const char *hostname)
179 : : {
180 : 3 : struct addrinfo *res = NULL;
181 : : struct addrinfo hints;
182 : :
183 : 3 : memset (&hints, 0, sizeof (hints));
184 : 3 : hints.ai_family = AF_UNSPEC; /* IPv4 or IPv6 */
185 : :
186 : : /* If it’s not blocked by nss-malcontent, we expect lookup to fail as ‘not
187 : : * found’, since no other modules are loaded for the `hosts` NSS database. */
188 : 3 : g_assert_cmpint (getaddrinfo (hostname, NULL, &hints, &res), ==, EAI_NODATA);
189 : 3 : g_assert_null (res);
190 : 3 : }
191 : :
192 : : static void
193 : 1 : assert_lookup_is_redirect (const char *hostname,
194 : : const char *expected_redirected_hostname)
195 : : {
196 : : /* These addresses are hardcoded in the nss-hardcoded module. */
197 : 1 : const char *redirected_addr_str = "1.2.3.4";
198 : 1 : const char *redirected_addr6_str = "2001:0DB8:AC10:FE01::";
199 : :
200 : : struct in_addr redirected_addr;
201 : : struct in6_addr redirected_addr6;
202 : 1 : struct addrinfo *res = NULL;
203 : : struct addrinfo hints;
204 : :
205 : 1 : memset (&hints, 0, sizeof (hints));
206 : 1 : hints.ai_family = AF_UNSPEC; /* IPv4 or IPv6 */
207 : 1 : hints.ai_flags = AI_CANONNAME;
208 : :
209 : 1 : g_assert_cmpint (getaddrinfo (hostname, NULL, &hints, &res), ==, 0);
210 : :
211 : 1 : g_assert_cmpint (inet_pton (AF_INET, redirected_addr_str, &redirected_addr), ==, 1);
212 : 1 : g_assert_cmpint (inet_pton (AF_INET6, redirected_addr6_str, &redirected_addr6), ==, 1);
213 : :
214 : 1 : g_assert_cmpstr (res->ai_canonname, ==, expected_redirected_hostname);
215 : :
216 [ + + ]: 19 : for (struct addrinfo *i = res; i != NULL; i = i->ai_next)
217 : : {
218 [ + + ]: 18 : if (i->ai_addr->sa_family == AF_INET)
219 : : {
220 : 9 : const struct sockaddr_in *p = (const struct sockaddr_in *) i->ai_addr;
221 : 9 : g_assert_cmpmem (&(p->sin_addr), sizeof (p->sin_addr), &redirected_addr, sizeof (redirected_addr));
222 : : }
223 [ + - ]: 9 : else if (i->ai_addr->sa_family == AF_INET6)
224 : : {
225 : 9 : const struct sockaddr_in6 *p = (const struct sockaddr_in6 *) i->ai_addr;
226 : 9 : g_assert_cmpmem (&(p->sin6_addr), sizeof (p->sin6_addr), &redirected_addr6, sizeof (redirected_addr6));
227 : : }
228 : : }
229 : :
230 : 1 : freeaddrinfo (res);
231 : 1 : }
232 : :
233 : : static void
234 : 9 : assert_load_nss_malcontent_module (void)
235 : : {
236 : : char path[PATH_MAX + 1];
237 : : struct addrinfo hints;
238 : 9 : void *handle = NULL;
239 : 9 : g_autofree char *expected_malcontent_module_dir = NULL;
240 : 9 : g_autofree char *canonical_expected_malcontent_module_dir = NULL;
241 : 9 : g_autofree char *expected_hardcoded_module_dir = NULL;
242 : 9 : g_autofree char *canonical_expected_hardcoded_module_dir = NULL;
243 : :
244 : : /* Force NSS to use the `malcontent` module for all `hosts` database requests
245 : : * (with the `hardcoded` mock test module as a fallback).
246 : : *
247 : : * This essentially overrides the `hosts` line of `/etc/nsswitch.conf` to be
248 : : * `hosts: malcontent hardcoded`.
249 : : *
250 : : * That’s good, because it’s impossible to override the `/etc/nsswitch.conf`
251 : : * file in-process otherwise. Because NSS is part of glibc, the dynamic linker
252 : : * is not involved in resolving the `fopen()` syscall, so we can’t hook that.
253 : : * The only way I know of to override `/etc/nsswitch.conf` is to run the test
254 : : * inside a bwrap wrapper which bind-mounts over the top of the system
255 : : * `/etc/nsswitch.conf` file. That can’t be done from within the test process
256 : : * without `CAP_SYS_ADMIN` privileges; it would rely on `bwrap` being setuid.
257 : : *
258 : : * Thanks to this article for the idea: https://ldpreload.com/blog/testing-glibc-nsswitch
259 : : *
260 : : * With the `libnss_malcontent.so.2` module loaded, we need to override
261 : : * `open()` and `getpwuid_r()` (above) to make it use a compiled web filter
262 : : * file of our choice.
263 : : *
264 : : * `libnss_hardcoded.so.2` needs no overrides.
265 : : */
266 : 9 : __nss_configure_lookup ("hosts", "malcontent hardcoded");
267 : :
268 : : /* Do a lookup to force the module to load. */
269 : 9 : memset (&hints, 0, sizeof (hints));
270 : 9 : hints.ai_family = AF_UNSPEC; /* IPv4 or IPv6 */
271 : 9 : getaddrinfo ("example.com", NULL, &hints, NULL);
272 : :
273 : : /* Check the right module was loaded. */
274 : 9 : expected_malcontent_module_dir = g_test_build_filename (G_TEST_BUILT, "..", NULL);
275 : 9 : canonical_expected_malcontent_module_dir = g_canonicalize_filename (expected_malcontent_module_dir, "/");
276 : :
277 : 9 : handle = dlopen ("libnss_malcontent.so.2", RTLD_LAZY | RTLD_NOLOAD);
278 : 9 : g_assert_nonnull (handle);
279 : 9 : g_assert_no_errno (dlinfo (handle, RTLD_DI_ORIGIN, &path));
280 : 9 : g_assert_cmpstr (path, ==, canonical_expected_malcontent_module_dir);
281 : 9 : dlclose (handle);
282 : :
283 : 9 : expected_hardcoded_module_dir = g_test_build_filename (G_TEST_BUILT, ".", NULL);
284 : 9 : canonical_expected_hardcoded_module_dir = g_canonicalize_filename (expected_hardcoded_module_dir, "/");
285 : :
286 : 9 : handle = dlopen ("libnss_hardcoded.so.2", RTLD_LAZY | RTLD_NOLOAD);
287 : 9 : g_assert_nonnull (handle);
288 : 9 : g_assert_no_errno (dlinfo (handle, RTLD_DI_ORIGIN, &path));
289 : 9 : g_assert_cmpstr (path, ==, canonical_expected_hardcoded_module_dir);
290 : 9 : dlclose (handle);
291 : 9 : }
292 : :
293 : : typedef GFile MockFilterListHandle;
294 : :
295 : : static void
296 : 9 : mock_filter_list_handle_destroy (MockFilterListHandle *handle)
297 : : {
298 : 9 : GFile *file = G_FILE (handle);
299 : :
300 : 9 : g_file_delete (file, NULL, NULL);
301 [ + - ]: 9 : g_clear_object (&file);
302 : :
303 [ + - ]: 9 : g_clear_pointer (&mock_filter_list_path, g_free);
304 : 9 : }
305 : :
306 : : typedef struct
307 : : {
308 : : const char *key; /* a hostname, or `*` or a hostname prefixed with `~` */
309 : : const char *value; /* (nullable); NULL to block, non-NULL to redirect */
310 : : } FilterListEntry;
311 : :
312 [ + - ]: 18 : G_DEFINE_AUTOPTR_CLEANUP_FUNC (MockFilterListHandle, mock_filter_list_handle_destroy)
313 : :
314 : : /* Build a test compiled filter file, and then set it to be used by the loaded
315 : : * NSS module by setting `mock_filter_list_path.
316 : : *
317 : : * @filter_list_entries is NULL-terminated */
318 : : static MockFilterListHandle *
319 : 9 : set_mock_filter_list (const FilterListEntry *filter_list_entries)
320 : : {
321 : : struct cdb_make cdbm;
322 : 9 : g_autofd int test_web_filter_fd = -1;
323 : 9 : g_autofree char *test_web_filter_path = NULL;
324 : 9 : g_autoptr(GError) local_error = NULL;
325 : :
326 : 9 : test_web_filter_fd = g_file_open_tmp ("test-web-filter-XXXXXX", &test_web_filter_path, &local_error);
327 : 9 : g_assert_no_error (local_error);
328 : 9 : g_assert_no_errno (cdb_make_start (&cdbm, test_web_filter_fd));
329 : :
330 [ + + ]: 17 : for (size_t i = 0; filter_list_entries[i].key != NULL; i++)
331 : : {
332 : 8 : g_assert_no_errno (cdb_make_put (&cdbm,
333 : : filter_list_entries[i].key,
334 : : strlen (filter_list_entries[i].key),
335 : : filter_list_entries[i].value,
336 : : (filter_list_entries[i].value != NULL) ? strlen (filter_list_entries[i].value) : 0,
337 : : CDB_PUT_REPLACE));
338 : : }
339 : :
340 : 9 : g_assert_no_errno (cdb_make_finish (&cdbm));
341 : 9 : g_assert_no_errno (fsync (test_web_filter_fd));
342 : :
343 [ - + ]: 9 : g_clear_pointer (&mock_filter_list_path, g_free);
344 : 9 : mock_filter_list_path = g_strdup (test_web_filter_path);
345 : :
346 : 9 : return (MockFilterListHandle *) g_file_new_for_path (test_web_filter_path);
347 : : }
348 : :
349 : : static void
350 : 1 : test_nss_malcontent_basic_blocklist (void)
351 : : {
352 : 1 : g_autoptr(MockFilterListHandle) handle = NULL;
353 : :
354 : 1 : g_test_summary ("Test a basic blocklist filter");
355 : :
356 : 1 : assert_load_nss_malcontent_module ();
357 : :
358 : 1 : handle = set_mock_filter_list ((const FilterListEntry[]) {
359 : : { "duckduckgo.com", NULL },
360 : : { NULL, NULL },
361 : : });
362 : :
363 : : /* Try a lookup */
364 : 1 : assert_lookup_is_sinkhole ("duckduckgo.com");
365 : 1 : assert_lookup_is_not_sinkhole ("not-blocked.com");
366 : 1 : }
367 : :
368 : : static void
369 : 1 : test_nss_malcontent_basic_allowlist (void)
370 : : {
371 : 1 : g_autoptr(MockFilterListHandle) handle = NULL;
372 : :
373 : 1 : g_test_summary ("Test a basic allowlist filter");
374 : :
375 : 1 : assert_load_nss_malcontent_module ();
376 : :
377 : 1 : handle = set_mock_filter_list ((const FilterListEntry[]) {
378 : : { "*", NULL },
379 : : { "~duckduckgo.com", NULL },
380 : : { NULL, NULL },
381 : : });
382 : :
383 : : /* Try a lookup */
384 : 1 : assert_lookup_is_sinkhole ("google.com");
385 : 1 : assert_lookup_is_not_sinkhole ("duckduckgo.com");
386 : 1 : }
387 : :
388 : : static void
389 : 1 : test_malcontent_use_application_dns (void)
390 : : {
391 : 1 : g_autoptr(MockFilterListHandle) handle = NULL;
392 : :
393 : 1 : g_test_summary ("Test that the DNS-over-HTTPS canary is disabled by the NSS module");
394 : :
395 : 1 : assert_load_nss_malcontent_module ();
396 : :
397 : : /* Build an empty filter list file. This should still force DNS-over-HTTPS to
398 : : * be disabled. */
399 : 1 : handle = set_mock_filter_list ((const FilterListEntry[]) {
400 : : { NULL, NULL },
401 : : });
402 : :
403 : : /* Try a lookup */
404 : 1 : assert_lookup_is_sinkhole ("use-application-dns.net");
405 : 1 : }
406 : :
407 : : static void
408 : 1 : test_nss_malcontent_redirect (void)
409 : : {
410 : 1 : g_autoptr(MockFilterListHandle) handle = NULL;
411 : :
412 : 1 : g_test_summary ("Test a basic redirect filter");
413 : :
414 : 1 : assert_load_nss_malcontent_module ();
415 : :
416 : 1 : handle = set_mock_filter_list ((const FilterListEntry[]) {
417 : : { "duckduckgo.com", "always-resolves.com" },
418 : : { "google.com", "never-resolves.com" },
419 : : { NULL, NULL },
420 : : });
421 : :
422 : : /* Try a lookup. A redirect pointing to a domain which can’t be resolved
423 : : * should be treated as blocked. */
424 : 1 : assert_lookup_is_redirect ("duckduckgo.com", "always-resolves.com");
425 : 1 : assert_lookup_is_sinkhole ("google.com");
426 : 1 : assert_lookup_is_not_sinkhole ("not-blocked.com");
427 : 1 : }
428 : :
429 : : static void
430 : 1 : test_nss_malcontent_unsupported_address_family (void)
431 : : {
432 : 1 : g_autoptr(MockFilterListHandle) handle = NULL;
433 : :
434 : 1 : g_test_summary ("Test that unsupported address families are ignored");
435 : :
436 : 1 : assert_load_nss_malcontent_module ();
437 : :
438 : 1 : handle = set_mock_filter_list ((const FilterListEntry[]) {
439 : : { NULL, NULL },
440 : : });
441 : :
442 : : /* Try a lookup with an unsupported address family. At the moment this won’t
443 : : * actually exercise the branch we want to test in `nss-malcontent.c` because
444 : : * glibc rejects all families which aren’t `AF_UNSPEC`, `AF_INET` or
445 : : * `AF_INET6` (see
446 : : * https://elixir.bootlin.com/glibc/glibc-2.42/source/nss/getaddrinfo.c#L2318).
447 : : * Since that behaviour could change in future, we keep the test here. */
448 : 1 : struct addrinfo *res = NULL;
449 : : struct addrinfo hints;
450 : :
451 : 1 : memset (&hints, 0, sizeof (hints));
452 : 1 : hints.ai_family = AF_UNIX;
453 : :
454 : 1 : g_assert_cmpint (getaddrinfo ("test.com", NULL, &hints, &res), ==, EAI_FAMILY);
455 : 1 : }
456 : :
457 : : static void
458 : 1 : test_nss_malcontent_too_long_hostname (void)
459 : : {
460 : 1 : g_autoptr(MockFilterListHandle) handle = NULL;
461 : : char very_long_hostname[256 + 1];
462 : :
463 : 1 : g_test_summary ("Test that too long hostnames are ignored");
464 : :
465 : 1 : assert_load_nss_malcontent_module ();
466 : :
467 : 1 : handle = set_mock_filter_list ((const FilterListEntry[]) {
468 : : { NULL, NULL },
469 : : });
470 : :
471 : : /* Try a lookup with a hostname longer than 255 bytes. */
472 : 1 : struct addrinfo *res = NULL;
473 : : struct addrinfo hints;
474 : :
475 : 1 : memset (&hints, 0, sizeof (hints));
476 : 1 : hints.ai_family = AF_UNSPEC; /* IPv4 or IPv6 */
477 : :
478 : 1 : memset (very_long_hostname, 'a', sizeof (very_long_hostname));
479 : 1 : very_long_hostname[sizeof (very_long_hostname) - 1] = '\0';
480 : :
481 : : /* We’d expect EAI_MEMORY here, but NSS seems to reserve that for its own
482 : : * internal memory allocation problems, and squares an ENOMEM returned by an
483 : : * NSS module to just be EAI_NODATA. */
484 : 1 : g_assert_cmpint (getaddrinfo (very_long_hostname, NULL, &hints, &res), ==, EAI_NODATA);
485 : 1 : }
486 : :
487 : : static void
488 : 1 : test_nss_malcontent_getpwuid_errors (void)
489 : : {
490 : 2 : g_autoptr(MockFilterListHandle) handle = NULL;
491 : :
492 : 1 : g_test_summary ("Test that errors returned by getpwuid_r() are handled gracefully");
493 : :
494 : 1 : assert_load_nss_malcontent_module ();
495 : :
496 : : /* Set a fairly standard mock filter list, with example.com blocked. */
497 : 1 : handle = set_mock_filter_list ((const FilterListEntry[]) {
498 : : { "example.com", NULL },
499 : : { NULL, NULL },
500 : : });
501 : :
502 : : /* Try a lookup where internally calling getpwuid_r() fails somehow. */
503 : : struct
504 : : {
505 : : int getpwuid_behaviour;
506 : : int expected_gai_errno;
507 : : }
508 : 1 : vectors[] =
509 : : {
510 : : { GETPWUID_EMPTY_USERNAME, EAI_NODATA },
511 : : { GETPWUID_USER_NOT_FOUND, EAI_NODATA },
512 : : { GETPWUID_ERROR_EIO, EAI_NODATA },
513 : : };
514 : :
515 [ + + ]: 4 : for (size_t i = 0; i < G_N_ELEMENTS (vectors); i++)
516 : : {
517 : 3 : struct addrinfo *res = NULL;
518 : : struct addrinfo hints;
519 : :
520 : 3 : memset (&hints, 0, sizeof (hints));
521 : 3 : hints.ai_family = AF_UNSPEC; /* IPv4 or IPv6 */
522 : :
523 : 3 : mock_getpwuid_behaviour = vectors[i].getpwuid_behaviour;
524 : :
525 : : /* Look up example.com. If the mock filter was successfully loaded, this
526 : : * should be blocked; but the filter should *not* be loaded because
527 : : * getpwuid_r() will fail. The code should handle this failure gracefully. */
528 : 3 : g_assert_cmpint (getaddrinfo ("example.com", NULL, &hints, &res), ==, vectors[i].expected_gai_errno);
529 : : }
530 : :
531 : 1 : mock_getpwuid_behaviour = GETPWUID_VALID_USER;
532 : 1 : }
533 : :
534 : : static void
535 : 1 : test_nss_malcontent_open_errors (void)
536 : : {
537 : 2 : g_autoptr(MockFilterListHandle) handle = NULL;
538 : :
539 : 1 : g_test_summary ("Test that errors returned by open() are handled gracefully");
540 : :
541 : 1 : assert_load_nss_malcontent_module ();
542 : :
543 : : /* Set a fairly standard mock filter list, with example.com blocked. */
544 : 1 : handle = set_mock_filter_list ((const FilterListEntry[]) {
545 : : { "example.com", NULL },
546 : : { NULL, NULL },
547 : : });
548 : :
549 : : /* Try a lookup where internally calling open() fails somehow. */
550 : : struct
551 : : {
552 : : int open_behaviour;
553 : : int expected_gai_errno;
554 : : }
555 : 1 : vectors[] =
556 : : {
557 : : { OPEN_ERROR_ENOENT, EAI_NODATA },
558 : : { OPEN_ERROR_EACCES, EAI_NODATA },
559 : : };
560 : :
561 [ + + ]: 3 : for (size_t i = 0; i < G_N_ELEMENTS (vectors); i++)
562 : : {
563 : 2 : struct addrinfo *res = NULL;
564 : : struct addrinfo hints;
565 : :
566 : 2 : memset (&hints, 0, sizeof (hints));
567 : 2 : hints.ai_family = AF_UNSPEC; /* IPv4 or IPv6 */
568 : :
569 : 2 : mock_open_behaviour = vectors[i].open_behaviour;
570 : :
571 : : /* Look up example.com. If the mock filter was successfully loaded, this
572 : : * should be blocked; but the filter should *not* be loaded because
573 : : * open() will fail. The code should handle this failure gracefully. */
574 : 2 : g_assert_cmpint (getaddrinfo ("example.com", NULL, &hints, &res), ==, vectors[i].expected_gai_errno);
575 : : }
576 : :
577 : 1 : mock_open_behaviour = OPEN_PASSTHROUGH;
578 : 1 : }
579 : :
580 : : static void
581 : 1 : test_nss_malcontent_aliases (void)
582 : : {
583 : 1 : g_autoptr(MockFilterListHandle) handle = NULL;
584 : : struct hostent *host;
585 : :
586 : 1 : g_test_summary ("Test that the returned struct hostent has a non-NULL h_aliases field");
587 : :
588 : 1 : assert_load_nss_malcontent_module ();
589 : :
590 : 1 : handle = set_mock_filter_list ((const FilterListEntry[]) {
591 : : { "duckduckgo.com", NULL },
592 : : { NULL, NULL },
593 : : });
594 : :
595 : : /* Try a lookup. Because this is the old gethostbyname2() function, it’s not
596 : : * thread-safe. */
597 : 1 : host = gethostbyname2 ("duckduckgo.com", AF_INET6);
598 : 1 : g_assert_nonnull (host);
599 : :
600 : 1 : g_assert_cmpstr (host->h_name, ==, "duckduckgo.com");
601 : 1 : g_assert_nonnull (host->h_aliases);
602 : 1 : g_assert_null (host->h_aliases[0]);
603 : 1 : g_assert_cmpint (host->h_addrtype, ==, AF_INET6);
604 : 1 : g_assert_cmpint (host->h_length, ==, sizeof (struct in6_addr));
605 : 1 : g_assert_nonnull (host->h_addr_list);
606 : 1 : g_assert_nonnull (host->h_addr_list[0]);
607 : 1 : }
608 : :
609 : : int
610 : 1 : main (int argc,
611 : : char **argv)
612 : : {
613 : : int retval;
614 : :
615 : 1 : setlocale (LC_ALL, "");
616 : 1 : g_test_init (&argc, &argv, G_TEST_OPTION_ISOLATE_DIRS, NULL);
617 : :
618 : 1 : g_test_add_func ("/nss-malcontent/basic-blocklist", test_nss_malcontent_basic_blocklist);
619 : 1 : g_test_add_func ("/nss-malcontent/basic-allowlist", test_nss_malcontent_basic_allowlist);
620 : 1 : g_test_add_func ("/nss-malcontent/use-application-dns", test_malcontent_use_application_dns);
621 : 1 : g_test_add_func ("/nss-malcontent/redirect", test_nss_malcontent_redirect);
622 : 1 : g_test_add_func ("/nss-malcontent/unsupported-address-family", test_nss_malcontent_unsupported_address_family);
623 : 1 : g_test_add_func ("/nss-malcontent/too-long-hostname", test_nss_malcontent_too_long_hostname);
624 : 1 : g_test_add_func ("/nss-malcontent/getpwuid-errors", test_nss_malcontent_getpwuid_errors);
625 : 1 : g_test_add_func ("/nss-malcontent/open-errors", test_nss_malcontent_open_errors);
626 : 1 : g_test_add_func ("/nss-malcontent/aliases", test_nss_malcontent_aliases);
627 : :
628 : 1 : retval = g_test_run ();
629 : :
630 : 1 : g_free (mock_filter_list_path);
631 : :
632 : 1 : return retval;
633 : : }
|