With a few minor exceptions for tests that had additional concerns and tests I don...
[exim.git] / test / src / fakens.c
CommitLineData
a2042e78 1/* $Cambridge: exim/test/src/fakens.c,v 1.3 2006/11/07 14:13:19 ph10 Exp $ */
c55a77db
PH
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
8avoids the need to install special zones in a real nameserver. When Exim is
9running in its (new) test harness, DNS lookups are first passed to this program
10instead of to the real resolver. (With a few exceptions - see the discussion in
11the test suite's README file.) The program is also passed the name of the Exim
12spool directory; it expects to find its "zone files" in ../dnszones relative to
13that directory. Note that there is little checking in this program. The fake
14zone files are assumed to be syntactically valid.
15
16The zones that are handled are found by scanning the dnszones directory. A file
17whose name is of the form db.ip4.x is a zone file for .x.in-addr.arpa; a file
18whose name is of the form db.ip6.x is a zone file for .x.ip6.arpa; a file of
19the form db.anything.else is a zone file for .anything.else. A file of the form
20qualify.x.y specifies the domain that is used to qualify single-component
21names, except for the name "dontqualify".
22
23The 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
29The output from the program is written to stdout. It is supposed to be in
30exactly the same format as a traditional namserver response (see RFC 1035) so
31that Exim can process it as normal. At present, no compression is used.
32Error messages are written to stderr.
33
34The return codes from the program are zero for success, and otherwise the
35values 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
42In a real nameserver, TRY_AGAIN is also used for a non-authoritative not found,
43but 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
47This is used for zones that fakens does not recognize. It is also used if a
48line in the zone file contains exactly this:
49
50 PASS ON NOT FOUND
51
52and the domain is not found. It converts the the result to PASS_ON instead of
53HOST_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
69typedef int BOOL;
70typedef 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
c55a77db
PH
84typedef struct zoneitem {
85 uschar *zone;
86 uschar *zonefile;
87} zoneitem;
88
89typedef struct tlist {
90 uschar *name;
91 int value;
92} tlist;
93
94/* On some (older?) operating systems, the standard ns_t_xxx definitions are
95not available, and only the older T_xxx ones exist in nameser.h. If ns_t_a is
96not defined, assume we are in this state. A really old system might not even
97know 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
117static 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
138Arguments:
139 format a format string
140 ... arguments
141
142Returns: pointer to formatted string
143*/
144
145static uschar *
146fcopystring(uschar *format, ...)
147{
148uschar *yield;
149char buffer[256];
150va_list ap;
151va_start(ap, format);
152vsprintf(buffer, format, ap);
153va_end(ap);
154yield = (uschar *)malloc(Ustrlen(buffer) + 1);
155Ustrcpy(yield, buffer);
156return 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
165present, it doesn't do any compression.
166
167Arguments:
168 name the name
169 pk where to put it
170
171Returns: the updated value of pk
172*/
173
174static uschar *
175packname(uschar *name, uschar *pk)
176{
177while (*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;
187return 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
197any that are found to the output buffer.
198
199Arguments:
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
c55a77db
PH
207
208Returns: 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
212static int
213find_records(FILE *f, uschar *zone, uschar *domain, uschar *qtype,
75e0e026 214 int qtypelen, uschar **pkptr, int *countptr)
c55a77db
PH
215{
216int yield = HOST_NOT_FOUND;
c55a77db
PH
217int domainlen = Ustrlen(domain);
218BOOL pass_on_not_found = FALSE;
219tlist *typeptr;
220uschar *pk = *pkptr;
221uschar buffer[256];
222uschar rrdomain[256];
75e0e026 223uschar RRdomain[256];
c55a77db
PH
224
225/* Decode the required type */
226
227for (typeptr = type_list; typeptr->name != NULL; typeptr++)
228 { if (Ustrcmp(typeptr->name, qtype) == 0) break; }
229if (typeptr->name == NULL)
230 {
231 fprintf(stderr, "fakens: unknown record type %s\n", qtype);
232 return NO_RECOVERY;
233 }
234
235rrdomain[0] = 0; /* No previous domain */
236(void)fseek(f, 0, SEEK_SET); /* Start again at the beginning */
237
238/* Scan for RRs */
239
240while (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;
75e0e026
PH
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 }
c55a77db
PH
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
75e0e026
PH
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);
c55a77db
PH
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;
75e0e026
PH
381 if (ep[-1] != '.') sprintf(ep, "%s.", zone);
382 pk = packname(p, pk);
383 plen = Ustrlen(p);
384 break;
c55a77db
PH
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:
c55a77db
PH
408 if (ep[-1] != '.') sprintf(ep, "%s.", zone);
409 pk = packname(p, pk);
410 plen = Ustrlen(p);
c55a77db
PH
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;
c55a77db
PH
418 }
419
420*pkptr = pk;
421return (yield == HOST_NOT_FOUND && pass_on_not_found)? PASS_ON : yield;
422}
423
424
425
426/*************************************************
427* Entry point and main program *
428*************************************************/
429
430int
431main(int argc, char **argv)
432{
433FILE *f;
434DIR *d;
75e0e026 435int domlen, qtypelen;
c55a77db
PH
436int yield, count;
437int i;
438int zonecount = 0;
c55a77db 439struct dirent *de;
c55a77db
PH
440zoneitem zones[32];
441uschar *qualify = NULL;
442uschar *p, *zone;
443uschar *zonefile = NULL;
444uschar domain[256];
445uschar buffer[256];
446uschar qtype[12];
447uschar packet[512];
448uschar *pk = packet;
449
450if (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
460d = opendir(CCS buffer);
461if (d == NULL)
462 {
463 fprintf(stderr, "fakens: failed to opendir %s: %s\n", buffer,
464 strerror(errno));
465 return NO_RECOVERY;
466 }
467
468while ((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
489Ustrncpy(qtype, argv[3], sizeof(qtype));
490qtypelen = Ustrlen(qtype);
491for (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,
494and set up the zone file name. The zone names in the table all start with a
495dot. */
496
497domlen = Ustrlen(argv[2]);
498if (argv[2][domlen-1] == '.') domlen--;
499Ustrncpy(domain, argv[2], domlen);
500domain[domlen] = 0;
501for (i = 0; i < domlen; i++) domain[i] = tolower(domain[i]);
502
503if (Ustrchr(domain, '.') == NULL && qualify != NULL &&
504 Ustrcmp(domain, "dontqualify") != 0)
505 {
506 Ustrcat(domain, qualify);
507 domlen += Ustrlen(qualify);
508 }
509
510for (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
523if (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
532everything, because we know that Exim will look only at the answer and
533additional section parts. */
534
535memset(packet, 0, 12);
536pk += 12;
537
538/* Open the zone file. */
539
540f = fopen(buffer, "r");
541if (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
549count = 0;
75e0e026 550yield = find_records(f, zone, domain, qtype, qtypelen, &pk, &count);
c55a77db
PH
551if (yield == NO_RECOVERY) goto END_OFF;
552
553packet[6] = (count >> 8) & 255;
554packet[7] = count & 255;
555
75e0e026
PH
556/* There is no need to return any additional records because Exim no longer
557(from release 4.61) makes any use of them. */
c55a77db 558
75e0e026
PH
559packet[10] = 0;
560packet[11] = 0;
c55a77db
PH
561
562/* Close the zone file, write the result, and return. */
563
564END_OFF:
565(void)fclose(f);
566(void)fwrite(packet, 1, pk - packet, stdout);
567return yield;
568}
569
570/* End of fakens.c */