ChangeLog updates for the security issues.
[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 */