Add retrans/retry options to dnsdb lookup. Bug 1539
[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>
9d787f10 67#include <signal.h>
c55a77db
PH
68#include <arpa/nameser.h>
69#include <sys/types.h>
846430d9 70#include <sys/time.h>
c55a77db
PH
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
c55a77db
PH
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
b4161d10
JH
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
c55a77db
PH
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 },
b4161d10 139 { US"TLSA", ns_t_tlsa },
c55a77db
PH
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);
e265af1f 165vsprintf(buffer, CS format, ap);
c55a77db
PH
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
b4161d10 203uschar *
36b894a6
JH
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 *
b4161d10
JH
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
c55a77db
PH
230
231
846430d9
JH
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
c55a77db
PH
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
c55a77db
PH
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,
4d4c2a9b 286 int qtypelen, uschar **pkptr, int *countptr, BOOL * dnssec)
c55a77db
PH
287{
288int yield = HOST_NOT_FOUND;
c55a77db
PH
289int domainlen = Ustrlen(domain);
290BOOL pass_on_not_found = FALSE;
291tlist *typeptr;
292uschar *pk = *pkptr;
293uschar buffer[256];
294uschar rrdomain[256];
75e0e026 295uschar RRdomain[256];
c55a77db
PH
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
b4161d10 310*dnssec = TRUE; /* cancelled by first nonsecure rec found */
4d4c2a9b 311
c55a77db
PH
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;
4d4c2a9b 322 BOOL rr_sec = FALSE;
846430d9 323 int delay = 0;
c55a77db
PH
324
325 p = buffer;
326 while (isspace(*p)) p++;
327 if (*p == 0 || *p == ';') continue;
328
4d4c2a9b 329 if (Ustrncmp(p, US"PASS ON NOT FOUND", 17) == 0)
c55a77db
PH
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;
846430d9
JH
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 }
4d4c2a9b 356
c55a77db
PH
357 if (!isspace(*p))
358 {
359 uschar *pp = rrdomain;
75e0e026
PH
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 }
c55a77db
PH
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
846430d9
JH
409 if (delay)
410 millisleep(delay);
411
4d4c2a9b
JH
412 if (!rr_sec)
413 *dnssec = FALSE; /* cancel AD return */
414
c55a77db
PH
415 yield = 0;
416 *countptr = *countptr + 1;
417
418 p += qtlen;
419 while (isspace(*p)) p++;
420
75e0e026
PH
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);
c55a77db
PH
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:
b4161d10 475 pk = shortfield(&p, pk);
42ec9880 476 if (ep[-1] != '.') sprintf(CS ep, "%s.", zone);
75e0e026
PH
477 pk = packname(p, pk);
478 plen = Ustrlen(p);
479 break;
c55a77db
PH
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
b4161d10 488 case ns_t_tlsa:
36b894a6
JH
489 pk = bytefield(&p, pk); /* usage */
490 pk = bytefield(&p, pk); /* selector */
491 pk = bytefield(&p, pk); /* match type */
b4161d10
JH
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
c55a77db
PH
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:
42ec9880 520 if (ep[-1] != '.') sprintf(CS ep, "%s.", zone);
c55a77db
PH
521 pk = packname(p, pk);
522 plen = Ustrlen(p);
c55a77db
PH
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;
c55a77db
PH
530 }
531
532*pkptr = pk;
533return (yield == HOST_NOT_FOUND && pass_on_not_found)? PASS_ON : yield;
534}
535
536
846430d9
JH
537static void
538alarmfn(int sig)
539{
540}
c55a77db
PH
541
542/*************************************************
543* Entry point and main program *
544*************************************************/
545
546int
547main(int argc, char **argv)
548{
549FILE *f;
550DIR *d;
75e0e026 551int domlen, qtypelen;
c55a77db
PH
552int yield, count;
553int i;
554int zonecount = 0;
c55a77db 555struct dirent *de;
c55a77db
PH
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;
4d4c2a9b 565BOOL dnssec;
c55a77db 566
846430d9
JH
567signal(SIGALRM, alarmfn);
568
c55a77db
PH
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
42ec9880 577(void)sprintf(CS buffer, "%s/../dnszones", argv[1]);
c55a77db
PH
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 {
e265af1f 589 uschar *name = US de->d_name;
c55a77db
PH
590 if (Ustrncmp(name, "qualify.", 8) == 0)
591 {
e265af1f 592 qualify = fcopystring(US "%s", name + 7);
c55a77db
PH
593 continue;
594 }
595 if (Ustrncmp(name, "db.", 3) != 0) continue;
596 if (Ustrncmp(name + 3, "ip4.", 4) == 0)
e265af1f 597 zones[zonecount].zone = fcopystring(US "%s.in-addr.arpa", name + 6);
c55a77db 598 else if (Ustrncmp(name + 3, "ip6.", 4) == 0)
e265af1f 599 zones[zonecount].zone = fcopystring(US "%s.ip6.arpa", name + 6);
c55a77db 600 else
e265af1f
JH
601 zones[zonecount].zone = fcopystring(US "%s", name + 2);
602 zones[zonecount++].zonefile = fcopystring(US "%s", name);
c55a77db
PH
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
42ec9880 648(void)sprintf(CS buffer, "%s/../dnszones/%s", argv[1], zonefile);
c55a77db
PH
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
42ec9880 659f = fopen(CS buffer, "r");
c55a77db
PH
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;
4d4c2a9b 669yield = find_records(f, zone, domain, qtype, qtypelen, &pk, &count, &dnssec);
c55a77db
PH
670if (yield == NO_RECOVERY) goto END_OFF;
671
672packet[6] = (count >> 8) & 255;
673packet[7] = count & 255;
674
75e0e026
PH
675/* There is no need to return any additional records because Exim no longer
676(from release 4.61) makes any use of them. */
c55a77db 677
75e0e026
PH
678packet[10] = 0;
679packet[11] = 0;
c55a77db 680
4d4c2a9b
JH
681if (dnssec)
682 ((HEADER *)packet)->ad = 1;
683
c55a77db
PH
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
4d4c2a9b
JH
692/* vi: aw ai sw=2
693*/
c55a77db 694/* End of fakens.c */