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.
74 #include <arpa/nameser.h>
75 #include <sys/types.h>
84 typedef unsigned char uschar;
87 #define CCS (const char *)
88 #define US (unsigned char *)
90 #define Ustrcat(s,t) strcat(CS(s),CCS(t))
91 #define Ustrchr(s,n) US strchr(CCS(s),n)
92 #define Ustrcmp(s,t) strcmp(CCS(s),CCS(t))
93 #define Ustrcpy(s,t) strcpy(CS(s),CCS(t))
94 #define Ustrlen(s) (int)strlen(CCS(s))
95 #define Ustrncmp(s,t,n) strncmp(CCS(s),CCS(t),n)
96 #define Ustrncpy(s,t,n) strncpy(CS(s),CCS(t),n)
98 typedef struct zoneitem {
103 typedef struct tlist {
108 /* On some (older?) operating systems, the standard ns_t_xxx definitions are
109 not available, and only the older T_xxx ones exist in nameser.h. If ns_t_a is
110 not defined, assume we are in this state. A really old system might not even
111 know about AAAA and SRV at all. */
115 # define ns_t_ns T_NS
116 # define ns_t_cname T_CNAME
117 # define ns_t_soa T_SOA
118 # define ns_t_ptr T_PTR
119 # define ns_t_mx T_MX
120 # define ns_t_txt T_TXT
121 # define ns_t_aaaa T_AAAA
122 # define ns_t_srv T_SRV
123 # define ns_t_tlsa T_TLSA
135 static tlist type_list[] = {
138 { US"CNAME", ns_t_cname },
139 { US"SOA", ns_t_soa },
140 { US"PTR", ns_t_ptr },
142 { US"TXT", ns_t_txt },
143 { US"AAAA", ns_t_aaaa },
144 { US"SRV", ns_t_srv },
145 { US"TLSA", ns_t_tlsa },
151 /*************************************************
152 * Get memory and sprintf into it *
153 *************************************************/
155 /* This is used when building a table of zones and their files.
158 format a format string
161 Returns: pointer to formatted string
165 fcopystring(uschar *format, ...)
170 va_start(ap, format);
171 vsprintf(buffer, CS format, ap);
173 yield = (uschar *)malloc(Ustrlen(buffer) + 1);
174 Ustrcpy(yield, buffer);
179 /*************************************************
180 * Pack name into memory *
181 *************************************************/
183 /* This function packs a domain name into memory according to DNS rules. At
184 present, it doesn't do any compression.
190 Returns: the updated value of pk
194 packname(uschar *name, uschar *pk)
199 while (*p != 0 && *p != '.') p++;
201 memmove(pk, name, p - name);
203 name = (*p == 0)? p : p + 1;
210 bytefield(uschar ** pp, uschar * pk)
215 while (isdigit(*p)) value = value*10 + *p++ - '0';
216 while (isspace(*p)) p++;
223 shortfield(uschar ** pp, uschar * pk)
228 while (isdigit(*p)) value = value*10 + *p++ - '0';
229 while (isspace(*p)) p++;
231 *pk++ = (value >> 8) & 255;
237 longfield(uschar ** pp, uschar * pk)
239 unsigned long value = 0;
242 while (isdigit(*p)) value = value*10 + *p++ - '0';
243 while (isspace(*p)) p++;
245 *pk++ = (value >> 24) & 255;
246 *pk++ = (value >> 16) & 255;
247 *pk++ = (value >> 8) & 255;
254 /*************************************************/
257 milliwait(struct itimerval *itval)
260 sigset_t old_sigmask;
262 if (itval->it_value.tv_usec < 100 && itval->it_value.tv_sec == 0)
264 (void)sigemptyset(&sigmask); /* Empty mask */
265 (void)sigaddset(&sigmask, SIGALRM); /* Add SIGALRM */
266 (void)sigprocmask(SIG_BLOCK, &sigmask, &old_sigmask); /* Block SIGALRM */
267 (void)setitimer(ITIMER_REAL, itval, NULL); /* Start timer */
268 (void)sigfillset(&sigmask); /* All signals */
269 (void)sigdelset(&sigmask, SIGALRM); /* Remove SIGALRM */
270 (void)sigsuspend(&sigmask); /* Until SIGALRM */
271 (void)sigprocmask(SIG_SETMASK, &old_sigmask, NULL); /* Restore mask */
277 struct itimerval itval;
278 itval.it_interval.tv_sec = 0;
279 itval.it_interval.tv_usec = 0;
280 itval.it_value.tv_sec = msec/1000;
281 itval.it_value.tv_usec = (msec % 1000) * 1000;
286 /*************************************************
287 * Scan file for RRs *
288 *************************************************/
290 /* This function scans an open "zone file" for appropriate records, and adds
291 any that are found to the output buffer.
295 zone the current zone name
296 domain the domain we are looking for
297 qtype the type of RR we want
298 qtypelen the length of qtype
299 pkptr points to the output buffer pointer; this is updated
300 countptr points to the record count; this is updated
301 dnssec points to the AD flag indicator; this updated
302 aa points to the AA flag indicator; this updated
304 Returns: 0 on success, else HOST_NOT_FOUND or NO_DATA or NO_RECOVERY or
305 PASS_ON - the latter if a "PASS ON NOT FOUND" line is seen
309 find_records(FILE *f, uschar *zone, uschar *domain, uschar *qtype,
310 int qtypelen, uschar **pkptr, int *countptr, BOOL * dnssec, BOOL * aa)
312 int yield = HOST_NOT_FOUND;
313 int domainlen = Ustrlen(domain);
314 BOOL pass_on_not_found = FALSE;
318 uschar rrdomain[256];
319 uschar RRdomain[256];
321 /* Decode the required type */
323 for (typeptr = type_list; typeptr->name != NULL; typeptr++)
324 { if (Ustrcmp(typeptr->name, qtype) == 0) break; }
325 if (typeptr->name == NULL)
327 fprintf(stderr, "fakens: unknown record type %s\n", qtype);
331 rrdomain[0] = 0; /* No previous domain */
332 (void)fseek(f, 0, SEEK_SET); /* Start again at the beginning */
334 *dnssec = TRUE; /* cancelled by first nonsecure rec found */
335 *aa = TRUE; /* cancelled by first non-authoritive record */
339 while (fgets(CS buffer, sizeof(buffer), f) != NULL)
343 BOOL found_cname = FALSE;
345 int tvalue = typeptr->value;
346 int qtlen = qtypelen;
352 while (isspace(*p)) p++;
353 if (*p == 0 || *p == ';') continue;
355 if (Ustrncmp(p, US"PASS ON NOT FOUND", 17) == 0)
357 pass_on_not_found = TRUE;
361 ep = buffer + Ustrlen(buffer);
362 while (isspace(ep[-1])) ep--;
368 if (Ustrncmp(p, US"DNSSEC ", 7) == 0) /* tagged as secure */
373 else if (Ustrncmp(p, US"AA ", 3) == 0) /* tagged as authoritive */
378 else if (Ustrncmp(p, US"DELAY=", 6) == 0) /* delay before response */
380 for (p += 6; *p >= '0' && *p <= '9'; p++) delay = delay*10 + *p - '0';
381 if (isspace(*p)) p++;
387 if (!isspace(*p)) /* new domain name */
389 uschar *pp = rrdomain;
390 uschar *PP = RRdomain;
406 } /* else use previous line's domain name */
408 /* Compare domain names; first check for a wildcard */
410 if (rrdomain[0] == '*')
412 int restlen = Ustrlen(rrdomain) - 1;
413 if (domainlen > restlen &&
414 Ustrcmp(domain + domainlen - restlen, rrdomain + 1) != 0) continue;
417 /* Not a wildcard RR */
419 else if (Ustrcmp(domain, rrdomain) != 0) continue;
421 /* The domain matches */
423 if (yield == HOST_NOT_FOUND) yield = NO_DATA;
425 /* Compare RR types; a CNAME record is always returned */
427 while (isspace(*p)) p++;
429 if (Ustrncmp(p, "CNAME", 5) == 0)
435 else if (Ustrncmp(p, qtype, qtypelen) != 0 || !isspace(p[qtypelen])) continue;
437 /* Found a relevant record */
443 *dnssec = FALSE; /* cancel AD return */
446 *aa = FALSE; /* cancel AA return */
449 *countptr = *countptr + 1;
452 while (isspace(*p)) p++;
454 /* For a wildcard record, use the search name; otherwise use the record's
455 name in its original case because it might contain upper case letters. */
457 pk = packname((rrdomain[0] == '*')? domain : RRdomain, pk);
458 *pk++ = (tvalue >> 8) & 255;
459 *pk++ = (tvalue) & 255;
461 *pk++ = 1; /* class = IN */
463 pk += 4; /* TTL field; don't care */
465 rdlptr = pk; /* remember rdlength field */
468 /* The rest of the data depends on the type */
475 if (ep[-1] != '.') sprintf(CS ep, "%s.", zone);
476 pk = packname(p, pk); /* primary ns */
477 p = strtok(NULL, " ");
478 pk = packname(p , pk); /* responsible mailbox */
479 *(p += strlen(p)) = ' ';
480 while (isspace(*p)) p++;
481 pk = longfield(&p, pk); /* serial */
482 pk = longfield(&p, pk); /* refresh */
483 pk = longfield(&p, pk); /* retry */
484 pk = longfield(&p, pk); /* expire */
485 pk = longfield(&p, pk); /* minimum */
489 for (i = 0; i < 4; i++)
492 while (isdigit(*p)) value = value*10 + *p++ - '0';
498 /* The only occurrence of a double colon is for ::1 */
500 if (Ustrcmp(p, "::1") == 0)
506 else for (i = 0; i < 8; i++)
511 value = value * 16 + toupper(*p) - (isdigit(*p)? '0' : '7');
514 *pk++ = (value >> 8) & 255;
521 pk = shortfield(&p, pk);
522 if (ep[-1] != '.') sprintf(CS ep, "%s.", zone);
523 pk = packname(p, pk);
528 if (*p == '"') p++; /* Should always be the case */
529 while (*p != 0 && *p != '"') *pk++ = *p++;
534 pk = bytefield(&p, pk); /* usage */
535 pk = bytefield(&p, pk); /* selector */
536 pk = bytefield(&p, pk); /* match type */
539 value = toupper(*p) - (isdigit(*p) ? '0' : '7') << 4;
542 value |= toupper(*p) - (isdigit(*p) ? '0' : '7');
551 for (i = 0; i < 3; i++)
554 while (isdigit(*p)) value = value*10 + *p++ - '0';
555 while (isspace(*p)) p++;
556 *pk++ = (value >> 8) & 255;
565 if (ep[-1] != '.') sprintf(CS ep, "%s.", zone);
566 pk = packname(p, pk);
570 /* Fill in the length, and we are done with this RR */
572 rdlptr[0] = ((pk - rdlptr - 2) >> 8) & 255;
573 rdlptr[1] = (pk -rdlptr - 2) & 255;
577 return (yield == HOST_NOT_FOUND && pass_on_not_found)? PASS_ON : yield;
587 /*************************************************
588 * Special-purpose domains *
589 *************************************************/
592 special_manyhome(uschar * packet, uschar * domain)
594 uschar *pk = packet + 12;
598 memset(packet, 0, 12);
600 for (i = 104; i <= 111; i++) for (j = 0; j <= 255; j++)
602 pk = packname(domain, pk);
603 *pk++ = (ns_t_a >> 8) & 255;
604 *pk++ = (ns_t_a) & 255;
606 *pk++ = 1; /* class = IN */
607 pk += 4; /* TTL field; don't care */
608 rdlptr = pk; /* remember rdlength field */
611 *pk++ = 10; *pk++ = 250; *pk++ = i; *pk++ = j;
613 rdlptr[0] = ((pk - rdlptr - 2) >> 8) & 255;
614 rdlptr[1] = (pk - rdlptr - 2) & 255;
617 packet[6] = (2048 >> 8) & 255;
618 packet[7] = 2048 & 255;
622 (void)fwrite(packet, 1, pk - packet, stdout);
627 special_again(uschar * packet, uschar * domain)
629 int delay = atoi(CCS domain); /* digits at the start of the name */
630 if (delay > 0) sleep(delay);
635 /*************************************************
636 * Entry point and main program *
637 *************************************************/
640 main(int argc, char **argv)
644 int domlen, qtypelen;
650 uschar *qualify = NULL;
652 uschar *zonefile = NULL;
656 uschar packet[2048 * 32 + 32];
661 signal(SIGALRM, alarmfn);
665 fprintf(stderr, "fakens: expected 3 arguments, received %d\n", argc-1);
671 (void)sprintf(CS buffer, "%s/dnszones", argv[1]);
673 d = opendir(CCS buffer);
676 fprintf(stderr, "fakens: failed to opendir %s: %s\n", buffer,
681 while ((de = readdir(d)) != NULL)
683 uschar *name = US de->d_name;
684 if (Ustrncmp(name, "qualify.", 8) == 0)
686 qualify = fcopystring(US "%s", name + 7);
689 if (Ustrncmp(name, "db.", 3) != 0) continue;
690 if (Ustrncmp(name + 3, "ip4.", 4) == 0)
691 zones[zonecount].zone = fcopystring(US "%s.in-addr.arpa", name + 6);
692 else if (Ustrncmp(name + 3, "ip6.", 4) == 0)
693 zones[zonecount].zone = fcopystring(US "%s.ip6.arpa", name + 6);
695 zones[zonecount].zone = fcopystring(US "%s", name + 2);
696 zones[zonecount++].zonefile = fcopystring(US "%s", name);
700 /* Get the RR type and upper case it, and check that we recognize it. */
702 Ustrncpy(qtype, argv[3], sizeof(qtype));
703 qtypelen = Ustrlen(qtype);
704 for (p = qtype; *p != 0; p++) *p = toupper(*p);
706 /* Find the domain, lower case it, deal with any specials,
707 check that it is in a zone that we handle,
708 and set up the zone file name. The zone names in the table all start with a
711 domlen = Ustrlen(argv[2]);
712 if (argv[2][domlen-1] == '.') domlen--;
713 Ustrncpy(domain, argv[2], domlen);
715 for (i = 0; i < domlen; i++) domain[i] = tolower(domain[i]);
717 if (Ustrcmp(domain, "manyhome.test.ex") == 0 && Ustrcmp(qtype, "A") == 0)
718 return special_manyhome(packet, domain);
719 else if (domlen >= 14 && Ustrcmp(domain + domlen - 14, "test.again.dns") == 0)
720 return special_again(packet, domain);
721 else if (domlen >= 13 && Ustrcmp(domain + domlen - 13, "test.fail.dns") == 0)
725 if (Ustrchr(domain, '.') == NULL && qualify != NULL &&
726 Ustrcmp(domain, "dontqualify") != 0)
728 Ustrcat(domain, qualify);
729 domlen += Ustrlen(qualify);
732 for (i = 0; i < zonecount; i++)
735 zone = zones[i].zone;
736 zlen = Ustrlen(zone);
737 if (Ustrcmp(domain, zone+1) == 0 || (domlen >= zlen &&
738 Ustrcmp(domain + domlen - zlen, zone) == 0))
740 zonefile = zones[i].zonefile;
745 if (zonefile == NULL)
747 fprintf(stderr, "fakens: query not in faked zone: domain is: %s\n", domain);
751 (void)sprintf(CS buffer, "%s/dnszones/%s", argv[1], zonefile);
753 /* Initialize the start of the response packet. We don't have to fake up
754 everything, because we know that Exim will look only at the answer and
755 additional section parts. */
757 memset(packet, 0, 12);
760 /* Open the zone file. */
762 f = fopen(CS buffer, "r");
765 fprintf(stderr, "fakens: failed to open %s: %s\n", buffer, strerror(errno));
769 /* Find the records we want, and add them to the result. */
772 yield = find_records(f, zone, domain, qtype, qtypelen, &pk, &count, &dnssec, &aa);
773 if (yield == NO_RECOVERY) goto END_OFF;
775 packet[6] = (count >> 8) & 255;
776 packet[7] = count & 255;
778 /* There is no need to return any additional records because Exim no longer
779 (from release 4.61) makes any use of them. */
785 ((HEADER *)packet)->ad = 1;
788 ((HEADER *)packet)->aa = 1;
790 /* Close the zone file, write the result, and return. */
794 (void)fwrite(packet, 1, pk - packet, stdout);
798 /* vi: aw ai sw=2 sts=2 ts=8 et
800 /* End of fakens.c */