Merge branch 'dbmjz'
[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
51HOST_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
67typedef int BOOL;
68typedef 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
c55a77db
PH
82typedef struct zoneitem {
83 uschar *zone;
84 uschar *zonefile;
85} zoneitem;
86
87typedef struct tlist {
88 uschar *name;
89 int value;
90} tlist;
91
92/* On some (older?) operating systems, the standard ns_t_xxx definitions are
93not available, and only the older T_xxx ones exist in nameser.h. If ns_t_a is
94not defined, assume we are in this state. A really old system might not even
95know 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
115static 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
136Arguments:
137 format a format string
138 ... arguments
139
140Returns: pointer to formatted string
141*/
142
143static uschar *
144fcopystring(uschar *format, ...)
145{
146uschar *yield;
147char buffer[256];
148va_list ap;
149va_start(ap, format);
150vsprintf(buffer, format, ap);
151va_end(ap);
152yield = (uschar *)malloc(Ustrlen(buffer) + 1);
153Ustrcpy(yield, buffer);
154return 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
163present, it doesn't do any compression.
164
165Arguments:
166 name the name
167 pk where to put it
168
169Returns: the updated value of pk
170*/
171
172static uschar *
173packname(uschar *name, uschar *pk)
174{
175while (*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;
185return 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
195any that are found to the output buffer.
196
197Arguments:
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
c55a77db
PH
205
206Returns: 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
210static int
211find_records(FILE *f, uschar *zone, uschar *domain, uschar *qtype,
75e0e026 212 int qtypelen, uschar **pkptr, int *countptr)
c55a77db
PH
213{
214int yield = HOST_NOT_FOUND;
c55a77db
PH
215int domainlen = Ustrlen(domain);
216BOOL pass_on_not_found = FALSE;
217tlist *typeptr;
218uschar *pk = *pkptr;
219uschar buffer[256];
220uschar rrdomain[256];
75e0e026 221uschar RRdomain[256];
c55a77db
PH
222
223/* Decode the required type */
224
225for (typeptr = type_list; typeptr->name != NULL; typeptr++)
226 { if (Ustrcmp(typeptr->name, qtype) == 0) break; }
227if (typeptr->name == NULL)
228 {
229 fprintf(stderr, "fakens: unknown record type %s\n", qtype);
230 return NO_RECOVERY;
231 }
232
233rrdomain[0] = 0; /* No previous domain */
234(void)fseek(f, 0, SEEK_SET); /* Start again at the beginning */
235
236/* Scan for RRs */
237
238while (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;
75e0e026
PH
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 }
c55a77db
PH
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
75e0e026
PH
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);
c55a77db
PH
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;
75e0e026
PH
379 if (ep[-1] != '.') sprintf(ep, "%s.", zone);
380 pk = packname(p, pk);
381 plen = Ustrlen(p);
382 break;
c55a77db
PH
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:
c55a77db
PH
406 if (ep[-1] != '.') sprintf(ep, "%s.", zone);
407 pk = packname(p, pk);
408 plen = Ustrlen(p);
c55a77db
PH
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;
c55a77db
PH
416 }
417
418*pkptr = pk;
419return (yield == HOST_NOT_FOUND && pass_on_not_found)? PASS_ON : yield;
420}
421
422
423
424/*************************************************
425* Entry point and main program *
426*************************************************/
427
428int
429main(int argc, char **argv)
430{
431FILE *f;
432DIR *d;
75e0e026 433int domlen, qtypelen;
c55a77db
PH
434int yield, count;
435int i;
436int zonecount = 0;
c55a77db 437struct dirent *de;
c55a77db
PH
438zoneitem zones[32];
439uschar *qualify = NULL;
440uschar *p, *zone;
441uschar *zonefile = NULL;
442uschar domain[256];
443uschar buffer[256];
444uschar qtype[12];
445uschar packet[512];
446uschar *pk = packet;
447
448if (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
458d = opendir(CCS buffer);
459if (d == NULL)
460 {
461 fprintf(stderr, "fakens: failed to opendir %s: %s\n", buffer,
462 strerror(errno));
463 return NO_RECOVERY;
464 }
465
466while ((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
487Ustrncpy(qtype, argv[3], sizeof(qtype));
488qtypelen = Ustrlen(qtype);
489for (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,
492and set up the zone file name. The zone names in the table all start with a
493dot. */
494
495domlen = Ustrlen(argv[2]);
496if (argv[2][domlen-1] == '.') domlen--;
497Ustrncpy(domain, argv[2], domlen);
498domain[domlen] = 0;
499for (i = 0; i < domlen; i++) domain[i] = tolower(domain[i]);
500
501if (Ustrchr(domain, '.') == NULL && qualify != NULL &&
502 Ustrcmp(domain, "dontqualify") != 0)
503 {
504 Ustrcat(domain, qualify);
505 domlen += Ustrlen(qualify);
506 }
507
508for (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
521if (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
530everything, because we know that Exim will look only at the answer and
531additional section parts. */
532
533memset(packet, 0, 12);
534pk += 12;
535
536/* Open the zone file. */
537
538f = fopen(buffer, "r");
539if (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
547count = 0;
75e0e026 548yield = find_records(f, zone, domain, qtype, qtypelen, &pk, &count);
c55a77db
PH
549if (yield == NO_RECOVERY) goto END_OFF;
550
551packet[6] = (count >> 8) & 255;
552packet[7] = count & 255;
553
75e0e026
PH
554/* There is no need to return any additional records because Exim no longer
555(from release 4.61) makes any use of them. */
c55a77db 556
75e0e026
PH
557packet[10] = 0;
558packet[11] = 0;
c55a77db
PH
559
560/* Close the zone file, write the result, and return. */
561
562END_OFF:
563(void)fclose(f);
564(void)fwrite(packet, 1, pk - packet, stdout);
565return yield;
566}
567
568/* End of fakens.c */