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