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