1 | /* Test parsing of /etc/resolv.conf. Generic version. |
2 | Copyright (C) 2017-2024 Free Software Foundation, Inc. |
3 | This file is part of the GNU C Library. |
4 | |
5 | The GNU C Library is free software; you can redistribute it and/or |
6 | modify it under the terms of the GNU Lesser General Public |
7 | License as published by the Free Software Foundation; either |
8 | version 2.1 of the License, or (at your option) any later version. |
9 | |
10 | The GNU C Library is distributed in the hope that it will be useful, |
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
13 | Lesser General Public License for more details. |
14 | |
15 | You should have received a copy of the GNU Lesser General Public |
16 | License along with the GNU C Library; if not, see |
17 | <https://www.gnu.org/licenses/>. */ |
18 | |
19 | /* Before including this file, TEST_THREAD has to be defined to 0 or |
20 | 1, depending on whether the threading tests should be compiled |
21 | in. */ |
22 | |
23 | #include <arpa/inet.h> |
24 | #include <errno.h> |
25 | #include <gnu/lib-names.h> |
26 | #include <netdb.h> |
27 | #include <resolv/resolv_context.h> |
28 | #include <stdio.h> |
29 | #include <stdlib.h> |
30 | #include <support/capture_subprocess.h> |
31 | #include <support/check.h> |
32 | #include <support/namespace.h> |
33 | #include <support/run_diff.h> |
34 | #include <support/support.h> |
35 | #include <support/temp_file.h> |
36 | #include <support/test-driver.h> |
37 | #include <support/xsocket.h> |
38 | #include <support/xstdio.h> |
39 | #include <support/xunistd.h> |
40 | |
41 | #if TEST_THREAD |
42 | # include <support/xthread.h> |
43 | #endif |
44 | |
45 | /* This is the host name used to ensure predictable behavior of |
46 | res_init. */ |
47 | static const char *const test_hostname = "www.example.com" ; |
48 | |
49 | struct support_chroot *chroot_env; |
50 | |
51 | static void |
52 | prepare (int argc, char **argv) |
53 | { |
54 | chroot_env = support_chroot_create |
55 | ((struct support_chroot_configuration) |
56 | { |
57 | .resolv_conf = "" , |
58 | }); |
59 | } |
60 | |
61 | /* Verify that the chroot environment has been set up. */ |
62 | static void |
63 | check_chroot_working (void *closure) |
64 | { |
65 | xchroot (path: chroot_env->path_chroot); |
66 | FILE *fp = xfopen (_PATH_RESCONF, mode: "r" ); |
67 | xfclose (fp); |
68 | |
69 | TEST_VERIFY_EXIT (res_init () == 0); |
70 | TEST_VERIFY (_res.options & RES_INIT); |
71 | |
72 | char buf[100]; |
73 | if (gethostname (name: buf, len: sizeof (buf)) < 0) |
74 | FAIL_EXIT1 ("gethostname: %m" ); |
75 | if (strcmp (s1: buf, s2: test_hostname) != 0) |
76 | FAIL_EXIT1 ("unexpected host name: %s" , buf); |
77 | } |
78 | |
79 | /* If FLAG is set in *OPTIONS, write NAME to FP, and clear it in |
80 | *OPTIONS. */ |
81 | static void |
82 | print_option_flag (FILE *fp, int *options, int flag, const char *name) |
83 | { |
84 | if (*options & flag) |
85 | { |
86 | fprintf (stream: fp, format: " %s" , name); |
87 | *options &= ~flag; |
88 | } |
89 | } |
90 | |
91 | /* Write a decoded version of the resolver configuration *RESP to the |
92 | stream FP. */ |
93 | static void |
94 | print_resp (FILE *fp, res_state resp) |
95 | { |
96 | struct resolv_context *ctx = __resolv_context_get_override (resp); |
97 | TEST_VERIFY_EXIT (ctx != NULL); |
98 | if (ctx->conf == NULL) |
99 | fprintf (stream: fp, format: "; extended resolver state missing\n" ); |
100 | |
101 | /* The options directive. */ |
102 | { |
103 | /* RES_INIT is used internally for tracking initialization. */ |
104 | TEST_VERIFY (resp->options & RES_INIT); |
105 | /* Also mask out other default flags which cannot be set through |
106 | the options directive. */ |
107 | int options |
108 | = resp->options & ~(RES_INIT | RES_RECURSE | RES_DEFNAMES | RES_DNSRCH); |
109 | if (options != 0 |
110 | || resp->ndots != 1 |
111 | || resp->retrans != RES_TIMEOUT |
112 | || resp->retry != RES_DFLRETRY) |
113 | { |
114 | fputs (s: "options" , stream: fp); |
115 | if (resp->ndots != 1) |
116 | fprintf (stream: fp, format: " ndots:%d" , resp->ndots); |
117 | if (resp->retrans != RES_TIMEOUT) |
118 | fprintf (stream: fp, format: " timeout:%d" , resp->retrans); |
119 | if (resp->retry != RES_DFLRETRY) |
120 | fprintf (stream: fp, format: " attempts:%d" , resp->retry); |
121 | print_option_flag (fp, options: &options, RES_USEVC, name: "use-vc" ); |
122 | print_option_flag (fp, options: &options, RES_ROTATE, name: "rotate" ); |
123 | print_option_flag (fp, options: &options, RES_USE_EDNS0, name: "edns0" ); |
124 | print_option_flag (fp, options: &options, RES_SNGLKUP, |
125 | name: "single-request" ); |
126 | print_option_flag (fp, options: &options, RES_SNGLKUPREOP, |
127 | name: "single-request-reopen" ); |
128 | print_option_flag (fp, options: &options, RES_NOTLDQUERY, name: "no-tld-query" ); |
129 | print_option_flag (fp, options: &options, RES_NORELOAD, name: "no-reload" ); |
130 | print_option_flag (fp, options: &options, RES_TRUSTAD, name: "trust-ad" ); |
131 | print_option_flag (fp, options: &options, RES_NOAAAA, name: "no-aaaa" ); |
132 | fputc (c: '\n', stream: fp); |
133 | if (options != 0) |
134 | fprintf (stream: fp, format: "; error: unresolved option bits: 0x%x\n" , options); |
135 | } |
136 | } |
137 | |
138 | /* The search and domain directives. */ |
139 | if (resp->dnsrch[0] != NULL) |
140 | { |
141 | fputs (s: "search" , stream: fp); |
142 | for (int i = 0; i < MAXDNSRCH && resp->dnsrch[i] != NULL; ++i) |
143 | { |
144 | fputc (c: ' ', stream: fp); |
145 | fputs (s: resp->dnsrch[i], stream: fp); |
146 | } |
147 | fputc (c: '\n', stream: fp); |
148 | } |
149 | else if (resp->defdname[0] != '\0') |
150 | fprintf (stream: fp, format: "domain %s\n" , resp->defdname); |
151 | |
152 | /* The extended search path. */ |
153 | { |
154 | size_t i = 0; |
155 | while (true) |
156 | { |
157 | const char *name = __resolv_context_search_list (ctx, index: i); |
158 | if (name == NULL) |
159 | break; |
160 | fprintf (stream: fp, format: "; search[%zu]: %s\n" , i, name); |
161 | ++i; |
162 | } |
163 | } |
164 | |
165 | /* The sortlist directive. */ |
166 | if (resp->nsort > 0) |
167 | { |
168 | fputs (s: "sortlist" , stream: fp); |
169 | for (int i = 0; i < resp->nsort && i < MAXRESOLVSORT; ++i) |
170 | { |
171 | char net[20]; |
172 | if (inet_ntop (AF_INET, cp: &resp->sort_list[i].addr, |
173 | buf: net, len: sizeof (net)) == NULL) |
174 | FAIL_EXIT1 ("inet_ntop: %m\n" ); |
175 | char mask[20]; |
176 | if (inet_ntop (AF_INET, cp: &resp->sort_list[i].mask, |
177 | buf: mask, len: sizeof (mask)) == NULL) |
178 | FAIL_EXIT1 ("inet_ntop: %m\n" ); |
179 | fprintf (stream: fp, format: " %s/%s" , net, mask); |
180 | } |
181 | fputc (c: '\n', stream: fp); |
182 | } |
183 | |
184 | /* The nameserver directives. */ |
185 | for (size_t i = 0; i < resp->nscount; ++i) |
186 | { |
187 | char host[NI_MAXHOST]; |
188 | char service[NI_MAXSERV]; |
189 | |
190 | /* See get_nsaddr in res_send.c. */ |
191 | void *addr; |
192 | size_t addrlen; |
193 | if (resp->nsaddr_list[i].sin_family == 0 |
194 | && resp->_u._ext.nsaddrs[i] != NULL) |
195 | { |
196 | addr = resp->_u._ext.nsaddrs[i]; |
197 | addrlen = sizeof (*resp->_u._ext.nsaddrs[i]); |
198 | } |
199 | else |
200 | { |
201 | addr = &resp->nsaddr_list[i]; |
202 | addrlen = sizeof (resp->nsaddr_list[i]); |
203 | } |
204 | |
205 | int ret = getnameinfo (sa: addr, salen: addrlen, |
206 | host: host, hostlen: sizeof (host), serv: service, servlen: sizeof (service), |
207 | NI_NUMERICHOST | NI_NUMERICSERV); |
208 | if (ret != 0) |
209 | { |
210 | if (ret == EAI_SYSTEM) |
211 | fprintf (stream: fp, format: "; error: getnameinfo: %m\n" ); |
212 | else |
213 | fprintf (stream: fp, format: "; error: getnameinfo: %s\n" , gai_strerror (ecode: ret)); |
214 | } |
215 | else |
216 | { |
217 | fprintf (stream: fp, format: "nameserver %s\n" , host); |
218 | if (strcmp (s1: service, s2: "53" ) != 0) |
219 | fprintf (stream: fp, format: "; unrepresentable port number %s\n\n" , service); |
220 | } |
221 | } |
222 | |
223 | /* The extended name server list. */ |
224 | { |
225 | size_t i = 0; |
226 | while (true) |
227 | { |
228 | const struct sockaddr *addr = __resolv_context_nameserver (ctx, index: i); |
229 | if (addr == NULL) |
230 | break; |
231 | size_t addrlen; |
232 | switch (addr->sa_family) |
233 | { |
234 | case AF_INET: |
235 | addrlen = sizeof (struct sockaddr_in); |
236 | break; |
237 | case AF_INET6: |
238 | addrlen = sizeof (struct sockaddr_in6); |
239 | break; |
240 | default: |
241 | FAIL_EXIT1 ("invalid address family %d" , addr->sa_family); |
242 | } |
243 | |
244 | char host[NI_MAXHOST]; |
245 | char service[NI_MAXSERV]; |
246 | int ret = getnameinfo (sa: addr, salen: addrlen, |
247 | host: host, hostlen: sizeof (host), serv: service, servlen: sizeof (service), |
248 | NI_NUMERICHOST | NI_NUMERICSERV); |
249 | |
250 | if (ret != 0) |
251 | { |
252 | if (ret == EAI_SYSTEM) |
253 | fprintf (stream: fp, format: "; error: getnameinfo: %m\n" ); |
254 | else |
255 | fprintf (stream: fp, format: "; error: getnameinfo: %s\n" , gai_strerror (ecode: ret)); |
256 | } |
257 | else |
258 | fprintf (stream: fp, format: "; nameserver[%zu]: [%s]:%s\n" , i, host, service); |
259 | ++i; |
260 | } |
261 | } |
262 | |
263 | TEST_VERIFY (!ferror (fp)); |
264 | |
265 | __resolv_context_put (ctx); |
266 | } |
267 | |
268 | /* Parameters of one test case. */ |
269 | struct test_case |
270 | { |
271 | /* A short, descriptive name of the test. */ |
272 | const char *name; |
273 | |
274 | /* The contents of the /etc/resolv.conf file. */ |
275 | const char *conf; |
276 | |
277 | /* The expected output from print_resp. */ |
278 | const char *expected; |
279 | |
280 | /* Setting for the LOCALDOMAIN environment variable. NULL if the |
281 | variable is not to be set. */ |
282 | const char *localdomain; |
283 | |
284 | /* Setting for the RES_OPTIONS environment variable. NULL if the |
285 | variable is not to be set. */ |
286 | const char *res_options; |
287 | |
288 | /* Override the system host name. NULL means that no change is made |
289 | and the default is used (test_hostname). */ |
290 | const char *hostname; |
291 | }; |
292 | |
293 | enum test_init |
294 | { |
295 | test_init, |
296 | test_ninit, |
297 | test_mkquery, |
298 | test_gethostbyname, |
299 | test_getaddrinfo, |
300 | test_init_method_last = test_getaddrinfo |
301 | }; |
302 | |
303 | static const char *const test_init_names[] = |
304 | { |
305 | [test_init] = "res_init" , |
306 | [test_ninit] = "res_ninit" , |
307 | [test_mkquery] = "res_mkquery" , |
308 | [test_gethostbyname] = "gethostbyname" , |
309 | [test_getaddrinfo] = "getaddrinfo" , |
310 | }; |
311 | |
312 | /* Closure argument for run_res_init. */ |
313 | struct test_context |
314 | { |
315 | enum test_init init; |
316 | const struct test_case *t; |
317 | }; |
318 | |
319 | static void |
320 | setup_nss_dns_and_chroot (void) |
321 | { |
322 | /* Load nss_dns outside of the chroot. */ |
323 | if (dlopen (LIBNSS_DNS_SO, RTLD_LAZY) == NULL) |
324 | FAIL_EXIT1 ("could not load " LIBNSS_DNS_SO ": %s" , dlerror ()); |
325 | xchroot (path: chroot_env->path_chroot); |
326 | /* Force the use of nss_dns. */ |
327 | __nss_configure_lookup (dbname: "hosts" , string: "dns" ); |
328 | } |
329 | |
330 | /* Run res_ninit or res_init in a subprocess and dump the parsed |
331 | resolver state to standard output. */ |
332 | static void |
333 | run_res_init (void *closure) |
334 | { |
335 | struct test_context *ctx = closure; |
336 | TEST_VERIFY (getenv ("LOCALDOMAIN" ) == NULL); |
337 | TEST_VERIFY (getenv ("RES_OPTIONS" ) == NULL); |
338 | if (ctx->t->localdomain != NULL) |
339 | setenv (name: "LOCALDOMAIN" , value: ctx->t->localdomain, replace: 1); |
340 | if (ctx->t->res_options != NULL) |
341 | setenv (name: "RES_OPTIONS" , value: ctx->t->res_options, replace: 1); |
342 | if (ctx->t->hostname != NULL) |
343 | { |
344 | #ifdef CLONE_NEWUTS |
345 | /* This test needs its own namespace, to avoid changing the host |
346 | name for the parent, too. */ |
347 | TEST_VERIFY_EXIT (unshare (CLONE_NEWUTS) == 0); |
348 | if (sethostname (name: ctx->t->hostname, len: strlen (s: ctx->t->hostname)) != 0) |
349 | FAIL_EXIT1 ("sethostname (\"%s\"): %m" , ctx->t->hostname); |
350 | #else |
351 | FAIL_UNSUPPORTED ("clone (CLONE_NEWUTS) not supported" ); |
352 | #endif |
353 | } |
354 | |
355 | switch (ctx->init) |
356 | { |
357 | case test_init: |
358 | xchroot (path: chroot_env->path_chroot); |
359 | TEST_VERIFY (res_init () == 0); |
360 | print_resp (stdout, resp: &_res); |
361 | return; |
362 | |
363 | case test_ninit: |
364 | xchroot (path: chroot_env->path_chroot); |
365 | res_state resp = xmalloc (n: sizeof (*resp)); |
366 | memset (s: resp, c: 0, n: sizeof (*resp)); |
367 | TEST_VERIFY (res_ninit (resp) == 0); |
368 | print_resp (stdout, resp); |
369 | res_nclose (resp); |
370 | free (ptr: resp); |
371 | return; |
372 | |
373 | case test_mkquery: |
374 | xchroot (path: chroot_env->path_chroot); |
375 | unsigned char buf[512]; |
376 | TEST_VERIFY (res_mkquery (QUERY, "www.example" , |
377 | C_IN, ns_t_a, NULL, 0, |
378 | NULL, buf, sizeof (buf)) > 0); |
379 | print_resp (stdout, resp: &_res); |
380 | return; |
381 | |
382 | case test_gethostbyname: |
383 | setup_nss_dns_and_chroot (); |
384 | /* Trigger implicit initialization of the _res structure. The |
385 | actual lookup result is immaterial. */ |
386 | (void )gethostbyname (name: "www.example" ); |
387 | print_resp (stdout, resp: &_res); |
388 | return; |
389 | |
390 | case test_getaddrinfo: |
391 | setup_nss_dns_and_chroot (); |
392 | /* Trigger implicit initialization of the _res structure. The |
393 | actual lookup result is immaterial. */ |
394 | struct addrinfo *ai; |
395 | (void) getaddrinfo (name: "www.example" , NULL, NULL, pai: &ai); |
396 | print_resp (stdout, resp: &_res); |
397 | return; |
398 | } |
399 | |
400 | FAIL_EXIT1 ("invalid init method %d" , ctx->init); |
401 | } |
402 | |
403 | #if TEST_THREAD |
404 | /* Helper function which calls run_res_init from a thread. */ |
405 | static void * |
406 | run_res_init_thread_func (void *closure) |
407 | { |
408 | run_res_init (closure); |
409 | return NULL; |
410 | } |
411 | |
412 | /* Variant of res_run_init which runs the function on a non-main |
413 | thread. */ |
414 | static void |
415 | run_res_init_on_thread (void *closure) |
416 | { |
417 | xpthread_join (xpthread_create (NULL, run_res_init_thread_func, closure)); |
418 | } |
419 | #endif /* TEST_THREAD */ |
420 | |
421 | struct test_case test_cases[] = |
422 | { |
423 | {.name = "empty file" , |
424 | .conf = "" , |
425 | .expected = "search example.com\n" |
426 | "; search[0]: example.com\n" |
427 | "nameserver 127.0.0.1\n" |
428 | "; nameserver[0]: [127.0.0.1]:53\n" |
429 | }, |
430 | {.name = "empty file, no-dot hostname" , |
431 | .conf = "" , |
432 | .expected = "nameserver 127.0.0.1\n" |
433 | "; nameserver[0]: [127.0.0.1]:53\n" , |
434 | .hostname = "example" , |
435 | }, |
436 | {.name = "empty file with LOCALDOMAIN" , |
437 | .conf = "" , |
438 | .expected = "search example.net\n" |
439 | "; search[0]: example.net\n" |
440 | "nameserver 127.0.0.1\n" |
441 | "; nameserver[0]: [127.0.0.1]:53\n" , |
442 | .localdomain = "example.net" , |
443 | }, |
444 | {.name = "empty file with RES_OPTIONS" , |
445 | .conf = "" , |
446 | .expected = "options attempts:5 edns0\n" |
447 | "search example.com\n" |
448 | "; search[0]: example.com\n" |
449 | "nameserver 127.0.0.1\n" |
450 | "; nameserver[0]: [127.0.0.1]:53\n" , |
451 | .res_options = "edns0 attempts:5" , |
452 | }, |
453 | {.name = "empty file with RES_OPTIONS and LOCALDOMAIN" , |
454 | .conf = "" , |
455 | .expected = "options attempts:5 edns0\n" |
456 | "search example.org\n" |
457 | "; search[0]: example.org\n" |
458 | "nameserver 127.0.0.1\n" |
459 | "; nameserver[0]: [127.0.0.1]:53\n" , |
460 | .localdomain = "example.org" , |
461 | .res_options = "edns0 attempts:5" , |
462 | }, |
463 | {.name = "basic" , |
464 | .conf = "search corp.example.com example.com\n" |
465 | "nameserver 192.0.2.1\n" , |
466 | .expected = "search corp.example.com example.com\n" |
467 | "; search[0]: corp.example.com\n" |
468 | "; search[1]: example.com\n" |
469 | "nameserver 192.0.2.1\n" |
470 | "; nameserver[0]: [192.0.2.1]:53\n" |
471 | }, |
472 | {.name = "basic with no-dot hostname" , |
473 | .conf = "search corp.example.com example.com\n" |
474 | "nameserver 192.0.2.1\n" , |
475 | .expected = "search corp.example.com example.com\n" |
476 | "; search[0]: corp.example.com\n" |
477 | "; search[1]: example.com\n" |
478 | "nameserver 192.0.2.1\n" |
479 | "; nameserver[0]: [192.0.2.1]:53\n" , |
480 | .hostname = "example" , |
481 | }, |
482 | {.name = "basic no-reload" , |
483 | .conf = "options no-reload\n" |
484 | "search corp.example.com example.com\n" |
485 | "nameserver 192.0.2.1\n" , |
486 | .expected = "options no-reload\n" |
487 | "search corp.example.com example.com\n" |
488 | "; search[0]: corp.example.com\n" |
489 | "; search[1]: example.com\n" |
490 | "nameserver 192.0.2.1\n" |
491 | "; nameserver[0]: [192.0.2.1]:53\n" |
492 | }, |
493 | {.name = "basic no-reload via RES_OPTIONS" , |
494 | .conf = "search corp.example.com example.com\n" |
495 | "nameserver 192.0.2.1\n" , |
496 | .expected = "options no-reload\n" |
497 | "search corp.example.com example.com\n" |
498 | "; search[0]: corp.example.com\n" |
499 | "; search[1]: example.com\n" |
500 | "nameserver 192.0.2.1\n" |
501 | "; nameserver[0]: [192.0.2.1]:53\n" , |
502 | .res_options = "no-reload" |
503 | }, |
504 | {.name = "whitespace" , |
505 | .conf = "# This test covers comment and whitespace processing " |
506 | " (trailing whitespace,\n" |
507 | "# missing newline at end of file).\n" |
508 | "\n" |
509 | ";search commented out\n" |
510 | "search corp.example.com\texample.com \n" |
511 | "#nameserver 192.0.2.3\n" |
512 | "nameserver 192.0.2.1 \n" |
513 | "nameserver 192.0.2.2" , /* No \n at end of file. */ |
514 | .expected = "search corp.example.com example.com\n" |
515 | "; search[0]: corp.example.com\n" |
516 | "; search[1]: example.com\n" |
517 | "nameserver 192.0.2.1\n" |
518 | "nameserver 192.0.2.2\n" |
519 | "; nameserver[0]: [192.0.2.1]:53\n" |
520 | "; nameserver[1]: [192.0.2.2]:53\n" |
521 | }, |
522 | {.name = "domain" , |
523 | .conf = "domain example.net\n" |
524 | "nameserver 192.0.2.1\n" , |
525 | .expected = "search example.net\n" |
526 | "; search[0]: example.net\n" |
527 | "nameserver 192.0.2.1\n" |
528 | "; nameserver[0]: [192.0.2.1]:53\n" |
529 | }, |
530 | {.name = "domain space" , |
531 | .conf = "domain example.net \n" |
532 | "nameserver 192.0.2.1\n" , |
533 | .expected = "search example.net\n" |
534 | "; search[0]: example.net\n" |
535 | "nameserver 192.0.2.1\n" |
536 | "; nameserver[0]: [192.0.2.1]:53\n" |
537 | }, |
538 | {.name = "domain tab" , |
539 | .conf = "domain example.net\t\n" |
540 | "nameserver 192.0.2.1\n" , |
541 | .expected = "search example.net\n" |
542 | "; search[0]: example.net\n" |
543 | "nameserver 192.0.2.1\n" |
544 | "; nameserver[0]: [192.0.2.1]:53\n" |
545 | }, |
546 | {.name = "domain override" , |
547 | .conf = "search example.com example.org\n" |
548 | "nameserver 192.0.2.1\n" |
549 | "domain example.net" , /* No \n at end of file. */ |
550 | .expected = "search example.net\n" |
551 | "; search[0]: example.net\n" |
552 | "nameserver 192.0.2.1\n" |
553 | "; nameserver[0]: [192.0.2.1]:53\n" |
554 | }, |
555 | {.name = "option values, multiple servers" , |
556 | .conf = "options\tinet6\tndots:3 edns0\tattempts:5\ttimeout:19\n" |
557 | "domain example.net\n" |
558 | ";domain comment\n" |
559 | "search corp.example.com\texample.com\n" |
560 | "nameserver 192.0.2.1\n" |
561 | "nameserver ::1\n" |
562 | "nameserver 192.0.2.2\n" , |
563 | .expected = "options ndots:3 timeout:19 attempts:5 edns0\n" |
564 | "search corp.example.com example.com\n" |
565 | "; search[0]: corp.example.com\n" |
566 | "; search[1]: example.com\n" |
567 | "nameserver 192.0.2.1\n" |
568 | "nameserver ::1\n" |
569 | "nameserver 192.0.2.2\n" |
570 | "; nameserver[0]: [192.0.2.1]:53\n" |
571 | "; nameserver[1]: [::1]:53\n" |
572 | "; nameserver[2]: [192.0.2.2]:53\n" |
573 | }, |
574 | {.name = "out-of-range option vales" , |
575 | .conf = "options use-vc timeout:999 attempts:999 ndots:99\n" |
576 | "search example.com\n" , |
577 | .expected = "options ndots:15 timeout:30 attempts:5 use-vc\n" |
578 | "search example.com\n" |
579 | "; search[0]: example.com\n" |
580 | "nameserver 127.0.0.1\n" |
581 | "; nameserver[0]: [127.0.0.1]:53\n" |
582 | }, |
583 | {.name = "repeated directives" , |
584 | .conf = "options ndots:3 use-vc\n" |
585 | "options edns0 ndots:2\n" |
586 | "domain corp.example\n" |
587 | "search example.net corp.example.com example.com\n" |
588 | "search example.org\n" |
589 | "search\n" , |
590 | .expected = "options ndots:2 use-vc edns0\n" |
591 | "search example.org\n" |
592 | "; search[0]: example.org\n" |
593 | "nameserver 127.0.0.1\n" |
594 | "; nameserver[0]: [127.0.0.1]:53\n" |
595 | }, |
596 | {.name = "many name servers, sortlist" , |
597 | .conf = "options single-request\n" |
598 | "search example.org example.com example.net corp.example.com\n" |
599 | "sortlist 192.0.2.0/255.255.255.0\n" |
600 | "nameserver 192.0.2.1\n" |
601 | "nameserver 192.0.2.2\n" |
602 | "nameserver 192.0.2.3\n" |
603 | "nameserver 192.0.2.4\n" |
604 | "nameserver 192.0.2.5\n" |
605 | "nameserver 192.0.2.6\n" |
606 | "nameserver 192.0.2.7\n" |
607 | "nameserver 192.0.2.8\n" , |
608 | .expected = "options single-request\n" |
609 | "search example.org example.com example.net corp.example.com\n" |
610 | "; search[0]: example.org\n" |
611 | "; search[1]: example.com\n" |
612 | "; search[2]: example.net\n" |
613 | "; search[3]: corp.example.com\n" |
614 | "sortlist 192.0.2.0/255.255.255.0\n" |
615 | "nameserver 192.0.2.1\n" |
616 | "nameserver 192.0.2.2\n" |
617 | "nameserver 192.0.2.3\n" |
618 | "; nameserver[0]: [192.0.2.1]:53\n" |
619 | "; nameserver[1]: [192.0.2.2]:53\n" |
620 | "; nameserver[2]: [192.0.2.3]:53\n" |
621 | "; nameserver[3]: [192.0.2.4]:53\n" |
622 | "; nameserver[4]: [192.0.2.5]:53\n" |
623 | "; nameserver[5]: [192.0.2.6]:53\n" |
624 | "; nameserver[6]: [192.0.2.7]:53\n" |
625 | "; nameserver[7]: [192.0.2.8]:53\n" |
626 | }, |
627 | {.name = "IPv4 and IPv6 nameservers" , |
628 | .conf = "options single-request\n" |
629 | "search example.org example.com example.net corp.example.com" |
630 | " legacy.example.com\n" |
631 | "sortlist 192.0.2.0\n" |
632 | "nameserver 192.0.2.1\n" |
633 | "nameserver 2001:db8::2\n" |
634 | "nameserver 192.0.2.3\n" |
635 | "nameserver 2001:db8::4\n" |
636 | "nameserver 192.0.2.5\n" |
637 | "nameserver 2001:db8::6\n" |
638 | "nameserver 192.0.2.7\n" |
639 | "nameserver 2001:db8::8\n" , |
640 | .expected = "options single-request\n" |
641 | "search example.org example.com example.net corp.example.com" |
642 | " legacy.example.com\n" |
643 | "; search[0]: example.org\n" |
644 | "; search[1]: example.com\n" |
645 | "; search[2]: example.net\n" |
646 | "; search[3]: corp.example.com\n" |
647 | "; search[4]: legacy.example.com\n" |
648 | "sortlist 192.0.2.0/255.255.255.0\n" |
649 | "nameserver 192.0.2.1\n" |
650 | "nameserver 2001:db8::2\n" |
651 | "nameserver 192.0.2.3\n" |
652 | "; nameserver[0]: [192.0.2.1]:53\n" |
653 | "; nameserver[1]: [2001:db8::2]:53\n" |
654 | "; nameserver[2]: [192.0.2.3]:53\n" |
655 | "; nameserver[3]: [2001:db8::4]:53\n" |
656 | "; nameserver[4]: [192.0.2.5]:53\n" |
657 | "; nameserver[5]: [2001:db8::6]:53\n" |
658 | "; nameserver[6]: [192.0.2.7]:53\n" |
659 | "; nameserver[7]: [2001:db8::8]:53\n" , |
660 | }, |
661 | {.name = "garbage after nameserver" , |
662 | .conf = "nameserver 192.0.2.1 garbage\n" |
663 | "nameserver 192.0.2.2:5353\n" |
664 | "nameserver 192.0.2.3 5353\n" , |
665 | .expected = "search example.com\n" |
666 | "; search[0]: example.com\n" |
667 | "nameserver 192.0.2.1\n" |
668 | "nameserver 192.0.2.3\n" |
669 | "; nameserver[0]: [192.0.2.1]:53\n" |
670 | "; nameserver[1]: [192.0.2.3]:53\n" |
671 | }, |
672 | {.name = "RES_OPTIONS is cumulative" , |
673 | .conf = "options timeout:7 ndots:2 use-vc\n" |
674 | "nameserver 192.0.2.1\n" , |
675 | .expected = "options ndots:3 timeout:7 attempts:5 use-vc edns0\n" |
676 | "search example.com\n" |
677 | "; search[0]: example.com\n" |
678 | "nameserver 192.0.2.1\n" |
679 | "; nameserver[0]: [192.0.2.1]:53\n" , |
680 | .res_options = "attempts:5 ndots:3 edns0 " , |
681 | }, |
682 | {.name = "many search list entries (bug 19569)" , |
683 | .conf = "nameserver 192.0.2.1\n" |
684 | "search corp.example.com support.example.com" |
685 | " community.example.org wan.example.net vpn.example.net" |
686 | " example.com example.org example.net\n" , |
687 | .expected = "search corp.example.com support.example.com" |
688 | " community.example.org wan.example.net vpn.example.net example.com\n" |
689 | "; search[0]: corp.example.com\n" |
690 | "; search[1]: support.example.com\n" |
691 | "; search[2]: community.example.org\n" |
692 | "; search[3]: wan.example.net\n" |
693 | "; search[4]: vpn.example.net\n" |
694 | "; search[5]: example.com\n" |
695 | "; search[6]: example.org\n" |
696 | "; search[7]: example.net\n" |
697 | "nameserver 192.0.2.1\n" |
698 | "; nameserver[0]: [192.0.2.1]:53\n" |
699 | }, |
700 | {.name = "very long search list entries (bug 21475)" , |
701 | .conf = "nameserver 192.0.2.1\n" |
702 | "search example.com " |
703 | #define H63 "this-host-name-is-longer-than-yours-yes-I-really-really-mean-it" |
704 | #define D63 "this-domain-name-is-as-long-as-the-previous-name--63-characters" |
705 | " " H63 "." D63 ".example.org" |
706 | " " H63 "." D63 ".example.net\n" , |
707 | .expected = "search example.com " H63 "." D63 ".example.org\n" |
708 | "; search[0]: example.com\n" |
709 | "; search[1]: " H63 "." D63 ".example.org\n" |
710 | "; search[2]: " H63 "." D63 ".example.net\n" |
711 | #undef H63 |
712 | #undef D63 |
713 | "nameserver 192.0.2.1\n" |
714 | "; nameserver[0]: [192.0.2.1]:53\n" |
715 | }, |
716 | {.name = "trust-ad flag" , |
717 | .conf = "options trust-ad\n" |
718 | "nameserver 192.0.2.1\n" , |
719 | .expected = "options trust-ad\n" |
720 | "search example.com\n" |
721 | "; search[0]: example.com\n" |
722 | "nameserver 192.0.2.1\n" |
723 | "; nameserver[0]: [192.0.2.1]:53\n" |
724 | }, |
725 | {.name = "no-aaaa flag" , |
726 | .conf = "options no-aaaa\n" |
727 | "nameserver 192.0.2.1\n" , |
728 | .expected = "options no-aaaa\n" |
729 | "search example.com\n" |
730 | "; search[0]: example.com\n" |
731 | "nameserver 192.0.2.1\n" |
732 | "; nameserver[0]: [192.0.2.1]:53\n" |
733 | }, |
734 | { NULL } |
735 | }; |
736 | |
737 | /* Run the indicated test case. This function assumes that the chroot |
738 | contents has already been set up. */ |
739 | static void |
740 | test_file_contents (const struct test_case *t) |
741 | { |
742 | #if TEST_THREAD |
743 | for (int do_thread = 0; do_thread < 2; ++do_thread) |
744 | #endif |
745 | for (int init_method = 0; init_method <= test_init_method_last; |
746 | ++init_method) |
747 | { |
748 | if (test_verbose > 0) |
749 | printf (format: "info: testing init method %s\n" , |
750 | test_init_names[init_method]); |
751 | struct test_context ctx = { .init = init_method, .t = t }; |
752 | void (*func) (void *) = run_res_init; |
753 | #if TEST_THREAD |
754 | if (do_thread) |
755 | func = run_res_init_on_thread; |
756 | #endif |
757 | struct support_capture_subprocess proc |
758 | = support_capture_subprocess (callback: func, closure: &ctx); |
759 | if (strcmp (s1: proc.out.buffer, s2: t->expected) != 0) |
760 | { |
761 | support_record_failure (); |
762 | printf (format: "error: output mismatch for %s (init method %s)\n" , |
763 | t->name, test_init_names[init_method]); |
764 | support_run_diff (left_label: "expected" , left: t->expected, |
765 | right_label: "actual" , right: proc.out.buffer); |
766 | } |
767 | support_capture_subprocess_check (&proc, context: t->name, status_or_signal: 0, |
768 | allowed: sc_allow_stdout); |
769 | support_capture_subprocess_free (&proc); |
770 | } |
771 | } |
772 | |
773 | /* Special tests which do not follow the general pattern. */ |
774 | enum { special_tests_count = 11 }; |
775 | |
776 | /* Implementation of special tests. */ |
777 | static void |
778 | special_test_callback (void *closure) |
779 | { |
780 | unsigned int *test_indexp = closure; |
781 | unsigned test_index = *test_indexp; |
782 | TEST_VERIFY (test_index < special_tests_count); |
783 | if (test_verbose > 0) |
784 | printf (format: "info: special test %u\n" , test_index); |
785 | xchroot (path: chroot_env->path_chroot); |
786 | |
787 | switch (test_index) |
788 | { |
789 | case 0: |
790 | case 1: |
791 | /* Second res_init with missing or empty file preserves |
792 | flags. */ |
793 | if (test_index == 1) |
794 | TEST_VERIFY (unlink (_PATH_RESCONF) == 0); |
795 | _res.options = RES_USE_EDNS0; |
796 | TEST_VERIFY (res_init () == 0); |
797 | /* First res_init clears flag. */ |
798 | TEST_VERIFY (!(_res.options & RES_USE_EDNS0)); |
799 | _res.options |= RES_USE_EDNS0; |
800 | TEST_VERIFY (res_init () == 0); |
801 | /* Second res_init preserves flag. */ |
802 | TEST_VERIFY (_res.options & RES_USE_EDNS0); |
803 | if (test_index == 1) |
804 | /* Restore empty file. */ |
805 | support_write_file_string (_PATH_RESCONF, contents: "" ); |
806 | break; |
807 | |
808 | case 2: |
809 | /* Second res_init is cumulative. */ |
810 | support_write_file_string (_PATH_RESCONF, |
811 | contents: "options rotate\n" |
812 | "nameserver 192.0.2.1\n" ); |
813 | _res.options = RES_USE_EDNS0; |
814 | TEST_VERIFY (res_init () == 0); |
815 | /* First res_init clears flag. */ |
816 | TEST_VERIFY (!(_res.options & RES_USE_EDNS0)); |
817 | /* And sets RES_ROTATE. */ |
818 | TEST_VERIFY (_res.options & RES_ROTATE); |
819 | _res.options |= RES_USE_EDNS0; |
820 | TEST_VERIFY (res_init () == 0); |
821 | /* Second res_init preserves flag. */ |
822 | TEST_VERIFY (_res.options & RES_USE_EDNS0); |
823 | TEST_VERIFY (_res.options & RES_ROTATE); |
824 | /* Reloading the configuration does not clear the explicitly set |
825 | flag. */ |
826 | support_write_file_string (_PATH_RESCONF, |
827 | contents: "nameserver 192.0.2.1\n" |
828 | "nameserver 192.0.2.2\n" ); |
829 | TEST_VERIFY (res_init () == 0); |
830 | TEST_VERIFY (_res.nscount == 2); |
831 | TEST_VERIFY (_res.options & RES_USE_EDNS0); |
832 | /* Whether RES_ROTATE (originally in resolv.conf, now removed) |
833 | should be preserved is subject to debate. See bug 21701. */ |
834 | /* TEST_VERIFY (!(_res.options & RES_ROTATE)); */ |
835 | break; |
836 | |
837 | case 3: |
838 | case 4: |
839 | case 5: |
840 | case 6: |
841 | support_write_file_string (_PATH_RESCONF, |
842 | contents: "options edns0\n" |
843 | "nameserver 192.0.2.1\n" ); |
844 | goto reload_tests; |
845 | case 7: /* 7 and the following tests are with no-reload. */ |
846 | case 8: |
847 | case 9: |
848 | case 10: |
849 | support_write_file_string (_PATH_RESCONF, |
850 | contents: "options edns0 no-reload\n" |
851 | "nameserver 192.0.2.1\n" ); |
852 | /* Fall through. */ |
853 | reload_tests: |
854 | for (int iteration = 0; iteration < 2; ++iteration) |
855 | { |
856 | switch (test_index) |
857 | { |
858 | case 3: |
859 | case 7: |
860 | TEST_VERIFY (res_init () == 0); |
861 | break; |
862 | case 4: |
863 | case 8: |
864 | { |
865 | unsigned char buf[512]; |
866 | TEST_VERIFY |
867 | (res_mkquery (QUERY, test_hostname, C_IN, T_A, |
868 | NULL, 0, NULL, buf, sizeof (buf)) > 0); |
869 | } |
870 | break; |
871 | case 5: |
872 | case 9: |
873 | gethostbyname (name: test_hostname); |
874 | break; |
875 | case 6: |
876 | case 10: |
877 | { |
878 | struct addrinfo *ai; |
879 | (void) getaddrinfo (name: test_hostname, NULL, NULL, pai: &ai); |
880 | } |
881 | break; |
882 | } |
883 | /* test_index == 7 is res_init and performs a reload even |
884 | with no-reload. */ |
885 | if (iteration == 0 || test_index > 7) |
886 | { |
887 | TEST_VERIFY (_res.options & RES_USE_EDNS0); |
888 | TEST_VERIFY (!(_res.options & RES_ROTATE)); |
889 | if (test_index < 7) |
890 | TEST_VERIFY (!(_res.options & RES_NORELOAD)); |
891 | else |
892 | TEST_VERIFY (_res.options & RES_NORELOAD); |
893 | TEST_VERIFY (_res.nscount == 1); |
894 | /* File change triggers automatic reloading. */ |
895 | support_write_file_string (_PATH_RESCONF, |
896 | contents: "options rotate\n" |
897 | "nameserver 192.0.2.1\n" |
898 | "nameserver 192.0.2.2\n" ); |
899 | } |
900 | else |
901 | { |
902 | if (test_index != 3 && test_index != 7) |
903 | /* test_index 3, 7 are res_init; this function does |
904 | not reset flags. See bug 21701. */ |
905 | TEST_VERIFY (!(_res.options & RES_USE_EDNS0)); |
906 | TEST_VERIFY (_res.options & RES_ROTATE); |
907 | TEST_VERIFY (_res.nscount == 2); |
908 | } |
909 | } |
910 | break; |
911 | } |
912 | } |
913 | |
914 | #if TEST_THREAD |
915 | /* Helper function which calls special_test_callback from a |
916 | thread. */ |
917 | static void * |
918 | special_test_thread_func (void *closure) |
919 | { |
920 | special_test_callback (closure); |
921 | return NULL; |
922 | } |
923 | |
924 | /* Variant of special_test_callback which runs the function on a |
925 | non-main thread. */ |
926 | static void |
927 | run_special_test_on_thread (void *closure) |
928 | { |
929 | xpthread_join (xpthread_create (NULL, special_test_thread_func, closure)); |
930 | } |
931 | #endif /* TEST_THREAD */ |
932 | |
933 | /* Perform the requested special test in a subprocess using |
934 | special_test_callback. */ |
935 | static void |
936 | special_test (unsigned int test_index) |
937 | { |
938 | #if TEST_THREAD |
939 | for (int do_thread = 0; do_thread < 2; ++do_thread) |
940 | #endif |
941 | { |
942 | void (*func) (void *) = special_test_callback; |
943 | #if TEST_THREAD |
944 | if (do_thread) |
945 | func = run_special_test_on_thread; |
946 | #endif |
947 | struct support_capture_subprocess proc |
948 | = support_capture_subprocess (callback: func, closure: &test_index); |
949 | char *test_name = xasprintf (format: "special test %u" , test_index); |
950 | if (strcmp (s1: proc.out.buffer, s2: "" ) != 0) |
951 | { |
952 | support_record_failure (); |
953 | printf (format: "error: output mismatch for %s\n" , test_name); |
954 | support_run_diff (left_label: "expected" , left: "" , |
955 | right_label: "actual" , right: proc.out.buffer); |
956 | } |
957 | support_capture_subprocess_check (&proc, context: test_name, status_or_signal: 0, allowed: sc_allow_stdout); |
958 | free (ptr: test_name); |
959 | support_capture_subprocess_free (&proc); |
960 | } |
961 | } |
962 | |
963 | |
964 | /* Dummy DNS server. It ensures that the probe queries sent by |
965 | gethostbyname and getaddrinfo receive a reply even if the system |
966 | applies a very strict rate limit to localhost. */ |
967 | static pid_t |
968 | start_dummy_server (void) |
969 | { |
970 | int server_socket = xsocket (AF_INET, SOCK_DGRAM, 0); |
971 | { |
972 | struct sockaddr_in sin = |
973 | { |
974 | .sin_family = AF_INET, |
975 | .sin_addr = { .s_addr = htonl (INADDR_LOOPBACK) }, |
976 | .sin_port = htons (53), |
977 | }; |
978 | int ret = bind (fd: server_socket, addr: (struct sockaddr *) &sin, len: sizeof (sin)); |
979 | if (ret < 0) |
980 | { |
981 | if (errno == EACCES) |
982 | /* The port is reserved, which means we cannot start the |
983 | server. */ |
984 | return -1; |
985 | FAIL_EXIT1 ("cannot bind socket to port 53: %m" ); |
986 | } |
987 | } |
988 | |
989 | pid_t pid = xfork (); |
990 | if (pid == 0) |
991 | { |
992 | /* Child process. Echo back queries as SERVFAIL responses. */ |
993 | while (true) |
994 | { |
995 | union |
996 | { |
997 | HEADER ; |
998 | unsigned char bytes[512]; |
999 | } packet; |
1000 | struct sockaddr_in sin; |
1001 | socklen_t sinlen = sizeof (sin); |
1002 | |
1003 | ssize_t ret = recvfrom |
1004 | (fd: server_socket, buf: &packet, n: sizeof (packet), |
1005 | MSG_NOSIGNAL, addr: (struct sockaddr *) &sin, addr_len: &sinlen); |
1006 | if (ret < 0) |
1007 | FAIL_EXIT1 ("recvfrom on fake server socket: %m" ); |
1008 | if (ret > sizeof (HEADER)) |
1009 | { |
1010 | /* Turn the query into a SERVFAIL response. */ |
1011 | packet.header.qr = 1; |
1012 | packet.header.rcode = ns_r_servfail; |
1013 | |
1014 | /* Send the response. */ |
1015 | ret = sendto (fd: server_socket, buf: &packet, n: ret, |
1016 | MSG_NOSIGNAL, addr: (struct sockaddr *) &sin, addr_len: sinlen); |
1017 | if (ret < 0) |
1018 | /* The peer may have closed socket prematurely, so |
1019 | this is not an error. */ |
1020 | printf (format: "warning: sending DNS server reply: %m\n" ); |
1021 | } |
1022 | } |
1023 | } |
1024 | |
1025 | /* In the parent, close the socket. */ |
1026 | xclose (server_socket); |
1027 | |
1028 | return pid; |
1029 | } |
1030 | |
1031 | static int |
1032 | do_test (void) |
1033 | { |
1034 | support_become_root (); |
1035 | support_enter_network_namespace (); |
1036 | if (!support_in_uts_namespace () || !support_can_chroot ()) |
1037 | return EXIT_UNSUPPORTED; |
1038 | |
1039 | /* We are in an UTS namespace, so we can set the host name without |
1040 | altering the state of the entire system. */ |
1041 | if (sethostname (name: test_hostname, len: strlen (s: test_hostname)) != 0) |
1042 | FAIL_EXIT1 ("sethostname: %m" ); |
1043 | |
1044 | /* These environment variables affect resolv.conf parsing. */ |
1045 | unsetenv (name: "LOCALDOMAIN" ); |
1046 | unsetenv (name: "RES_OPTIONS" ); |
1047 | |
1048 | /* Ensure that the chroot setup worked. */ |
1049 | { |
1050 | struct support_capture_subprocess proc |
1051 | = support_capture_subprocess (callback: check_chroot_working, NULL); |
1052 | support_capture_subprocess_check (&proc, context: "chroot" , status_or_signal: 0, allowed: sc_allow_none); |
1053 | support_capture_subprocess_free (&proc); |
1054 | } |
1055 | |
1056 | pid_t server = start_dummy_server (); |
1057 | |
1058 | for (size_t i = 0; test_cases[i].name != NULL; ++i) |
1059 | { |
1060 | if (test_verbose > 0) |
1061 | printf (format: "info: running test: %s\n" , test_cases[i].name); |
1062 | TEST_VERIFY (test_cases[i].conf != NULL); |
1063 | TEST_VERIFY (test_cases[i].expected != NULL); |
1064 | |
1065 | support_write_file_string (path: chroot_env->path_resolv_conf, |
1066 | contents: test_cases[i].conf); |
1067 | |
1068 | test_file_contents (t: &test_cases[i]); |
1069 | |
1070 | /* The expected output from the empty file test is used for |
1071 | further tests. */ |
1072 | if (test_cases[i].conf[0] == '\0') |
1073 | { |
1074 | if (test_verbose > 0) |
1075 | printf (format: "info: special test: missing file\n" ); |
1076 | TEST_VERIFY (unlink (chroot_env->path_resolv_conf) == 0); |
1077 | test_file_contents (t: &test_cases[i]); |
1078 | |
1079 | if (test_verbose > 0) |
1080 | printf (format: "info: special test: dangling symbolic link\n" ); |
1081 | TEST_VERIFY (symlink ("does-not-exist" , chroot_env->path_resolv_conf) == 0); |
1082 | test_file_contents (t: &test_cases[i]); |
1083 | TEST_VERIFY (unlink (chroot_env->path_resolv_conf) == 0); |
1084 | |
1085 | if (test_verbose > 0) |
1086 | printf (format: "info: special test: unreadable file\n" ); |
1087 | support_write_file_string (path: chroot_env->path_resolv_conf, contents: "" ); |
1088 | TEST_VERIFY (chmod (chroot_env->path_resolv_conf, 0) == 0); |
1089 | test_file_contents (t: &test_cases[i]); |
1090 | |
1091 | /* Restore the empty file. */ |
1092 | TEST_VERIFY (unlink (chroot_env->path_resolv_conf) == 0); |
1093 | support_write_file_string (path: chroot_env->path_resolv_conf, contents: "" ); |
1094 | } |
1095 | } |
1096 | |
1097 | /* The tests which do not follow a regular pattern. */ |
1098 | for (unsigned int test_index = 0; |
1099 | test_index < special_tests_count; ++test_index) |
1100 | special_test (test_index); |
1101 | |
1102 | if (server > 0) |
1103 | { |
1104 | if (kill (pid: server, SIGTERM) < 0) |
1105 | FAIL_EXIT1 ("could not terminate server process: %m" ); |
1106 | xwaitpid (server, NULL, flags: 0); |
1107 | } |
1108 | |
1109 | support_chroot_free (chroot_env); |
1110 | return 0; |
1111 | } |
1112 | |
1113 | #define PREPARE prepare |
1114 | #include <support/test-driver.c> |
1115 | |