Dnssec observability: add variable $lookup_dnssec_authenticated
[users/heiko/exim.git] / test / src / fakens.c
1 /*************************************************
2 *       fakens - A Fake Nameserver Program       *
3 *************************************************/
4
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 that directory. Note that there is little checking in this program. The fake
12 zone files are assumed to be syntactically valid.
13
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".
20
21 The 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
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.
31
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:
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
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:
42
43   5 PASS_ON            requests Exim to call res_search()
44
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:
47
48   PASS ON NOT FOUND
49
50 and the domain is not found. It converts the the result to PASS_ON instead of
51 HOST_NOT_FOUND. */
52
53 #include <ctype.h>
54 #include <stdarg.h>
55 #include <stdio.h>
56 #include <string.h>
57 #include <netdb.h>
58 #include <errno.h>
59 #include <arpa/nameser.h>
60 #include <sys/types.h>
61 #include <dirent.h>
62
63 #define FALSE         0
64 #define TRUE          1
65 #define PASS_ON       5
66
67 typedef int BOOL;
68 typedef unsigned char uschar;
69
70 #define CS   (char *)
71 #define CCS  (const char *)
72 #define US   (unsigned char *)
73
74 #define Ustrcat(s,t)       strcat(CS(s),CCS(t))
75 #define Ustrchr(s,n)       US strchr(CCS(s),n)
76 #define Ustrcmp(s,t)       strcmp(CCS(s),CCS(t))
77 #define Ustrcpy(s,t)       strcpy(CS(s),CCS(t))
78 #define Ustrlen(s)         (int)strlen(CCS(s))
79 #define Ustrncmp(s,t,n)    strncmp(CCS(s),CCS(t),n)
80 #define Ustrncpy(s,t,n)    strncpy(CS(s),CCS(t),n)
81
82 typedef struct zoneitem {
83   uschar *zone;
84   uschar *zonefile;
85 } zoneitem;
86
87 typedef struct tlist {
88   uschar *name;
89   int value;
90 } tlist;
91
92 /* On some (older?) operating systems, the standard ns_t_xxx definitions are
93 not available, and only the older T_xxx ones exist in nameser.h. If ns_t_a is
94 not defined, assume we are in this state. A really old system might not even
95 know about AAAA and SRV at all. */
96
97 #ifndef ns_t_a
98 #define ns_t_a      T_A
99 #define ns_t_ns     T_NS
100 #define ns_t_cname  T_CNAME
101 #define ns_t_soa    T_SOA
102 #define ns_t_ptr    T_PTR
103 #define ns_t_mx     T_MX
104 #define ns_t_txt    T_TXT
105 #define ns_t_aaaa   T_AAAA
106 #define ns_t_srv    T_SRV
107 #ifndef T_AAAA
108 #define T_AAAA      28
109 #endif
110 #ifndef T_SRV
111 #define T_SRV       33
112 #endif
113 #endif
114
115 static tlist type_list[] = {
116   { US"A",       ns_t_a },
117   { US"NS",      ns_t_ns },
118   { US"CNAME",   ns_t_cname },
119 /*  { US"SOA",     ns_t_soa },  Not currently in use */
120   { US"PTR",     ns_t_ptr },
121   { US"MX",      ns_t_mx },
122   { US"TXT",     ns_t_txt },
123   { US"AAAA",    ns_t_aaaa },
124   { US"SRV",     ns_t_srv },
125   { NULL,        0 }
126 };
127
128
129
130 /*************************************************
131 *           Get memory and sprintf into it       *
132 *************************************************/
133
134 /* This is used when building a table of zones and their files.
135
136 Arguments:
137   format       a format string
138   ...          arguments
139
140 Returns:       pointer to formatted string
141 */
142
143 static uschar *
144 fcopystring(uschar *format, ...)
145 {
146 uschar *yield;
147 char buffer[256];
148 va_list ap;
149 va_start(ap, format);
150 vsprintf(buffer, format, ap);
151 va_end(ap);
152 yield = (uschar *)malloc(Ustrlen(buffer) + 1);
153 Ustrcpy(yield, buffer);
154 return yield;
155 }
156
157
158 /*************************************************
159 *             Pack name into memory              *
160 *************************************************/
161
162 /* This function packs a domain name into memory according to DNS rules. At
163 present, it doesn't do any compression.
164
165 Arguments:
166   name         the name
167   pk           where to put it
168
169 Returns:       the updated value of pk
170 */
171
172 static uschar *
173 packname(uschar *name, uschar *pk)
174 {
175 while (*name != 0)
176   {
177   uschar *p = name;
178   while (*p != 0 && *p != '.') p++;
179   *pk++ = (p - name);
180   memmove(pk, name, p - name);
181   pk += p - name;
182   name = (*p == 0)? p : p + 1;
183   }
184 *pk++ = 0;
185 return pk;
186 }
187
188
189
190 /*************************************************
191 *              Scan file for RRs                 *
192 *************************************************/
193
194 /* This function scans an open "zone file" for appropriate records, and adds
195 any that are found to the output buffer.
196
197 Arguments:
198   f           the input FILE
199   zone        the current zone name
200   domain      the domain we are looking for
201   qtype       the type of RR we want
202   qtypelen    the length of qtype
203   pkptr       points to the output buffer pointer; this is updated
204   countptr    points to the record count; this is updated
205
206 Returns:      0 on success, else HOST_NOT_FOUND or NO_DATA or NO_RECOVERY or
207               PASS_ON - the latter if a "PASS ON NOT FOUND" line is seen
208 */
209
210 static int
211 find_records(FILE *f, uschar *zone, uschar *domain, uschar *qtype,
212   int qtypelen, uschar **pkptr, int *countptr)
213 {
214 int yield = HOST_NOT_FOUND;
215 int domainlen = Ustrlen(domain);
216 BOOL pass_on_not_found = FALSE;
217 tlist *typeptr;
218 uschar *pk = *pkptr;
219 uschar buffer[256];
220 uschar rrdomain[256];
221 uschar RRdomain[256];
222
223 /* Decode the required type */
224
225 for (typeptr = type_list; typeptr->name != NULL; typeptr++)
226   { if (Ustrcmp(typeptr->name, qtype) == 0) break; }
227 if (typeptr->name == NULL)
228   {
229   fprintf(stderr, "fakens: unknown record type %s\n", qtype);
230   return NO_RECOVERY;
231   }
232
233 rrdomain[0] = 0;                 /* No previous domain */
234 (void)fseek(f, 0, SEEK_SET);     /* Start again at the beginning */
235
236 /* Scan for RRs */
237
238 while (fgets(CS buffer, sizeof(buffer), f) != NULL)
239   {
240   uschar *rdlptr;
241   uschar *p, *ep, *pp;
242   BOOL found_cname = FALSE;
243   int i, plen, value;
244   int tvalue = typeptr->value;
245   int qtlen = qtypelen;
246
247   p = buffer;
248   while (isspace(*p)) p++;
249   if (*p == 0 || *p == ';') continue;
250
251   if (Ustrncmp(p, "PASS ON NOT FOUND", 17) == 0)
252     {
253     pass_on_not_found = TRUE;
254     continue;
255     }
256
257   ep = buffer + Ustrlen(buffer);
258   while (isspace(ep[-1])) ep--;
259   *ep = 0;
260
261   p = buffer;
262   if (!isspace(*p))
263     {
264     uschar *pp = rrdomain;
265     uschar *PP = RRdomain;
266     while (!isspace(*p))
267       {
268       *pp++ = tolower(*p);
269       *PP++ = *p++;
270       }
271     if (pp[-1] != '.')
272       {
273       Ustrcpy(pp, zone);
274       Ustrcpy(PP, zone);
275       }
276     else
277       {
278       pp[-1] = 0;
279       PP[-1] = 0;
280       }
281     }
282
283   /* Compare domain names; first check for a wildcard */
284
285   if (rrdomain[0] == '*')
286     {
287     int restlen = Ustrlen(rrdomain) - 1;
288     if (domainlen > restlen &&
289         Ustrcmp(domain + domainlen - restlen, rrdomain + 1) != 0) continue;
290     }
291
292   /* Not a wildcard RR */
293
294   else if (Ustrcmp(domain, rrdomain) != 0) continue;
295
296   /* The domain matches */
297
298   if (yield == HOST_NOT_FOUND) yield = NO_DATA;
299
300   /* Compare RR types; a CNAME record is always returned */
301
302   while (isspace(*p)) p++;
303
304   if (Ustrncmp(p, "CNAME", 5) == 0)
305     {
306     tvalue = ns_t_cname;
307     qtlen = 5;
308     found_cname = TRUE;
309     }
310   else if (Ustrncmp(p, qtype, qtypelen) != 0 || !isspace(p[qtypelen])) continue;
311
312   /* Found a relevant record */
313
314   yield = 0;
315   *countptr = *countptr + 1;
316
317   p += qtlen;
318   while (isspace(*p)) p++;
319
320   /* For a wildcard record, use the search name; otherwise use the record's
321   name in its original case because it might contain upper case letters. */
322
323   pk = packname((rrdomain[0] == '*')? domain : RRdomain, pk);
324   *pk++ = (tvalue >> 8) & 255;
325   *pk++ = (tvalue) & 255;
326   *pk++ = 0;
327   *pk++ = 1;     /* class = IN */
328
329   pk += 4;       /* TTL field; don't care */
330
331   rdlptr = pk;   /* remember rdlength field */
332   pk += 2;
333
334   /* The rest of the data depends on the type */
335
336   switch (tvalue)
337     {
338     case ns_t_soa:  /* Not currently used */
339     break;
340
341     case ns_t_a:
342     for (i = 0; i < 4; i++)
343       {
344       value = 0;
345       while (isdigit(*p)) value = value*10 + *p++ - '0';
346       *pk++ = value;
347       p++;
348       }
349     break;
350
351     /* The only occurrence of a double colon is for ::1 */
352     case ns_t_aaaa:
353     if (Ustrcmp(p, "::1") == 0)
354       {
355       memset(pk, 0, 15);
356       pk += 15;
357       *pk++ = 1;
358       }
359     else for (i = 0; i < 8; i++)
360       {
361       value = 0;
362       while (isxdigit(*p))
363         {
364         value = value * 16 + toupper(*p) - (isdigit(*p)? '0' : '7');
365         p++;
366         }
367       *pk++ = (value >> 8) & 255;
368       *pk++ = value & 255;
369       p++;
370       }
371     break;
372
373     case ns_t_mx:
374     value = 0;
375     while (isdigit(*p)) value = value*10 + *p++ - '0';
376     while (isspace(*p)) p++;
377     *pk++ = (value >> 8) & 255;
378     *pk++ = value & 255;
379     if (ep[-1] != '.') sprintf(ep, "%s.", zone);
380     pk = packname(p, pk);
381     plen = Ustrlen(p);
382     break;
383
384     case ns_t_txt:
385     pp = pk++;
386     if (*p == '"') p++;   /* Should always be the case */
387     while (*p != 0 && *p != '"') *pk++ = *p++;
388     *pp = pk - pp - 1;
389     break;
390
391     case ns_t_srv:
392     for (i = 0; i < 3; i++)
393       {
394       value = 0;
395       while (isdigit(*p)) value = value*10 + *p++ - '0';
396       while (isspace(*p)) p++;
397       *pk++ = (value >> 8) & 255;
398       *pk++ = value & 255;
399       }
400
401     /* Fall through */
402
403     case ns_t_cname:
404     case ns_t_ns:
405     case ns_t_ptr:
406     if (ep[-1] != '.') sprintf(ep, "%s.", zone);
407     pk = packname(p, pk);
408     plen = Ustrlen(p);
409     break;
410     }
411
412   /* Fill in the length, and we are done with this RR */
413
414   rdlptr[0] = ((pk - rdlptr - 2) >> 8) & 255;
415   rdlptr[1] = (pk -rdlptr - 2) & 255;
416   }
417
418 *pkptr = pk;
419 return (yield == HOST_NOT_FOUND && pass_on_not_found)? PASS_ON : yield;
420 }
421
422
423
424 /*************************************************
425 *           Entry point and main program         *
426 *************************************************/
427
428 int
429 main(int argc, char **argv)
430 {
431 FILE *f;
432 DIR *d;
433 int domlen, qtypelen;
434 int yield, count;
435 int i;
436 int zonecount = 0;
437 struct dirent *de;
438 zoneitem zones[32];
439 uschar *qualify = NULL;
440 uschar *p, *zone;
441 uschar *zonefile = NULL;
442 uschar domain[256];
443 uschar buffer[256];
444 uschar qtype[12];
445 uschar packet[512];
446 uschar *pk = packet;
447
448 if (argc != 4)
449   {
450   fprintf(stderr, "fakens: expected 3 arguments, received %d\n", argc-1);
451   return NO_RECOVERY;
452   }
453
454 /* Find the zones */
455
456 (void)sprintf(buffer, "%s/../dnszones", argv[1]);
457
458 d = opendir(CCS buffer);
459 if (d == NULL)
460   {
461   fprintf(stderr, "fakens: failed to opendir %s: %s\n", buffer,
462     strerror(errno));
463   return NO_RECOVERY;
464   }
465
466 while ((de = readdir(d)) != NULL)
467   {
468   uschar *name = de->d_name;
469   if (Ustrncmp(name, "qualify.", 8) == 0)
470     {
471     qualify = fcopystring("%s", name + 7);
472     continue;
473     }
474   if (Ustrncmp(name, "db.", 3) != 0) continue;
475   if (Ustrncmp(name + 3, "ip4.", 4) == 0)
476     zones[zonecount].zone = fcopystring("%s.in-addr.arpa", name + 6);
477   else if (Ustrncmp(name + 3, "ip6.", 4) == 0)
478     zones[zonecount].zone = fcopystring("%s.ip6.arpa", name + 6);
479   else
480     zones[zonecount].zone = fcopystring("%s", name + 2);
481   zones[zonecount++].zonefile = fcopystring("%s", name);
482   }
483 (void)closedir(d);
484
485 /* Get the RR type and upper case it, and check that we recognize it. */
486
487 Ustrncpy(qtype, argv[3], sizeof(qtype));
488 qtypelen = Ustrlen(qtype);
489 for (p = qtype; *p != 0; p++) *p = toupper(*p);
490
491 /* Find the domain, lower case it, check that it is in a zone that we handle,
492 and set up the zone file name. The zone names in the table all start with a
493 dot. */
494
495 domlen = Ustrlen(argv[2]);
496 if (argv[2][domlen-1] == '.') domlen--;
497 Ustrncpy(domain, argv[2], domlen);
498 domain[domlen] = 0;
499 for (i = 0; i < domlen; i++) domain[i] = tolower(domain[i]);
500
501 if (Ustrchr(domain, '.') == NULL && qualify != NULL &&
502     Ustrcmp(domain, "dontqualify") != 0)
503   {
504   Ustrcat(domain, qualify);
505   domlen += Ustrlen(qualify);
506   }
507
508 for (i = 0; i < zonecount; i++)
509   {
510   int zlen;
511   zone = zones[i].zone;
512   zlen = Ustrlen(zone);
513   if (Ustrcmp(domain, zone+1) == 0 || (domlen >= zlen &&
514       Ustrcmp(domain + domlen - zlen, zone) == 0))
515     {
516     zonefile = zones[i].zonefile;
517     break;
518     }
519   }
520
521 if (zonefile == NULL)
522   {
523   fprintf(stderr, "fakens: query not in faked zone: domain is: %s\n", domain);
524   return PASS_ON;
525   }
526
527 (void)sprintf(buffer, "%s/../dnszones/%s", argv[1], zonefile);
528
529 /* Initialize the start of the response packet. We don't have to fake up
530 everything, because we know that Exim will look only at the answer and
531 additional section parts. */
532
533 memset(packet, 0, 12);
534 pk += 12;
535
536 /* Open the zone file. */
537
538 f = fopen(buffer, "r");
539 if (f == NULL)
540   {
541   fprintf(stderr, "fakens: failed to open %s: %s\n", buffer, strerror(errno));
542   return NO_RECOVERY;
543   }
544
545 /* Find the records we want, and add them to the result. */
546
547 count = 0;
548 yield = find_records(f, zone, domain, qtype, qtypelen, &pk, &count);
549 if (yield == NO_RECOVERY) goto END_OFF;
550
551 packet[6] = (count >> 8) & 255;
552 packet[7] = count & 255;
553
554 /* There is no need to return any additional records because Exim no longer
555 (from release 4.61) makes any use of them. */
556
557 packet[10] = 0;
558 packet[11] = 0;
559
560 /* Close the zone file, write the result, and return. */
561
562 END_OFF:
563 (void)fclose(f);
564 (void)fwrite(packet, 1, pk - packet, stdout);
565 return yield;
566 }
567
568 /* End of fakens.c */