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 */