Log lengthy DNS lookups. Bug 514
[exim.git] / test / src / fakens.c
CommitLineData
c55a77db
PH
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
4d4c2a9b
JH
51HOST_NOT_FOUND.
52
846430d9
JH
53Any DNS record line in a zone file can be prefixed with "DELAY=" and
54a number of milliseconds (followed by whitespace).
55
4d4c2a9b
JH
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. */
c55a77db
PH
59
60#include <ctype.h>
61#include <stdarg.h>
62#include <stdio.h>
e265af1f 63#include <stdlib.h>
c55a77db
PH
64#include <string.h>
65#include <netdb.h>
66#include <errno.h>
67#include <arpa/nameser.h>
68#include <sys/types.h>
846430d9 69#include <sys/time.h>
c55a77db
PH
70#include <dirent.h>
71
72#define FALSE 0
73#define TRUE 1
74#define PASS_ON 5
75
76typedef int BOOL;
77typedef 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
c55a77db
PH
91typedef struct zoneitem {
92 uschar *zone;
93 uschar *zonefile;
94} zoneitem;
95
96typedef struct tlist {
97 uschar *name;
98 int value;
99} tlist;
100
101/* On some (older?) operating systems, the standard ns_t_xxx definitions are
102not available, and only the older T_xxx ones exist in nameser.h. If ns_t_a is
103not defined, assume we are in this state. A really old system might not even
104know about AAAA and SRV at all. */
105
106#ifndef ns_t_a
b4161d10
JH
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
c55a77db
PH
126#endif
127
128static 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 },
b4161d10 138 { US"TLSA", ns_t_tlsa },
c55a77db
PH
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
150Arguments:
151 format a format string
152 ... arguments
153
154Returns: pointer to formatted string
155*/
156
157static uschar *
158fcopystring(uschar *format, ...)
159{
160uschar *yield;
161char buffer[256];
162va_list ap;
163va_start(ap, format);
e265af1f 164vsprintf(buffer, CS format, ap);
c55a77db
PH
165va_end(ap);
166yield = (uschar *)malloc(Ustrlen(buffer) + 1);
167Ustrcpy(yield, buffer);
168return 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
177present, it doesn't do any compression.
178
179Arguments:
180 name the name
181 pk where to put it
182
183Returns: the updated value of pk
184*/
185
186static uschar *
187packname(uschar *name, uschar *pk)
188{
189while (*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;
199return pk;
200}
201
36b894a6
JH
202uschar *
203bytefield(uschar ** pp, uschar * pk)
204{
205unsigned value = 0;
206uschar * p = *pp;
207
208while (isdigit(*p)) value = value*10 + *p++ - '0';
209while (isspace(*p)) p++;
210*pp = p;
211*pk++ = value & 255;
212return pk;
213}
214
b4161d10
JH
215uschar *
216shortfield(uschar ** pp, uschar * pk)
217{
218unsigned value = 0;
219uschar * p = *pp;
220
221while (isdigit(*p)) value = value*10 + *p++ - '0';
222while (isspace(*p)) p++;
223*pp = p;
224*pk++ = (value >> 8) & 255;
225*pk++ = value & 255;
226return pk;
227}
228
c55a77db
PH
229
230
846430d9
JH
231/*************************************************/
232
233static void
234milliwait(struct itimerval *itval)
235{
236sigset_t sigmask;
237sigset_t old_sigmask;
238
239if (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
251static void
252millisleep(int msec)
253{
254struct itimerval itval;
255itval.it_interval.tv_sec = 0;
256itval.it_interval.tv_usec = 0;
257itval.it_value.tv_sec = msec/1000;
258itval.it_value.tv_usec = (msec % 1000) * 1000;
259milliwait(&itval);
260}
261
262
c55a77db
PH
263/*************************************************
264* Scan file for RRs *
265*************************************************/
266
267/* This function scans an open "zone file" for appropriate records, and adds
268any that are found to the output buffer.
269
270Arguments:
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
c55a77db
PH
278
279Returns: 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
283static int
284find_records(FILE *f, uschar *zone, uschar *domain, uschar *qtype,
4d4c2a9b 285 int qtypelen, uschar **pkptr, int *countptr, BOOL * dnssec)
c55a77db
PH
286{
287int yield = HOST_NOT_FOUND;
c55a77db
PH
288int domainlen = Ustrlen(domain);
289BOOL pass_on_not_found = FALSE;
290tlist *typeptr;
291uschar *pk = *pkptr;
292uschar buffer[256];
293uschar rrdomain[256];
75e0e026 294uschar RRdomain[256];
c55a77db
PH
295
296/* Decode the required type */
297
298for (typeptr = type_list; typeptr->name != NULL; typeptr++)
299 { if (Ustrcmp(typeptr->name, qtype) == 0) break; }
300if (typeptr->name == NULL)
301 {
302 fprintf(stderr, "fakens: unknown record type %s\n", qtype);
303 return NO_RECOVERY;
304 }
305
306rrdomain[0] = 0; /* No previous domain */
307(void)fseek(f, 0, SEEK_SET); /* Start again at the beginning */
308
b4161d10 309*dnssec = TRUE; /* cancelled by first nonsecure rec found */
4d4c2a9b 310
c55a77db
PH
311/* Scan for RRs */
312
313while (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;
4d4c2a9b 321 BOOL rr_sec = FALSE;
846430d9 322 int delay = 0;
c55a77db
PH
323
324 p = buffer;
325 while (isspace(*p)) p++;
326 if (*p == 0 || *p == ';') continue;
327
4d4c2a9b 328 if (Ustrncmp(p, US"PASS ON NOT FOUND", 17) == 0)
c55a77db
PH
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;
846430d9
JH
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 }
4d4c2a9b 355
c55a77db
PH
356 if (!isspace(*p))
357 {
358 uschar *pp = rrdomain;
75e0e026
PH
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 }
c55a77db
PH
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
846430d9
JH
408 if (delay)
409 millisleep(delay);
410
4d4c2a9b
JH
411 if (!rr_sec)
412 *dnssec = FALSE; /* cancel AD return */
413
c55a77db
PH
414 yield = 0;
415 *countptr = *countptr + 1;
416
417 p += qtlen;
418 while (isspace(*p)) p++;
419
75e0e026
PH
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);
c55a77db
PH
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:
b4161d10 474 pk = shortfield(&p, pk);
42ec9880 475 if (ep[-1] != '.') sprintf(CS ep, "%s.", zone);
75e0e026
PH
476 pk = packname(p, pk);
477 plen = Ustrlen(p);
478 break;
c55a77db
PH
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
b4161d10 487 case ns_t_tlsa:
36b894a6
JH
488 pk = bytefield(&p, pk); /* usage */
489 pk = bytefield(&p, pk); /* selector */
490 pk = bytefield(&p, pk); /* match type */
b4161d10
JH
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
c55a77db
PH
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:
42ec9880 519 if (ep[-1] != '.') sprintf(CS ep, "%s.", zone);
c55a77db
PH
520 pk = packname(p, pk);
521 plen = Ustrlen(p);
c55a77db
PH
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;
c55a77db
PH
529 }
530
531*pkptr = pk;
532return (yield == HOST_NOT_FOUND && pass_on_not_found)? PASS_ON : yield;
533}
534
535
846430d9
JH
536static void
537alarmfn(int sig)
538{
539}
c55a77db
PH
540
541/*************************************************
542* Entry point and main program *
543*************************************************/
544
545int
546main(int argc, char **argv)
547{
548FILE *f;
549DIR *d;
75e0e026 550int domlen, qtypelen;
c55a77db
PH
551int yield, count;
552int i;
553int zonecount = 0;
c55a77db 554struct dirent *de;
c55a77db
PH
555zoneitem zones[32];
556uschar *qualify = NULL;
557uschar *p, *zone;
558uschar *zonefile = NULL;
559uschar domain[256];
560uschar buffer[256];
561uschar qtype[12];
562uschar packet[512];
563uschar *pk = packet;
4d4c2a9b 564BOOL dnssec;
c55a77db 565
846430d9
JH
566signal(SIGALRM, alarmfn);
567
c55a77db
PH
568if (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
42ec9880 576(void)sprintf(CS buffer, "%s/../dnszones", argv[1]);
c55a77db
PH
577
578d = opendir(CCS buffer);
579if (d == NULL)
580 {
581 fprintf(stderr, "fakens: failed to opendir %s: %s\n", buffer,
582 strerror(errno));
583 return NO_RECOVERY;
584 }
585
586while ((de = readdir(d)) != NULL)
587 {
e265af1f 588 uschar *name = US de->d_name;
c55a77db
PH
589 if (Ustrncmp(name, "qualify.", 8) == 0)
590 {
e265af1f 591 qualify = fcopystring(US "%s", name + 7);
c55a77db
PH
592 continue;
593 }
594 if (Ustrncmp(name, "db.", 3) != 0) continue;
595 if (Ustrncmp(name + 3, "ip4.", 4) == 0)
e265af1f 596 zones[zonecount].zone = fcopystring(US "%s.in-addr.arpa", name + 6);
c55a77db 597 else if (Ustrncmp(name + 3, "ip6.", 4) == 0)
e265af1f 598 zones[zonecount].zone = fcopystring(US "%s.ip6.arpa", name + 6);
c55a77db 599 else
e265af1f
JH
600 zones[zonecount].zone = fcopystring(US "%s", name + 2);
601 zones[zonecount++].zonefile = fcopystring(US "%s", name);
c55a77db
PH
602 }
603(void)closedir(d);
604
605/* Get the RR type and upper case it, and check that we recognize it. */
606
607Ustrncpy(qtype, argv[3], sizeof(qtype));
608qtypelen = Ustrlen(qtype);
609for (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,
612and set up the zone file name. The zone names in the table all start with a
613dot. */
614
615domlen = Ustrlen(argv[2]);
616if (argv[2][domlen-1] == '.') domlen--;
617Ustrncpy(domain, argv[2], domlen);
618domain[domlen] = 0;
619for (i = 0; i < domlen; i++) domain[i] = tolower(domain[i]);
620
621if (Ustrchr(domain, '.') == NULL && qualify != NULL &&
622 Ustrcmp(domain, "dontqualify") != 0)
623 {
624 Ustrcat(domain, qualify);
625 domlen += Ustrlen(qualify);
626 }
627
628for (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
641if (zonefile == NULL)
642 {
643 fprintf(stderr, "fakens: query not in faked zone: domain is: %s\n", domain);
644 return PASS_ON;
645 }
646
42ec9880 647(void)sprintf(CS buffer, "%s/../dnszones/%s", argv[1], zonefile);
c55a77db
PH
648
649/* Initialize the start of the response packet. We don't have to fake up
650everything, because we know that Exim will look only at the answer and
651additional section parts. */
652
653memset(packet, 0, 12);
654pk += 12;
655
656/* Open the zone file. */
657
42ec9880 658f = fopen(CS buffer, "r");
c55a77db
PH
659if (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
667count = 0;
4d4c2a9b 668yield = find_records(f, zone, domain, qtype, qtypelen, &pk, &count, &dnssec);
c55a77db
PH
669if (yield == NO_RECOVERY) goto END_OFF;
670
671packet[6] = (count >> 8) & 255;
672packet[7] = count & 255;
673
75e0e026
PH
674/* There is no need to return any additional records because Exim no longer
675(from release 4.61) makes any use of them. */
c55a77db 676
75e0e026
PH
677packet[10] = 0;
678packet[11] = 0;
c55a77db 679
4d4c2a9b
JH
680if (dnssec)
681 ((HEADER *)packet)->ad = 1;
682
c55a77db
PH
683/* Close the zone file, write the result, and return. */
684
685END_OFF:
686(void)fclose(f);
687(void)fwrite(packet, 1, pk - packet, stdout);
688return yield;
689}
690
4d4c2a9b
JH
691/* vi: aw ai sw=2
692*/
c55a77db 693/* End of fakens.c */