Merge branch 'dbmjz'
[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 */