Add new errors mail_4xx, data_4xx, lost_connection, tls_required.
[exim.git] / test / src / fakens.c
1 /* $Cambridge: exim/test/src/fakens.c,v 1.2 2006/02/16 10:05:34 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 /* If we have just yielded a CNAME, we must change the domain name to the
420 new domain, and re-start the scan from the beginning. */
421
422 if (found_cname)
423 {
424 domain = fcopystring("%s", p);
425 domainlen = Ustrlen(domain);
426 domain[domainlen - 1] = 0; /* Removed trailing dot */
427 rrdomain[0] = 0; /* No previous domain */
428 (void)fseek(f, 0, SEEK_SET); /* Start again at the beginning */
429 }
430 }
431
432 *pkptr = pk;
433 return (yield == HOST_NOT_FOUND && pass_on_not_found)? PASS_ON : yield;
434 }
435
436
437
438 /*************************************************
439 * Entry point and main program *
440 *************************************************/
441
442 int
443 main(int argc, char **argv)
444 {
445 FILE *f;
446 DIR *d;
447 int domlen, qtypelen;
448 int yield, count;
449 int i;
450 int zonecount = 0;
451 struct dirent *de;
452 zoneitem zones[32];
453 uschar *qualify = NULL;
454 uschar *p, *zone;
455 uschar *zonefile = NULL;
456 uschar domain[256];
457 uschar buffer[256];
458 uschar qtype[12];
459 uschar packet[512];
460 uschar *pk = packet;
461
462 if (argc != 4)
463 {
464 fprintf(stderr, "fakens: expected 3 arguments, received %d\n", argc-1);
465 return NO_RECOVERY;
466 }
467
468 /* Find the zones */
469
470 (void)sprintf(buffer, "%s/../dnszones", argv[1]);
471
472 d = opendir(CCS buffer);
473 if (d == NULL)
474 {
475 fprintf(stderr, "fakens: failed to opendir %s: %s\n", buffer,
476 strerror(errno));
477 return NO_RECOVERY;
478 }
479
480 while ((de = readdir(d)) != NULL)
481 {
482 uschar *name = de->d_name;
483 if (Ustrncmp(name, "qualify.", 8) == 0)
484 {
485 qualify = fcopystring("%s", name + 7);
486 continue;
487 }
488 if (Ustrncmp(name, "db.", 3) != 0) continue;
489 if (Ustrncmp(name + 3, "ip4.", 4) == 0)
490 zones[zonecount].zone = fcopystring("%s.in-addr.arpa", name + 6);
491 else if (Ustrncmp(name + 3, "ip6.", 4) == 0)
492 zones[zonecount].zone = fcopystring("%s.ip6.arpa", name + 6);
493 else
494 zones[zonecount].zone = fcopystring("%s", name + 2);
495 zones[zonecount++].zonefile = fcopystring("%s", name);
496 }
497 (void)closedir(d);
498
499 /* Get the RR type and upper case it, and check that we recognize it. */
500
501 Ustrncpy(qtype, argv[3], sizeof(qtype));
502 qtypelen = Ustrlen(qtype);
503 for (p = qtype; *p != 0; p++) *p = toupper(*p);
504
505 /* Find the domain, lower case it, check that it is in a zone that we handle,
506 and set up the zone file name. The zone names in the table all start with a
507 dot. */
508
509 domlen = Ustrlen(argv[2]);
510 if (argv[2][domlen-1] == '.') domlen--;
511 Ustrncpy(domain, argv[2], domlen);
512 domain[domlen] = 0;
513 for (i = 0; i < domlen; i++) domain[i] = tolower(domain[i]);
514
515 if (Ustrchr(domain, '.') == NULL && qualify != NULL &&
516 Ustrcmp(domain, "dontqualify") != 0)
517 {
518 Ustrcat(domain, qualify);
519 domlen += Ustrlen(qualify);
520 }
521
522 for (i = 0; i < zonecount; i++)
523 {
524 int zlen;
525 zone = zones[i].zone;
526 zlen = Ustrlen(zone);
527 if (Ustrcmp(domain, zone+1) == 0 || (domlen >= zlen &&
528 Ustrcmp(domain + domlen - zlen, zone) == 0))
529 {
530 zonefile = zones[i].zonefile;
531 break;
532 }
533 }
534
535 if (zonefile == NULL)
536 {
537 fprintf(stderr, "fakens: query not in faked zone: domain is: %s\n", domain);
538 return PASS_ON;
539 }
540
541 (void)sprintf(buffer, "%s/../dnszones/%s", argv[1], zonefile);
542
543 /* Initialize the start of the response packet. We don't have to fake up
544 everything, because we know that Exim will look only at the answer and
545 additional section parts. */
546
547 memset(packet, 0, 12);
548 pk += 12;
549
550 /* Open the zone file. */
551
552 f = fopen(buffer, "r");
553 if (f == NULL)
554 {
555 fprintf(stderr, "fakens: failed to open %s: %s\n", buffer, strerror(errno));
556 return NO_RECOVERY;
557 }
558
559 /* Find the records we want, and add them to the result. */
560
561 count = 0;
562 yield = find_records(f, zone, domain, qtype, qtypelen, &pk, &count);
563 if (yield == NO_RECOVERY) goto END_OFF;
564
565 packet[6] = (count >> 8) & 255;
566 packet[7] = count & 255;
567
568 /* There is no need to return any additional records because Exim no longer
569 (from release 4.61) makes any use of them. */
570
571 packet[10] = 0;
572 packet[11] = 0;
573
574 /* Close the zone file, write the result, and return. */
575
576 END_OFF:
577 (void)fclose(f);
578 (void)fwrite(packet, 1, pk - packet, stdout);
579 return yield;
580 }
581
582 /* End of fakens.c */