Add retrans/retry options to dnsdb lookup. Bug 1539
[exim.git] / test / src / fakens.c
... / ...
CommitLineData
1/*************************************************
2* fakens - A Fake Nameserver Program *
3*************************************************/
4
5/* This program exists to support the testing of DNS handling code in Exim. It
6avoids the need to install special zones in a real nameserver. When Exim is
7running in its (new) test harness, DNS lookups are first passed to this program
8instead of to the real resolver. (With a few exceptions - see the discussion in
9the test suite's README file.) The program is also passed the name of the Exim
10spool directory; it expects to find its "zone files" in ../dnszones relative to
11that directory. Note that there is little checking in this program. The fake
12zone files are assumed to be syntactically valid.
13
14The zones that are handled are found by scanning the dnszones directory. A file
15whose name is of the form db.ip4.x is a zone file for .x.in-addr.arpa; a file
16whose name is of the form db.ip6.x is a zone file for .x.ip6.arpa; a file of
17the form db.anything.else is a zone file for .anything.else. A file of the form
18qualify.x.y specifies the domain that is used to qualify single-component
19names, except for the name "dontqualify".
20
21The 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
27The output from the program is written to stdout. It is supposed to be in
28exactly the same format as a traditional namserver response (see RFC 1035) so
29that Exim can process it as normal. At present, no compression is used.
30Error messages are written to stderr.
31
32The return codes from the program are zero for success, and otherwise the
33values 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
40In a real nameserver, TRY_AGAIN is also used for a non-authoritative not found,
41but 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
45This is used for zones that fakens does not recognize. It is also used if a
46line in the zone file contains exactly this:
47
48 PASS ON NOT FOUND
49
50and the domain is not found. It converts the the result to PASS_ON instead of
51HOST_NOT_FOUND.
52
53Any DNS record line in a zone file can be prefixed with "DELAY=" and
54a number of milliseconds (followed by whitespace).
55
56Any DNS record line in a zone file can be prefixed with "DNSSEC" and
57at least one space; if all the records found by a lookup are marked
58as 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
77typedef int BOOL;
78typedef 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
92typedef struct zoneitem {
93 uschar *zone;
94 uschar *zonefile;
95} zoneitem;
96
97typedef struct tlist {
98 uschar *name;
99 int value;
100} tlist;
101
102/* On some (older?) operating systems, the standard ns_t_xxx definitions are
103not available, and only the older T_xxx ones exist in nameser.h. If ns_t_a is
104not defined, assume we are in this state. A really old system might not even
105know 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
129static 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
151Arguments:
152 format a format string
153 ... arguments
154
155Returns: pointer to formatted string
156*/
157
158static uschar *
159fcopystring(uschar *format, ...)
160{
161uschar *yield;
162char buffer[256];
163va_list ap;
164va_start(ap, format);
165vsprintf(buffer, CS format, ap);
166va_end(ap);
167yield = (uschar *)malloc(Ustrlen(buffer) + 1);
168Ustrcpy(yield, buffer);
169return 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
178present, it doesn't do any compression.
179
180Arguments:
181 name the name
182 pk where to put it
183
184Returns: the updated value of pk
185*/
186
187static uschar *
188packname(uschar *name, uschar *pk)
189{
190while (*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;
200return pk;
201}
202
203uschar *
204bytefield(uschar ** pp, uschar * pk)
205{
206unsigned value = 0;
207uschar * p = *pp;
208
209while (isdigit(*p)) value = value*10 + *p++ - '0';
210while (isspace(*p)) p++;
211*pp = p;
212*pk++ = value & 255;
213return pk;
214}
215
216uschar *
217shortfield(uschar ** pp, uschar * pk)
218{
219unsigned value = 0;
220uschar * p = *pp;
221
222while (isdigit(*p)) value = value*10 + *p++ - '0';
223while (isspace(*p)) p++;
224*pp = p;
225*pk++ = (value >> 8) & 255;
226*pk++ = value & 255;
227return pk;
228}
229
230
231
232/*************************************************/
233
234static void
235milliwait(struct itimerval *itval)
236{
237sigset_t sigmask;
238sigset_t old_sigmask;
239
240if (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
252static void
253millisleep(int msec)
254{
255struct itimerval itval;
256itval.it_interval.tv_sec = 0;
257itval.it_interval.tv_usec = 0;
258itval.it_value.tv_sec = msec/1000;
259itval.it_value.tv_usec = (msec % 1000) * 1000;
260milliwait(&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
269any that are found to the output buffer.
270
271Arguments:
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
280Returns: 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
284static int
285find_records(FILE *f, uschar *zone, uschar *domain, uschar *qtype,
286 int qtypelen, uschar **pkptr, int *countptr, BOOL * dnssec)
287{
288int yield = HOST_NOT_FOUND;
289int domainlen = Ustrlen(domain);
290BOOL pass_on_not_found = FALSE;
291tlist *typeptr;
292uschar *pk = *pkptr;
293uschar buffer[256];
294uschar rrdomain[256];
295uschar RRdomain[256];
296
297/* Decode the required type */
298
299for (typeptr = type_list; typeptr->name != NULL; typeptr++)
300 { if (Ustrcmp(typeptr->name, qtype) == 0) break; }
301if (typeptr->name == NULL)
302 {
303 fprintf(stderr, "fakens: unknown record type %s\n", qtype);
304 return NO_RECOVERY;
305 }
306
307rrdomain[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
314while (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;
533return (yield == HOST_NOT_FOUND && pass_on_not_found)? PASS_ON : yield;
534}
535
536
537static void
538alarmfn(int sig)
539{
540}
541
542/*************************************************
543* Entry point and main program *
544*************************************************/
545
546int
547main(int argc, char **argv)
548{
549FILE *f;
550DIR *d;
551int domlen, qtypelen;
552int yield, count;
553int i;
554int zonecount = 0;
555struct dirent *de;
556zoneitem zones[32];
557uschar *qualify = NULL;
558uschar *p, *zone;
559uschar *zonefile = NULL;
560uschar domain[256];
561uschar buffer[256];
562uschar qtype[12];
563uschar packet[512];
564uschar *pk = packet;
565BOOL dnssec;
566
567signal(SIGALRM, alarmfn);
568
569if (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
579d = opendir(CCS buffer);
580if (d == NULL)
581 {
582 fprintf(stderr, "fakens: failed to opendir %s: %s\n", buffer,
583 strerror(errno));
584 return NO_RECOVERY;
585 }
586
587while ((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
608Ustrncpy(qtype, argv[3], sizeof(qtype));
609qtypelen = Ustrlen(qtype);
610for (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,
613and set up the zone file name. The zone names in the table all start with a
614dot. */
615
616domlen = Ustrlen(argv[2]);
617if (argv[2][domlen-1] == '.') domlen--;
618Ustrncpy(domain, argv[2], domlen);
619domain[domlen] = 0;
620for (i = 0; i < domlen; i++) domain[i] = tolower(domain[i]);
621
622if (Ustrchr(domain, '.') == NULL && qualify != NULL &&
623 Ustrcmp(domain, "dontqualify") != 0)
624 {
625 Ustrcat(domain, qualify);
626 domlen += Ustrlen(qualify);
627 }
628
629for (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
642if (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
651everything, because we know that Exim will look only at the answer and
652additional section parts. */
653
654memset(packet, 0, 12);
655pk += 12;
656
657/* Open the zone file. */
658
659f = fopen(CS buffer, "r");
660if (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
668count = 0;
669yield = find_records(f, zone, domain, qtype, qtypelen, &pk, &count, &dnssec);
670if (yield == NO_RECOVERY) goto END_OFF;
671
672packet[6] = (count >> 8) & 255;
673packet[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
678packet[10] = 0;
679packet[11] = 0;
680
681if (dnssec)
682 ((HEADER *)packet)->ad = 1;
683
684/* Close the zone file, write the result, and return. */
685
686END_OFF:
687(void)fclose(f);
688(void)fwrite(packet, 1, pk - packet, stdout);
689return yield;
690}
691
692/* vi: aw ai sw=2
693*/
694/* End of fakens.c */