Log lengthy DNS lookups. Bug 514
[exim.git] / test / src / fakens.c
1 /*************************************************
2 * fakens - A Fake Nameserver Program *
3 *************************************************/
4
5 /* This program exists to support the testing of DNS handling code in Exim. It
6 avoids the need to install special zones in a real nameserver. When Exim is
7 running in its (new) test harness, DNS lookups are first passed to this program
8 instead of to the real resolver. (With a few exceptions - see the discussion in
9 the test suite's README file.) The program is also passed the name of the Exim
10 spool directory; it expects to find its "zone files" in ../dnszones relative to
11 that directory. Note that there is little checking in this program. The fake
12 zone files are assumed to be syntactically valid.
13
14 The zones that are handled are found by scanning the dnszones directory. A file
15 whose name is of the form db.ip4.x is a zone file for .x.in-addr.arpa; a file
16 whose name is of the form db.ip6.x is a zone file for .x.ip6.arpa; a file of
17 the form db.anything.else is a zone file for .anything.else. A file of the form
18 qualify.x.y specifies the domain that is used to qualify single-component
19 names, except for the name "dontqualify".
20
21 The arguments to the program are:
22
23 the name of the Exim spool directory
24 the domain name that is being sought
25 the DNS record type that is being sought
26
27 The output from the program is written to stdout. It is supposed to be in
28 exactly the same format as a traditional namserver response (see RFC 1035) so
29 that Exim can process it as normal. At present, no compression is used.
30 Error messages are written to stderr.
31
32 The return codes from the program are zero for success, and otherwise the
33 values that are set in h_errno after a failing call to the normal resolver:
34
35 1 HOST_NOT_FOUND host not found (authoritative)
36 2 TRY_AGAIN server failure
37 3 NO_RECOVERY non-recoverable error
38 4 NO_DATA valid name, no data of requested type
39
40 In a real nameserver, TRY_AGAIN is also used for a non-authoritative not found,
41 but it is not used for that here. There is also one extra return code:
42
43 5 PASS_ON requests Exim to call res_search()
44
45 This is used for zones that fakens does not recognize. It is also used if a
46 line in the zone file contains exactly this:
47
48 PASS ON NOT FOUND
49
50 and the domain is not found. It converts the the result to PASS_ON instead of
51 HOST_NOT_FOUND.
52
53 Any DNS record line in a zone file can be prefixed with "DELAY=" and
54 a number of milliseconds (followed by whitespace).
55
56 Any DNS record line in a zone file can be prefixed with "DNSSEC" and
57 at least one space; if all the records found by a lookup are marked
58 as such then the response will have the "AD" bit set. */
59
60 #include <ctype.h>
61 #include <stdarg.h>
62 #include <stdio.h>
63 #include <stdlib.h>
64 #include <string.h>
65 #include <netdb.h>
66 #include <errno.h>
67 #include <arpa/nameser.h>
68 #include <sys/types.h>
69 #include <sys/time.h>
70 #include <dirent.h>
71
72 #define FALSE 0
73 #define TRUE 1
74 #define PASS_ON 5
75
76 typedef int BOOL;
77 typedef unsigned char uschar;
78
79 #define CS (char *)
80 #define CCS (const char *)
81 #define US (unsigned char *)
82
83 #define Ustrcat(s,t) strcat(CS(s),CCS(t))
84 #define Ustrchr(s,n) US strchr(CCS(s),n)
85 #define Ustrcmp(s,t) strcmp(CCS(s),CCS(t))
86 #define Ustrcpy(s,t) strcpy(CS(s),CCS(t))
87 #define Ustrlen(s) (int)strlen(CCS(s))
88 #define Ustrncmp(s,t,n) strncmp(CCS(s),CCS(t),n)
89 #define Ustrncpy(s,t,n) strncpy(CS(s),CCS(t),n)
90
91 typedef struct zoneitem {
92 uschar *zone;
93 uschar *zonefile;
94 } zoneitem;
95
96 typedef struct tlist {
97 uschar *name;
98 int value;
99 } tlist;
100
101 /* On some (older?) operating systems, the standard ns_t_xxx definitions are
102 not available, and only the older T_xxx ones exist in nameser.h. If ns_t_a is
103 not defined, assume we are in this state. A really old system might not even
104 know about AAAA and SRV at all. */
105
106 #ifndef ns_t_a
107 # define ns_t_a T_A
108 # define ns_t_ns T_NS
109 # define ns_t_cname T_CNAME
110 # define ns_t_soa T_SOA
111 # define ns_t_ptr T_PTR
112 # define ns_t_mx T_MX
113 # define ns_t_txt T_TXT
114 # define ns_t_aaaa T_AAAA
115 # define ns_t_srv T_SRV
116 # define ns_t_tlsa T_TLSA
117 # ifndef T_AAAA
118 # define T_AAAA 28
119 # endif
120 # ifndef T_SRV
121 # define T_SRV 33
122 # endif
123 # ifndef T_TLSA
124 # define T_TLSA 52
125 # endif
126 #endif
127
128 static tlist type_list[] = {
129 { US"A", ns_t_a },
130 { US"NS", ns_t_ns },
131 { US"CNAME", ns_t_cname },
132 /* { US"SOA", ns_t_soa }, Not currently in use */
133 { US"PTR", ns_t_ptr },
134 { US"MX", ns_t_mx },
135 { US"TXT", ns_t_txt },
136 { US"AAAA", ns_t_aaaa },
137 { US"SRV", ns_t_srv },
138 { US"TLSA", ns_t_tlsa },
139 { NULL, 0 }
140 };
141
142
143
144 /*************************************************
145 * Get memory and sprintf into it *
146 *************************************************/
147
148 /* This is used when building a table of zones and their files.
149
150 Arguments:
151 format a format string
152 ... arguments
153
154 Returns: pointer to formatted string
155 */
156
157 static uschar *
158 fcopystring(uschar *format, ...)
159 {
160 uschar *yield;
161 char buffer[256];
162 va_list ap;
163 va_start(ap, format);
164 vsprintf(buffer, CS format, ap);
165 va_end(ap);
166 yield = (uschar *)malloc(Ustrlen(buffer) + 1);
167 Ustrcpy(yield, buffer);
168 return yield;
169 }
170
171
172 /*************************************************
173 * Pack name into memory *
174 *************************************************/
175
176 /* This function packs a domain name into memory according to DNS rules. At
177 present, it doesn't do any compression.
178
179 Arguments:
180 name the name
181 pk where to put it
182
183 Returns: the updated value of pk
184 */
185
186 static uschar *
187 packname(uschar *name, uschar *pk)
188 {
189 while (*name != 0)
190 {
191 uschar *p = name;
192 while (*p != 0 && *p != '.') p++;
193 *pk++ = (p - name);
194 memmove(pk, name, p - name);
195 pk += p - name;
196 name = (*p == 0)? p : p + 1;
197 }
198 *pk++ = 0;
199 return pk;
200 }
201
202 uschar *
203 bytefield(uschar ** pp, uschar * pk)
204 {
205 unsigned value = 0;
206 uschar * p = *pp;
207
208 while (isdigit(*p)) value = value*10 + *p++ - '0';
209 while (isspace(*p)) p++;
210 *pp = p;
211 *pk++ = value & 255;
212 return pk;
213 }
214
215 uschar *
216 shortfield(uschar ** pp, uschar * pk)
217 {
218 unsigned value = 0;
219 uschar * p = *pp;
220
221 while (isdigit(*p)) value = value*10 + *p++ - '0';
222 while (isspace(*p)) p++;
223 *pp = p;
224 *pk++ = (value >> 8) & 255;
225 *pk++ = value & 255;
226 return pk;
227 }
228
229
230
231 /*************************************************/
232
233 static void
234 milliwait(struct itimerval *itval)
235 {
236 sigset_t sigmask;
237 sigset_t old_sigmask;
238
239 if (itval->it_value.tv_usec < 100 && itval->it_value.tv_sec == 0)
240 return;
241 (void)sigemptyset(&sigmask); /* Empty mask */
242 (void)sigaddset(&sigmask, SIGALRM); /* Add SIGALRM */
243 (void)sigprocmask(SIG_BLOCK, &sigmask, &old_sigmask); /* Block SIGALRM */
244 (void)setitimer(ITIMER_REAL, itval, NULL); /* Start timer */
245 (void)sigfillset(&sigmask); /* All signals */
246 (void)sigdelset(&sigmask, SIGALRM); /* Remove SIGALRM */
247 (void)sigsuspend(&sigmask); /* Until SIGALRM */
248 (void)sigprocmask(SIG_SETMASK, &old_sigmask, NULL); /* Restore mask */
249 }
250
251 static void
252 millisleep(int msec)
253 {
254 struct itimerval itval;
255 itval.it_interval.tv_sec = 0;
256 itval.it_interval.tv_usec = 0;
257 itval.it_value.tv_sec = msec/1000;
258 itval.it_value.tv_usec = (msec % 1000) * 1000;
259 milliwait(&itval);
260 }
261
262
263 /*************************************************
264 * Scan file for RRs *
265 *************************************************/
266
267 /* This function scans an open "zone file" for appropriate records, and adds
268 any that are found to the output buffer.
269
270 Arguments:
271 f the input FILE
272 zone the current zone name
273 domain the domain we are looking for
274 qtype the type of RR we want
275 qtypelen the length of qtype
276 pkptr points to the output buffer pointer; this is updated
277 countptr points to the record count; this is updated
278
279 Returns: 0 on success, else HOST_NOT_FOUND or NO_DATA or NO_RECOVERY or
280 PASS_ON - the latter if a "PASS ON NOT FOUND" line is seen
281 */
282
283 static int
284 find_records(FILE *f, uschar *zone, uschar *domain, uschar *qtype,
285 int qtypelen, uschar **pkptr, int *countptr, BOOL * dnssec)
286 {
287 int yield = HOST_NOT_FOUND;
288 int domainlen = Ustrlen(domain);
289 BOOL pass_on_not_found = FALSE;
290 tlist *typeptr;
291 uschar *pk = *pkptr;
292 uschar buffer[256];
293 uschar rrdomain[256];
294 uschar RRdomain[256];
295
296 /* Decode the required type */
297
298 for (typeptr = type_list; typeptr->name != NULL; typeptr++)
299 { if (Ustrcmp(typeptr->name, qtype) == 0) break; }
300 if (typeptr->name == NULL)
301 {
302 fprintf(stderr, "fakens: unknown record type %s\n", qtype);
303 return NO_RECOVERY;
304 }
305
306 rrdomain[0] = 0; /* No previous domain */
307 (void)fseek(f, 0, SEEK_SET); /* Start again at the beginning */
308
309 *dnssec = TRUE; /* cancelled by first nonsecure rec found */
310
311 /* Scan for RRs */
312
313 while (fgets(CS buffer, sizeof(buffer), f) != NULL)
314 {
315 uschar *rdlptr;
316 uschar *p, *ep, *pp;
317 BOOL found_cname = FALSE;
318 int i, plen, value;
319 int tvalue = typeptr->value;
320 int qtlen = qtypelen;
321 BOOL rr_sec = FALSE;
322 int delay = 0;
323
324 p = buffer;
325 while (isspace(*p)) p++;
326 if (*p == 0 || *p == ';') continue;
327
328 if (Ustrncmp(p, US"PASS ON NOT FOUND", 17) == 0)
329 {
330 pass_on_not_found = TRUE;
331 continue;
332 }
333
334 ep = buffer + Ustrlen(buffer);
335 while (isspace(ep[-1])) ep--;
336 *ep = 0;
337
338 p = buffer;
339 for (;;)
340 {
341 if (Ustrncmp(p, US"DNSSEC ", 7) == 0) /* tagged as secure */
342 {
343 rr_sec = TRUE;
344 p += 7;
345 }
346 else if (Ustrncmp(p, US"DELAY=", 6) == 0) /* delay beforee response */
347 {
348 for (p += 6; *p >= '0' && *p <= '9'; p++)
349 delay = delay*10 + *p - '0';
350 while (isspace(*p)) p++;
351 }
352 else
353 break;
354 }
355
356 if (!isspace(*p))
357 {
358 uschar *pp = rrdomain;
359 uschar *PP = RRdomain;
360 while (!isspace(*p))
361 {
362 *pp++ = tolower(*p);
363 *PP++ = *p++;
364 }
365 if (pp[-1] != '.')
366 {
367 Ustrcpy(pp, zone);
368 Ustrcpy(PP, zone);
369 }
370 else
371 {
372 pp[-1] = 0;
373 PP[-1] = 0;
374 }
375 }
376
377 /* Compare domain names; first check for a wildcard */
378
379 if (rrdomain[0] == '*')
380 {
381 int restlen = Ustrlen(rrdomain) - 1;
382 if (domainlen > restlen &&
383 Ustrcmp(domain + domainlen - restlen, rrdomain + 1) != 0) continue;
384 }
385
386 /* Not a wildcard RR */
387
388 else if (Ustrcmp(domain, rrdomain) != 0) continue;
389
390 /* The domain matches */
391
392 if (yield == HOST_NOT_FOUND) yield = NO_DATA;
393
394 /* Compare RR types; a CNAME record is always returned */
395
396 while (isspace(*p)) p++;
397
398 if (Ustrncmp(p, "CNAME", 5) == 0)
399 {
400 tvalue = ns_t_cname;
401 qtlen = 5;
402 found_cname = TRUE;
403 }
404 else if (Ustrncmp(p, qtype, qtypelen) != 0 || !isspace(p[qtypelen])) continue;
405
406 /* Found a relevant record */
407
408 if (delay)
409 millisleep(delay);
410
411 if (!rr_sec)
412 *dnssec = FALSE; /* cancel AD return */
413
414 yield = 0;
415 *countptr = *countptr + 1;
416
417 p += qtlen;
418 while (isspace(*p)) p++;
419
420 /* For a wildcard record, use the search name; otherwise use the record's
421 name in its original case because it might contain upper case letters. */
422
423 pk = packname((rrdomain[0] == '*')? domain : RRdomain, pk);
424 *pk++ = (tvalue >> 8) & 255;
425 *pk++ = (tvalue) & 255;
426 *pk++ = 0;
427 *pk++ = 1; /* class = IN */
428
429 pk += 4; /* TTL field; don't care */
430
431 rdlptr = pk; /* remember rdlength field */
432 pk += 2;
433
434 /* The rest of the data depends on the type */
435
436 switch (tvalue)
437 {
438 case ns_t_soa: /* Not currently used */
439 break;
440
441 case ns_t_a:
442 for (i = 0; i < 4; i++)
443 {
444 value = 0;
445 while (isdigit(*p)) value = value*10 + *p++ - '0';
446 *pk++ = value;
447 p++;
448 }
449 break;
450
451 /* The only occurrence of a double colon is for ::1 */
452 case ns_t_aaaa:
453 if (Ustrcmp(p, "::1") == 0)
454 {
455 memset(pk, 0, 15);
456 pk += 15;
457 *pk++ = 1;
458 }
459 else for (i = 0; i < 8; i++)
460 {
461 value = 0;
462 while (isxdigit(*p))
463 {
464 value = value * 16 + toupper(*p) - (isdigit(*p)? '0' : '7');
465 p++;
466 }
467 *pk++ = (value >> 8) & 255;
468 *pk++ = value & 255;
469 p++;
470 }
471 break;
472
473 case ns_t_mx:
474 pk = shortfield(&p, pk);
475 if (ep[-1] != '.') sprintf(CS ep, "%s.", zone);
476 pk = packname(p, pk);
477 plen = Ustrlen(p);
478 break;
479
480 case ns_t_txt:
481 pp = pk++;
482 if (*p == '"') p++; /* Should always be the case */
483 while (*p != 0 && *p != '"') *pk++ = *p++;
484 *pp = pk - pp - 1;
485 break;
486
487 case ns_t_tlsa:
488 pk = bytefield(&p, pk); /* usage */
489 pk = bytefield(&p, pk); /* selector */
490 pk = bytefield(&p, pk); /* match type */
491 while (isxdigit(*p))
492 {
493 value = toupper(*p) - (isdigit(*p) ? '0' : '7') << 4;
494 if (isxdigit(*++p))
495 {
496 value |= toupper(*p) - (isdigit(*p) ? '0' : '7');
497 p++;
498 }
499 *pk++ = value & 255;
500 }
501
502 break;
503
504 case ns_t_srv:
505 for (i = 0; i < 3; i++)
506 {
507 value = 0;
508 while (isdigit(*p)) value = value*10 + *p++ - '0';
509 while (isspace(*p)) p++;
510 *pk++ = (value >> 8) & 255;
511 *pk++ = value & 255;
512 }
513
514 /* Fall through */
515
516 case ns_t_cname:
517 case ns_t_ns:
518 case ns_t_ptr:
519 if (ep[-1] != '.') sprintf(CS ep, "%s.", zone);
520 pk = packname(p, pk);
521 plen = Ustrlen(p);
522 break;
523 }
524
525 /* Fill in the length, and we are done with this RR */
526
527 rdlptr[0] = ((pk - rdlptr - 2) >> 8) & 255;
528 rdlptr[1] = (pk -rdlptr - 2) & 255;
529 }
530
531 *pkptr = pk;
532 return (yield == HOST_NOT_FOUND && pass_on_not_found)? PASS_ON : yield;
533 }
534
535
536 static void
537 alarmfn(int sig)
538 {
539 }
540
541 /*************************************************
542 * Entry point and main program *
543 *************************************************/
544
545 int
546 main(int argc, char **argv)
547 {
548 FILE *f;
549 DIR *d;
550 int domlen, qtypelen;
551 int yield, count;
552 int i;
553 int zonecount = 0;
554 struct dirent *de;
555 zoneitem zones[32];
556 uschar *qualify = NULL;
557 uschar *p, *zone;
558 uschar *zonefile = NULL;
559 uschar domain[256];
560 uschar buffer[256];
561 uschar qtype[12];
562 uschar packet[512];
563 uschar *pk = packet;
564 BOOL dnssec;
565
566 signal(SIGALRM, alarmfn);
567
568 if (argc != 4)
569 {
570 fprintf(stderr, "fakens: expected 3 arguments, received %d\n", argc-1);
571 return NO_RECOVERY;
572 }
573
574 /* Find the zones */
575
576 (void)sprintf(CS buffer, "%s/../dnszones", argv[1]);
577
578 d = opendir(CCS buffer);
579 if (d == NULL)
580 {
581 fprintf(stderr, "fakens: failed to opendir %s: %s\n", buffer,
582 strerror(errno));
583 return NO_RECOVERY;
584 }
585
586 while ((de = readdir(d)) != NULL)
587 {
588 uschar *name = US de->d_name;
589 if (Ustrncmp(name, "qualify.", 8) == 0)
590 {
591 qualify = fcopystring(US "%s", name + 7);
592 continue;
593 }
594 if (Ustrncmp(name, "db.", 3) != 0) continue;
595 if (Ustrncmp(name + 3, "ip4.", 4) == 0)
596 zones[zonecount].zone = fcopystring(US "%s.in-addr.arpa", name + 6);
597 else if (Ustrncmp(name + 3, "ip6.", 4) == 0)
598 zones[zonecount].zone = fcopystring(US "%s.ip6.arpa", name + 6);
599 else
600 zones[zonecount].zone = fcopystring(US "%s", name + 2);
601 zones[zonecount++].zonefile = fcopystring(US "%s", name);
602 }
603 (void)closedir(d);
604
605 /* Get the RR type and upper case it, and check that we recognize it. */
606
607 Ustrncpy(qtype, argv[3], sizeof(qtype));
608 qtypelen = Ustrlen(qtype);
609 for (p = qtype; *p != 0; p++) *p = toupper(*p);
610
611 /* Find the domain, lower case it, check that it is in a zone that we handle,
612 and set up the zone file name. The zone names in the table all start with a
613 dot. */
614
615 domlen = Ustrlen(argv[2]);
616 if (argv[2][domlen-1] == '.') domlen--;
617 Ustrncpy(domain, argv[2], domlen);
618 domain[domlen] = 0;
619 for (i = 0; i < domlen; i++) domain[i] = tolower(domain[i]);
620
621 if (Ustrchr(domain, '.') == NULL && qualify != NULL &&
622 Ustrcmp(domain, "dontqualify") != 0)
623 {
624 Ustrcat(domain, qualify);
625 domlen += Ustrlen(qualify);
626 }
627
628 for (i = 0; i < zonecount; i++)
629 {
630 int zlen;
631 zone = zones[i].zone;
632 zlen = Ustrlen(zone);
633 if (Ustrcmp(domain, zone+1) == 0 || (domlen >= zlen &&
634 Ustrcmp(domain + domlen - zlen, zone) == 0))
635 {
636 zonefile = zones[i].zonefile;
637 break;
638 }
639 }
640
641 if (zonefile == NULL)
642 {
643 fprintf(stderr, "fakens: query not in faked zone: domain is: %s\n", domain);
644 return PASS_ON;
645 }
646
647 (void)sprintf(CS buffer, "%s/../dnszones/%s", argv[1], zonefile);
648
649 /* Initialize the start of the response packet. We don't have to fake up
650 everything, because we know that Exim will look only at the answer and
651 additional section parts. */
652
653 memset(packet, 0, 12);
654 pk += 12;
655
656 /* Open the zone file. */
657
658 f = fopen(CS buffer, "r");
659 if (f == NULL)
660 {
661 fprintf(stderr, "fakens: failed to open %s: %s\n", buffer, strerror(errno));
662 return NO_RECOVERY;
663 }
664
665 /* Find the records we want, and add them to the result. */
666
667 count = 0;
668 yield = find_records(f, zone, domain, qtype, qtypelen, &pk, &count, &dnssec);
669 if (yield == NO_RECOVERY) goto END_OFF;
670
671 packet[6] = (count >> 8) & 255;
672 packet[7] = count & 255;
673
674 /* There is no need to return any additional records because Exim no longer
675 (from release 4.61) makes any use of them. */
676
677 packet[10] = 0;
678 packet[11] = 0;
679
680 if (dnssec)
681 ((HEADER *)packet)->ad = 1;
682
683 /* Close the zone file, write the result, and return. */
684
685 END_OFF:
686 (void)fclose(f);
687 (void)fwrite(packet, 1, pk - packet, stdout);
688 return yield;
689 }
690
691 /* vi: aw ai sw=2
692 */
693 /* End of fakens.c */