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