1 /*************************************************
2 * fakens - A Fake Nameserver Program *
3 *************************************************/
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 exim config_main_directory. Note that there is little checking in this program. The fake
12 zone files are assumed to be syntactically valid.
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".
21 The arguments to the program are:
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
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.
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:
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
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:
43 5 PASS_ON requests Exim to call res_search()
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:
50 and the domain is not found. It converts the the result to PASS_ON instead of
53 Any DNS record line in a zone file can be prefixed with "DELAY=" and
54 a number of milliseconds (followed by one space).
56 Any DNS record line in a zone file can be prefixed with "DNSSEC ";
57 if all the records found by a lookup are marked
58 as such then the response will have the "AD" bit set.
60 Any DNS record line in a zone file can be prefixed with "AA "
61 if all the records found by a lookup are marked
62 as such then the response will have the "AA" bit set.
64 Any DNS record line in a zone file can be prefixed with "TTL=" and
65 a number of seconds (followed by one space).
77 #include <arpa/nameser.h>
78 #include <arpa/inet.h>
79 #include <sys/types.h>
89 typedef unsigned char uschar;
92 #define CCS (const char *)
93 #define US (unsigned char *)
95 #define Ustrcat(s,t) strcat(CS(s),CCS(t))
96 #define Ustrchr(s,n) US strchr(CCS(s),n)
97 #define Ustrcmp(s,t) strcmp(CCS(s),CCS(t))
98 #define Ustrcpy(s,t) strcpy(CS(s),CCS(t))
99 #define Ustrlen(s) (int)strlen(CCS(s))
100 #define Ustrncmp(s,t,n) strncmp(CCS(s),CCS(t),n)
101 #define Ustrncpy(s,t,n) strncpy(CS(s),CCS(t),n)
103 typedef struct zoneitem {
108 typedef struct tlist {
113 #define DEFAULT_TTL 3600U
115 /* On some (older?) operating systems, the standard ns_t_xxx definitions are
116 not available, and only the older T_xxx ones exist in nameser.h. If ns_t_a is
117 not defined, assume we are in this state. A really old system might not even
118 know about AAAA and SRV at all. */
122 # define ns_t_ns T_NS
123 # define ns_t_cname T_CNAME
124 # define ns_t_soa T_SOA
125 # define ns_t_ptr T_PTR
126 # define ns_t_mx T_MX
127 # define ns_t_txt T_TXT
128 # define ns_t_aaaa T_AAAA
129 # define ns_t_srv T_SRV
130 # define ns_t_tlsa T_TLSA
142 static tlist type_list[] = {
145 { US"CNAME", ns_t_cname },
146 { US"SOA", ns_t_soa },
147 { US"PTR", ns_t_ptr },
149 { US"TXT", ns_t_txt },
150 { US"AAAA", ns_t_aaaa },
151 { US"SRV", ns_t_srv },
152 { US"TLSA", ns_t_tlsa },
158 /*************************************************
159 * Get memory and sprintf into it *
160 *************************************************/
162 /* This is used when building a table of zones and their files.
165 format a format string
168 Returns: pointer to formatted string
172 fcopystring(uschar *format, ...)
177 va_start(ap, format);
178 vsprintf(buffer, CS format, ap);
180 yield = (uschar *)malloc(Ustrlen(buffer) + 1);
181 Ustrcpy(yield, buffer);
186 /*************************************************
187 * Pack name into memory *
188 *************************************************/
190 /* This function packs a domain name into memory according to DNS rules. At
191 present, it doesn't do any compression.
197 Returns: the updated value of pk
201 packname(uschar *name, uschar *pk)
206 while (*p != 0 && *p != '.') p++;
208 memmove(pk, name, p - name);
210 name = (*p == 0)? p : p + 1;
217 bytefield(uschar ** pp, uschar * pk)
222 while (isdigit(*p)) value = value*10 + *p++ - '0';
223 while (isspace(*p)) p++;
230 shortfield(uschar ** pp, uschar * pk)
235 while (isdigit(*p)) value = value*10 + *p++ - '0';
236 while (isspace(*p)) p++;
238 *pk++ = (value >> 8) & 255;
244 longfield(uschar ** pp, uschar * pk)
246 unsigned long value = 0;
249 while (isdigit(*p)) value = value*10 + *p++ - '0';
250 while (isspace(*p)) p++;
252 *pk++ = (value >> 24) & 255;
253 *pk++ = (value >> 16) & 255;
254 *pk++ = (value >> 8) & 255;
261 /*************************************************/
264 milliwait(struct itimerval *itval)
267 sigset_t old_sigmask;
269 if (itval->it_value.tv_usec < 100 && itval->it_value.tv_sec == 0)
271 (void)sigemptyset(&sigmask); /* Empty mask */
272 (void)sigaddset(&sigmask, SIGALRM); /* Add SIGALRM */
273 (void)sigprocmask(SIG_BLOCK, &sigmask, &old_sigmask); /* Block SIGALRM */
274 (void)setitimer(ITIMER_REAL, itval, NULL); /* Start timer */
275 (void)sigfillset(&sigmask); /* All signals */
276 (void)sigdelset(&sigmask, SIGALRM); /* Remove SIGALRM */
277 (void)sigsuspend(&sigmask); /* Until SIGALRM */
278 (void)sigprocmask(SIG_SETMASK, &old_sigmask, NULL); /* Restore mask */
284 struct itimerval itval;
285 itval.it_interval.tv_sec = 0;
286 itval.it_interval.tv_usec = 0;
287 itval.it_value.tv_sec = msec/1000;
288 itval.it_value.tv_usec = (msec % 1000) * 1000;
293 /*************************************************
294 * Scan file for RRs *
295 *************************************************/
297 /* This function scans an open "zone file" for appropriate records, and adds
298 any that are found to the output buffer.
302 zone the current zone name
303 domain the domain we are looking for
304 qtype the type of RR we want
305 qtypelen the length of qtype
306 pkptr points to the output buffer pointer; this is updated
307 countptr points to the record count; this is updated
308 dnssec points to the AD flag indicator; this is updated
309 aa points to the AA flag indicator; this is updated
311 Returns: 0 on success, else HOST_NOT_FOUND or NO_DATA or NO_RECOVERY or
312 PASS_ON - the latter if a "PASS ON NOT FOUND" line is seen
316 find_records(FILE *f, uschar *zone, uschar *domain, uschar *qtype,
317 int qtypelen, uschar **pkptr, int *countptr, BOOL * dnssec, BOOL * aa)
319 int yield = HOST_NOT_FOUND;
320 int domainlen = Ustrlen(domain);
321 BOOL pass_on_not_found = FALSE;
325 uschar rrdomain[256];
326 uschar RRdomain[256];
329 /* Decode the required type */
330 for (typeptr = type_list; typeptr->name != NULL; typeptr++)
331 { if (Ustrcmp(typeptr->name, qtype) == 0) break; }
332 if (typeptr->name == NULL)
334 fprintf(stderr, "fakens: unknown record type %s\n", qtype);
338 rrdomain[0] = 0; /* No previous domain */
339 (void)fseek(f, 0, SEEK_SET); /* Start again at the beginning */
341 if (dnssec) *dnssec = TRUE; /* cancelled by first nonsecure rec found */
342 if (aa) *aa = TRUE; /* cancelled by first non-aa rec found */
346 while (fgets(CS buffer, sizeof(buffer), f) != NULL)
350 BOOL found_cname = FALSE;
352 int tvalue = typeptr->value;
353 int qtlen = qtypelen;
357 uint ttl = DEFAULT_TTL;
360 while (isspace(*p)) p++;
361 if (*p == 0 || *p == ';') continue;
363 if (Ustrncmp(p, US"PASS ON NOT FOUND", 17) == 0)
365 pass_on_not_found = TRUE;
369 ep = buffer + Ustrlen(buffer);
370 while (isspace(ep[-1])) ep--;
376 if (Ustrncmp(p, US"DNSSEC ", 7) == 0) /* tagged as secure */
381 else if (Ustrncmp(p, US"AA ", 3) == 0) /* tagged as authoritive */
386 else if (Ustrncmp(p, US"DELAY=", 6) == 0) /* delay before response */
388 for (p += 6; *p >= '0' && *p <= '9'; p++) delay = delay*10 + *p - '0';
389 if (isspace(*p)) p++;
391 else if (Ustrncmp(p, US"TTL=", 4) == 0) /* TTL for record */
394 for (p += 4; *p >= '0' && *p <= '9'; p++) ttl = ttl*10 + *p - '0';
395 if (isspace(*p)) p++;
401 if (!isspace(*p)) /* new domain name */
403 uschar *pp = rrdomain;
404 uschar *PP = RRdomain;
420 } /* else use previous line's domain name */
422 /* Compare domain names; first check for a wildcard */
424 if (rrdomain[0] == '*')
426 int restlen = Ustrlen(rrdomain) - 1;
427 if (domainlen > restlen &&
428 Ustrcmp(domain + domainlen - restlen, rrdomain + 1) != 0) continue;
431 /* Not a wildcard RR */
433 else if (Ustrcmp(domain, rrdomain) != 0) continue;
435 /* The domain matches */
437 if (yield == HOST_NOT_FOUND) yield = NO_DATA;
439 /* Compare RR types; a CNAME record is always returned */
441 while (isspace(*p)) p++;
443 if (Ustrncmp(p, "CNAME", 5) == 0)
449 else if (Ustrncmp(p, qtype, qtypelen) != 0 || !isspace(p[qtypelen])) continue;
451 /* Found a relevant record */
455 if (dnssec && !rr_sec)
456 *dnssec = FALSE; /* cancel AD return */
459 *aa = FALSE; /* cancel AA return */
462 *countptr = *countptr + 1;
465 while (isspace(*p)) p++;
467 /* For a wildcard record, use the search name; otherwise use the record's
468 name in its original case because it might contain upper case letters. */
470 pk = packname((rrdomain[0] == '*')? domain : RRdomain, pk);
471 *pk++ = (tvalue >> 8) & 255;
472 *pk++ = (tvalue) & 255;
474 *pk++ = 1; /* class = IN */
476 *pk++ = (ttl >>24) & 255;
477 *pk++ = (ttl >>16) & 255;
478 *pk++ = (ttl >> 8) & 255;
481 rdlptr = pk; /* remember rdlength field */
484 /* The rest of the data depends on the type */
491 if (ep[-1] != '.') sprintf(CS ep, "%s.", zone);
492 pk = packname(p, pk); /* primary ns */
493 p = strtok(NULL, " ");
494 pk = packname(p , pk); /* responsible mailbox */
495 *(p += strlen(p)) = ' ';
496 while (isspace(*p)) p++;
497 pk = longfield(&p, pk); /* serial */
498 pk = longfield(&p, pk); /* refresh */
499 pk = longfield(&p, pk); /* retry */
500 pk = longfield(&p, pk); /* expire */
501 pk = longfield(&p, pk); /* minimum */
505 inet_pton(AF_INET, p, pk); /* FIXME: error checking */
510 inet_pton(AF_INET6, p, pk); /* FIXME: error checking */
515 pk = shortfield(&p, pk);
516 if (ep[-1] != '.') sprintf(CS ep, "%s.", zone);
517 pk = packname(p, pk);
522 if (*p == '"') p++; /* Should always be the case */
523 while (*p != 0 && *p != '"') *pk++ = *p++;
528 pk = bytefield(&p, pk); /* usage */
529 pk = bytefield(&p, pk); /* selector */
530 pk = bytefield(&p, pk); /* match type */
533 value = toupper(*p) - (isdigit(*p) ? '0' : '7') << 4;
536 value |= toupper(*p) - (isdigit(*p) ? '0' : '7');
545 for (i = 0; i < 3; i++)
548 while (isdigit(*p)) value = value*10 + *p++ - '0';
549 while (isspace(*p)) p++;
550 *pk++ = (value >> 8) & 255;
559 if (ep[-1] != '.') sprintf(CS ep, "%s.", zone);
560 pk = packname(p, pk);
564 /* Fill in the length, and we are done with this RR */
566 rdlptr[0] = ((pk - rdlptr - 2) >> 8) & 255;
567 rdlptr[1] = (pk -rdlptr - 2) & 255;
571 return (yield == HOST_NOT_FOUND && pass_on_not_found)? PASS_ON : yield;
581 /*************************************************
582 * Special-purpose domains *
583 *************************************************/
586 special_manyhome(uschar * packet, uschar * domain)
588 uschar *pk = packet + 12;
592 memset(packet, 0, 12);
594 for (i = 104; i <= 111; i++) for (j = 0; j <= 255; j++)
596 pk = packname(domain, pk);
597 *pk++ = (ns_t_a >> 8) & 255;
598 *pk++ = (ns_t_a) & 255;
600 *pk++ = 1; /* class = IN */
601 pk += 4; /* TTL field; don't care */
602 rdlptr = pk; /* remember rdlength field */
605 *pk++ = 10; *pk++ = 250; *pk++ = i; *pk++ = j;
607 rdlptr[0] = ((pk - rdlptr - 2) >> 8) & 255;
608 rdlptr[1] = (pk - rdlptr - 2) & 255;
611 packet[6] = (2048 >> 8) & 255;
612 packet[7] = 2048 & 255;
616 (void)fwrite(packet, 1, pk - packet, stdout);
621 special_again(uschar * packet, uschar * domain)
623 int delay = atoi(CCS domain); /* digits at the start of the name */
624 if (delay > 0) sleep(delay);
629 /*************************************************
630 * Entry point and main program *
631 *************************************************/
634 main(int argc, char **argv)
638 int domlen, qtypelen;
644 uschar *qualify = NULL;
646 uschar *zonefile = NULL;
650 uschar packet[2048 * 32 + 32];
651 HEADER *header = (HEADER *)packet;
656 signal(SIGALRM, alarmfn);
660 fprintf(stderr, "fakens: expected 3 arguments, received %d\n", argc-1);
666 (void)sprintf(CS buffer, "%s/dnszones", argv[1]);
668 d = opendir(CCS buffer);
671 fprintf(stderr, "fakens: failed to opendir %s: %s\n", buffer,
676 while ((de = readdir(d)) != NULL)
678 uschar *name = US de->d_name;
679 if (Ustrncmp(name, "qualify.", 8) == 0)
681 qualify = fcopystring(US "%s", name + 7);
684 if (Ustrncmp(name, "db.", 3) != 0) continue;
685 if (Ustrncmp(name + 3, "ip4.", 4) == 0)
686 zones[zonecount].zone = fcopystring(US "%s.in-addr.arpa", name + 6);
687 else if (Ustrncmp(name + 3, "ip6.", 4) == 0)
688 zones[zonecount].zone = fcopystring(US "%s.ip6.arpa", name + 6);
690 zones[zonecount].zone = fcopystring(US "%s", name + 2);
691 zones[zonecount++].zonefile = fcopystring(US "%s", name);
695 /* Get the RR type and upper case it, and check that we recognize it. */
697 Ustrncpy(qtype, argv[3], sizeof(qtype));
698 qtypelen = Ustrlen(qtype);
699 for (p = qtype; *p != 0; p++) *p = toupper(*p);
701 /* Find the domain, lower case it, deal with any specials,
702 check that it is in a zone that we handle,
703 and set up the zone file name. The zone names in the table all start with a
706 domlen = Ustrlen(argv[2]);
707 if (argv[2][domlen-1] == '.') domlen--;
708 Ustrncpy(domain, argv[2], domlen);
710 for (i = 0; i < domlen; i++) domain[i] = tolower(domain[i]);
712 if (Ustrcmp(domain, "manyhome.test.ex") == 0 && Ustrcmp(qtype, "A") == 0)
713 return special_manyhome(packet, domain);
714 else if (domlen >= 14 && Ustrcmp(domain + domlen - 14, "test.again.dns") == 0)
715 return special_again(packet, domain);
716 else if (domlen >= 13 && Ustrcmp(domain + domlen - 13, "test.fail.dns") == 0)
720 if (Ustrchr(domain, '.') == NULL && qualify != NULL &&
721 Ustrcmp(domain, "dontqualify") != 0)
723 Ustrcat(domain, qualify);
724 domlen += Ustrlen(qualify);
727 for (i = 0; i < zonecount; i++)
730 zone = zones[i].zone;
731 zlen = Ustrlen(zone);
732 if (Ustrcmp(domain, zone+1) == 0 || (domlen >= zlen &&
733 Ustrcmp(domain + domlen - zlen, zone) == 0))
735 zonefile = zones[i].zonefile;
740 if (zonefile == NULL)
742 fprintf(stderr, "fakens: query not in faked zone: domain is: %s\n", domain);
746 (void)sprintf(CS buffer, "%s/dnszones/%s", argv[1], zonefile);
748 /* Initialize the start of the response packet. We don't have to fake up
749 everything, because we know that Exim will look only at the answer and
750 additional section parts. */
752 memset(packet, 0, 12);
755 /* Open the zone file. */
757 f = fopen(CS buffer, "r");
760 fprintf(stderr, "fakens: failed to open %s: %s\n", buffer, strerror(errno));
764 /* Find the records we want, and add them to the result. */
767 yield = find_records(f, zone, domain, qtype, qtypelen, &pk, &count, &dnssec, &aa);
768 if (yield == NO_RECOVERY) goto END_OFF;
769 header->ancount = htons(count);
771 /* If the AA bit should be set (as indicated by the AA prefix in the zone file),
772 we are expected to return some records in the authortive section. Bind9: If
773 there is data in the answer section, the authoritive section contains the NS
774 records, otherwise it contains the SOA record. Currently we mimic this
775 behaviour for the first case (there is some answer record).
779 find_records(f, zone, zone[0] == '.' ? zone+1 : zone, US"NS", 2, &pk, &count, NULL, NULL);
780 header->nscount = htons(count - ntohs(header->ancount));
782 /* There is no need to return any additional records because Exim no longer
783 (from release 4.61) makes any use of them. */
792 /* Close the zone file, write the result, and return. */
796 (void)fwrite(packet, 1, pk - packet, stdout);
800 /* vi: aw ai sw=2 sts=2 ts=8 et
802 /* End of fakens.c */