chiark / gitweb /
ce3019444f6a8e14d50cb88d442c69dcbb098830
[chiark-utils.git] / scripts / named-conf
1 #!/usr/bin/perl -w
2
3 use strict;
4 use IO::File;
5 use Data::Dumper;
6 use POSIX;
7
8 use vars qw($quis
9             $mode $doall
10             $etcfile $where
11             $debug $needglue $localonly $verbosity);
12
13 $quis= $0; $quis =~ s,.*/,,;
14
15 $mode= '';
16 $doall= 0;
17 $etcfile= "/etc/bind/chiark-conf-gen.zones";
18 $where= '<built-in>';
19 $debug= 0;
20 $needglue= 2;
21 $localonly= 0;
22 $verbosity= 1;
23
24 use vars qw($dig_owner $dig_type $dig_rdata);
25
26 while (@ARGV && $ARGV[0] =~ m/^\-/) {
27     $_= shift @ARGV;
28     if (s/^\-\-//) {
29         last if m/^$/;
30         if (m/^(yes|no|force)$/) { m/^./; $mode= $&; }
31         elsif (m/^all$/) { $doall=1; }
32         elsif (m/^config$/) { $etcfile= loarg(); $where= '--config option'; }
33         elsif (m/^glueless$/) { $needglue--; }
34         elsif (m/^localonly$/) { $localonly=1; }
35         elsif (m/^quiet$/) { $verbosity=0; }
36         elsif (m/^verbose$/) { $verbosity=2; }
37         else { usageerr("unknown option --$_"); }
38     } else {
39         s/^\-//;
40         last if m/^$/;
41         while (m/^./) {
42             if (s/^[ynf]//) { $mode=$&; }
43             elsif (s/^A//) { $doall=1; }
44             elsif (s/^C//) { $etcfile= soarg(); $where= '-C option'; }
45             elsif (s/^D//) { $debug++; }
46             elsif (s/^g//) { $needglue--; }
47             elsif (s/^l//) { $localonly=1; }
48             elsif (s/^q//) { $verbosity=0; }
49             elsif (s/^v//) { $verbosity=2; }
50             else { usageerr("unknown option -$&"); }
51         }
52     }
53 }
54
55 sub loarg() { usageerr("missing option value") if !@ARGV; return shift @ARGV; }
56 sub soarg() { my ($rv); $rv=$_; $_=''; return length $rv ? $rv : loarg(); }
57
58 usageerr("-g may be specified at most twice") if $needglue<0;
59 usageerr("-D may be specified at most twice") if $debug>2;
60 usageerr("must specify either -f|-y|-n or zones (and not both)")
61     if !!$mode == !!@ARGV;
62
63 sub usageerr ($) {
64     die
65 "$_[0]
66 usage: named-conf-regen [-rvq] -f|-y|-n|<zone>...\n".
67 "operation modes:\n".
68 " -f --force   install without checking\n".
69 " -y --yes     check and install\n".
70 " -n --no      check only\n".
71 "additional options:\n".
72 " -q --quiet   no output for OK zones\n".
73 " -v --verbose extra verbose\n";
74 }
75
76 cfg_fail("config filename $etcfile should not be directory")
77     if $etcfile =~ m,/$,;
78
79 use vars qw($default_dir);
80 $default_dir= $etcfile =~ m,^.*/, ? $& : './';
81
82 use vars qw($slave_dir $slave_prefix $slave_suffix);
83 $slave_dir= 'slave';
84 $slave_prefix= '';
85 $slave_suffix= '';
86
87 use vars qw(@self_ns @self_soa @self_addr @forbid_addr);
88 @self_ns= @self_soa= @self_addr= @forbid_addr= ();
89
90 use vars qw(%zone_cfg @zone_cfg_list);
91 %zone_cfg= ();
92 @zone_cfg_list= ();
93
94 use vars qw($output $default_output %output_contents);
95 $output= '';
96 $default_output= '';
97 %output_contents= ();
98
99 use vars qw($check $install);
100 $check= $mode !~ m/^f/;
101 $install= $mode =~ m/^[yf]/;
102
103 read_config($etcfile);
104 debug_dump('@zone_cfg_list %zone_cfg');
105 process_zones($mode ? @zone_cfg_list : @ARGV);
106 debug_dump('%output_contents');
107 output_files() if $install;
108
109 exit 0;
110
111 #-------------------- configuration reading
112
113 sub cfg_fail ($) { die "$quis: $where:\n $_[0]\n"; }
114
115 sub read_config ($) {
116     my ($if) = @_;
117     my ($fh,$z,@self,$before,
118         $mod,$dir,$prefix,$suffix,$subfile,$lprefix,$lsuffix,$zf);
119     local ($_);
120
121     $fh= new IO::File $if,'r' or cfg_fail("open $if:\n $!");
122     $before= '';
123     for (;;) {
124         if (!defined($_= <$fh>)) {
125             cfg_fail("read config file $if:\n $!") if $fh->error();
126             last;
127         }
128         chomp; s/\s+$//;
129         if (m/\\$/) { $before.= $_; next; }
130         $_= $before.$_;
131         $before= '';
132         s/^\s+//;
133         $where= "$if:$.";
134         next if m/^\#/;
135         last if m/^end$/;
136         next unless m/\S/;
137         if (m/^self(\-ns|\-soa|)\s+(\S.*\S)/) {
138             @self= split /\s+/, $2;
139             @self_ns= @self if $1 ne '-soa';
140             @self_soa= @self if $1 ne '-ns';
141         } elsif (m/^self\-addr\s+([0-9. \t]+)/) {
142             @self_addr= split /\s+/, $1;
143         } elsif (m/^forbid\-addr(?:\s+([0-9. \t]+))?/) {
144             @forbid_addr= defined $1 ? split /\s+/, $1 : ();
145         } elsif (m,^
146                  primary\-dir ([*?]?)
147                  \s+ (\S+)/([^/ \t]*)
148                  (?: \s+ ([^/ \t]*) (?: (/.+) )?
149                   )?
150                  $,x) {
151             ($mod, $dir, $prefix, $suffix, $subfile) =
152                 ($1,qualify($2),$3,$4,$5);
153             $suffix= '' if !defined $suffix;
154             $subfile= '' if !defined $subfile;
155             $suffix= '_db' if !length $suffix && !length $subfile;
156             if (-d "$dir/$prefix") { $dir.='/'; $dir.=$prefix; $prefix=''; }
157             opendir D, $dir or cfg_fail("open primary-dir $dir:\n $!");
158             $lprefix= length $prefix; $lsuffix= length $suffix;
159             while (defined($_= readdir D)) {
160                 next if m/^\./ && !$lprefix;
161                 next unless length > $lprefix+$lsuffix;
162                 next unless substr($_,0,$lprefix) eq $prefix;
163                 next unless substr($_,length($_)-$lsuffix) eq $suffix;
164                 $z= substr($_,$lprefix,length($_)-($lprefix+$lsuffix));
165                 $zf= $dir.'/'.$prefix.$z.$suffix.$subfile;
166                 if (!stat $zf) {
167                     next if length $subfile && $! == &ENOENT;
168                     cfg_fail("cannot stat zonefile $zf:\n $!");
169                 }
170                 -f _ or cfg_fail("zonefile $zf is not a plain file");
171                 zone_conf($z,'primary','p',$mod,$zf);
172             }
173             closedir D or cfg_fail("close primary-dir $dir:\n $!");
174         } elsif (m/^primary([*?]?)\s+(\S+)\s+(\S+)$/) {
175             zone_conf($2,'primary','p',$1,qualify($3));
176         } elsif (m/^published([*?]?)\s+(\S+)\s+([0-9.\t]+)$/) {
177             zone_conf($2,'published','s',$1,'',$3);
178         } elsif (m/^stealth([*?]?)\s+(\S+)\s+([0-9. \t]+)$/) {
179             zone_conf($2,'stealth','u',$1,'',split /\s+/, $3);
180         } elsif (m/^slave\-dir\s+(\S+)(?:(?:\s+(\S+))\s+(\S+))?$/) {
181             ($slave_dir, $slave_prefix, $slave_suffix) = (qualify($1),$2,$3);
182             $slave_prefix='' if !defined $slave_prefix;
183             $slave_suffix='' if !defined $slave_suffix;
184         } elsif (m/^output\s+bind8\+(\S+)$/) {
185             cfg_fail("default output may not apply to only some zones")
186                 if @zone_cfg_list && length $default_output;
187             set_output(qualify($1));
188         } elsif (m/^include\s+(\S+)$/) {
189             read_config($1);
190         } else {
191             cfg_fail("unknown configuration directive".
192                      " or incorrect syntax or arguments");
193         }
194     }
195     $fh->close or cfg_fail("close config file $if:\n $!");
196 }
197
198 sub qualify ($) {
199     my ($i) = @_;
200     $i= "$default_dir$i" unless $i =~ m,^/,;
201     return $i;
202 }
203
204 sub zone_conf ($$$$$@) {
205     my ($zone,$style,$sabbr,$mod,$file,@servers) = @_;
206     my ($sfx,$aref);
207     $file= qualify("$slave_dir/$slave_prefix".$zone.$slave_suffix)
208         unless length $file;
209     if (!length $output) {
210         $default_output= qualify('chiark-conf-gen.bind8')
211             unless length $default_output;
212         set_output($default_output);
213     }
214     cfg_fail("redefined zone $zone\n".
215              " earlier definition $zone_cfg{$zone}{'where'}")
216         if exists $zone_cfg{$zone};
217     $zone_cfg{$zone}{'where'}= $where;
218     $zone_cfg{$zone}{'file'}= $file;
219     $zone_cfg{$zone}{'style_p'}= $style.$mod;
220     $zone_cfg{$zone}{'s'}= $sabbr.$mod; # p)rimary s)econdary u)npub f)oreign
221     $zone_cfg{$zone}{'servers'}= [ @servers ];
222     foreach $sfx (qw(soa ns addr)) {
223         { no strict 'refs'; $aref= [ @{ "self_$sfx" } ]; }
224         @$aref or cfg_fail("failed to specify self-$sfx before zone");
225         $zone_cfg{$zone}{"self_$sfx"}= $aref;
226     }
227     $zone_cfg{$zone}{'output'}= $output;
228     push @zone_cfg_list, $zone;
229 }
230
231 sub set_output($) {
232     my ($newout) = @_;
233     $output= $newout;
234     $output_contents{$output}= '';
235 }
236
237
238 #-------------------- checking
239
240 use vars qw($zone $cfg $warnings);
241 $warnings= 0;
242
243 sub progress ($) {
244     return if !$verbosity;
245     print "$_[0]\n";
246 }
247
248 sub verbose ($) {
249     return if $verbosity<2;
250     print "    $_[0]\n";
251 }
252
253 sub process_zones (@) {
254     my (@zones) = @_;
255     local ($zone,$cfg);
256
257     foreach $zone (@zones) {
258         $cfg= $zone_cfg{$zone} || {
259             'style_p' => 'foreign',
260             's' => 'f',
261             'servers' => [ ],
262             };
263         progress(sprintf "%-40s %s", $zone, $$cfg{'style_p'});
264         if ($check && ($doall || $cfg->{'s'} !~ m/\?/)) {
265             eval {
266                 if ($localonly && $cfg->{'s'} =~ m/f/) {
267                     zone_warning("foreign zone specified with -l");
268                 } elsif ($cfg->{'s'} =~ m/\*/ ||
269                          ($localonly && $cfg->{'s'} !~ m/p/)) {
270                     zone_check_local();
271                 } else {
272                     zone_check_full();
273                 }
274             };
275             zone_warning("checks failed: $@") if length $@;
276         }
277         $output_contents{$$cfg{'output'}} .= zone_output()
278             if $install;
279     }
280     print STDERR "$quis: $warnings warnings\n" or die $!
281         if $warnings;
282 }
283
284 sub zone_warning ($) {
285     my ($w) = @_;
286     $w =~ s/\n$//;
287     $w =~ s,\n, // ,g;
288     print STDERR "$zone: warning: $w\n" or die $!;
289     $warnings++;
290 }
291
292 sub zone_warnmore ($) {
293     print STDERR "$zone:  $_[0]\n" or die $!;
294 }
295
296 use vars qw(%delgs); # $delgs{$nameserver_list} = [ $whosaidandwhy ]
297 use vars qw(%auths); # $auths{$nameserver_list} = [ $whosaidandwhy ]
298 use vars qw(%glue);  # $glue{$name}{$addr_list} = [ $whosaidandwhy ]
299 use vars qw(%soas);  # $soa{"$origin $serial"} = [ $whosaidandwhy ]
300 use vars qw(%addr_is_ok %warned_glueless %warned_nameaddr);
301 use vars qw($delg_to_us);
302 use vars qw(@to_check); # ($addr,$whyask,$is_auth,$glueless_ok, ...)
303 use vars qw(@to_check_soa); # ($addr,$whyask, ...)
304
305 sub zone_check_full () {
306     zone_reset();
307     zone_investigate();
308     zone_consistency();
309     zone_servers_ok();
310 }
311
312 sub zone_reset() {
313     %delgs= %auths= %glue= %soas=
314         %warned_glueless= %warned_nameaddr=
315             %addr_is_ok= ();
316     $delg_to_us= 0;
317     @to_check= @to_check_soa= ();
318 }
319
320 sub zone_investigate() {
321     my ($super_zone, @super_nsnames,
322         $super_ns, @super_ns_addrs, $s, $wa, $is_auth,
323         %nsrrset_checked, %soa_checked, $addr, $glueless_ok, $rcode);
324
325     $super_zone= $zone;
326     for (;;) {
327         debug_trace("zone $zone superzone $super_zone");
328         $super_zone =~ s/^[^.]+\.// or die "no superzone ? ($super_zone)\n";
329         ($rcode,@super_nsnames)= lookup($super_zone,'ns-','06');
330         last if !$rcode;
331     }
332     for $super_ns (@super_nsnames) {
333         $super_ns= lc $super_ns;
334         ($rcode,@super_ns_addrs)= lookup($super_ns,'a','0');
335         foreach $addr (@super_ns_addrs) {
336             push @to_check,
337                  $addr,
338                  "$super_ns, server for $super_zone",
339                  0, 0;
340         }
341     }
342     for (;;) {
343         # We do these in order so that we always do NS RRset checks on
344         # nameservers that came from other NS RRsets first; otherwise
345         # we might set nsrrset_checked due to a glueless_ok check,
346         # and then not check for gluefulness later.
347         debug_dump('@to_check @to_check_soa');
348         if (($addr,$wa,$is_auth,$glueless_ok,@to_check) = @to_check) {
349             push @to_check_soa, $addr, $wa if $is_auth;
350             next if $nsrrset_checked{$addr}++;
351             zone_check_nsrrset($addr, "[$addr] $wa",
352                                $is_auth, $glueless_ok);
353         } elsif (($addr,$wa,@to_check_soa) = @to_check_soa) {
354             next if $soa_checked{$addr}++;
355             zone_check_soa($addr,"[$addr] $wa","[$addr] NS");
356         } else {
357             last;
358         }
359     }
360 }
361
362 sub zone_check_nsrrset ($$$$) {
363     my ($uaddr,$ww, $is_auth, $glueless_ok) = @_;
364     my (@s, $s, %s2g, @glue, $glue, $delgs_or_auths, $wwn);
365     verbose("checking delegation by $ww");
366     dig(sub {
367         if ($dig_type eq 'ns' && $dig_owner eq $zone) {
368             $s2g{lc $dig_rdata} = [ ];
369         } elsif ($dig_type eq 'a' && exists $s2g{$dig_owner}) {
370             $wwn= "in glue from $ww";
371             push @to_check, $dig_rdata, "$dig_owner, $wwn", 1, 0;
372             zone_server_addr($dig_rdata,$dig_owner,$wwn,"NS [$uaddr]",0);
373             push @{ $s2g{$dig_owner} }, $dig_rdata;
374         }
375     },
376              $zone,'ns',$uaddr);
377     if (!%s2g) { zone_warning("unable to find NS RRset at $ww"); return; }
378     elsif (keys %s2g == 1) { zone_warning("only one nameserver at $ww"); }
379     @s= sort keys %s2g;
380     foreach $s (@s) {
381         @glue= @{ $s2g{$s} };
382         if (!@glue) {
383             zone_warning("glueless NS $s,".
384                          ($needglue<=1 ? " (eg)" : "").
385                          " from $ww")
386                 unless $glueless_ok || !$needglue ||
387                        ($needglue<=1 && $warned_glueless{$s}++);
388             next;
389         }
390         $glue= join ' ', sort @glue;
391         push @{ $glue{$s}{$glue} }, $ww;
392     }
393     $s= join ' ', @s;
394     $delgs_or_auths= $is_auth ? \%auths : \%delgs;
395     push @{ $delgs_or_auths->{$s} }, $ww;
396 }
397
398 sub zone_server_addr ($$$$$) {
399     my ($addr,$name,$ww,$wwq,$is_soa) = @_;
400     $addr_is_ok{$addr}= "$name ($wwq)"
401         if $is_soa || $cfg->{'s'} =~ m/u/;
402     zone_warning("configured as stealth but we [$addr]".
403                  " are published ($name $wwq)")
404         if $cfg->{'s'} =~ m/u/ && grep { $_ eq $addr } @self_addr;
405     zone_warning("forbidden nameserver address [$addr] $name ($wwq)")
406         if grep { $_ eq $addr } @forbid_addr;
407
408     my ($name_is_self, $addr_is_self);
409     $name_is_self= grep { $_ eq $name }
410         @{ $cfg->{$is_soa ? 'self_soa' : 'self_ns'} };
411     $addr_is_self= grep { $_ eq $addr }
412         @{ $cfg->{'self_addr'} };
413     if ($name_is_self && !$addr_is_self) {
414         zone_warning("our name $name with wrong address [$addr], (eg) $ww")
415             unless $warned_nameaddr{$name}{$addr}++;
416     } elsif (!$name_is_self && $addr_is_self) {
417         zone_warning(($is_soa ? "SOA ORIGIN maps to" : "allegedly served by").
418                      " us [$addr] with wrong name $name, (eg) $ww")
419             unless $warned_nameaddr{$name}{$addr}++;
420     }
421     $delg_to_us=1 if $name_is_self;
422 }
423
424 sub zone_check_soa ($$$) {
425     my ($uaddr,$ww,$wwq) = @_;
426     my ($lame,$origin,$got,$rcode,@soa_addrs,$soa_addr,$wwn);
427     verbose("checking service at $wwq");
428     $lame= 'dead or lame';
429     dig(sub {
430         if ($dig_type eq 'flags:') {
431             $lame= $dig_rdata =~ m/ aa / ? '' : 'lame';
432         } elsif ($dig_type eq 'soa' && $dig_owner eq $zone && !$lame) {
433             die "several SOAs ? $ww" if defined $origin;
434             $got= $dig_rdata;
435             $got =~ m/^(\S+) \d+/ or die "$got ?";
436             $origin= $1;
437         }
438     },
439              $zone,'soa',$uaddr);
440     $lame= 'broken' if !$lame && !defined $origin;
441     if ($lame) { zone_warning("$lame server $ww"); return; }
442     push @{ $soas{$got} }, $ww;
443     ($rcode,@soa_addrs)= lookup($origin,'a','0');
444     foreach $soa_addr (@soa_addrs) {
445         $wwn= "SOA ORIGIN from $ww";
446         zone_server_addr($soa_addr,$origin,$wwn,"SOA [$uaddr]",1);
447         push @to_check, $soa_addr, "$origin, $wwn";
448     }
449 }
450
451 sub zone_consistency() {
452     my ($d, $org_ser, $origin, $a, $h, $self_soa);
453     zone_consistency_set('delegations',\%delgs);
454     foreach $d (keys %delgs) { delete $auths{$d}; }
455     zone_consistency_set('zone nameserver rrset',\%auths);
456     foreach $h (keys %glue) {
457         zone_consistency_set("glue for $h", $glue{$h});
458     }
459     zone_consistency_set("SOA ORIGIN and SERIAL",\%soas);
460     $self_soa= $cfg->{'self_soa'};
461     if ($cfg->{'s'} =~ m/p/) {
462         foreach $org_ser (keys %soas) {
463             $org_ser =~ m/^(\S+) \d+$/ or die "$org_ser ?";
464             $origin= $1;
465             next if grep { $_ eq $origin } @$self_soa;
466             zone_warning("SOA ORIGIN $origin is not our name (@$self_soa),".
467                          " eg from ".($soas{$org_ser}[0]));
468         }
469     }
470 }
471
472 sub zone_servers_ok () {
473     my ($showok);
474     if (%addr_is_ok) {
475         $showok= 0;
476         foreach $a (@{ $cfg->{'servers'} }) {
477             next if exists $addr_is_ok{$a};
478             zone_warning("we slave from $a"); $showok=1;
479         }
480         if ($showok) {
481             foreach $a (keys %addr_is_ok) {
482                 zone_warnmore("permitted master [$a] $addr_is_ok{$a}");
483             }
484         }
485     }
486     if ($cfg->{'s'} =~ m/s/ && !$delg_to_us) {
487         zone_warning("we are supposedly published secondary,".
488                      " but not listed as a nameserver");
489     }
490 }
491
492 sub zone_consistency_set ($%) {
493     my ($msg,$set) = @_;
494     my ($d,$o);
495     if (keys(%$set) > 1) {
496         zone_warning("inconsistent $msg:");
497         foreach $d (keys %$set) {
498             foreach $o (@{ $set->{$d} }) { zone_warnmore(" $d from $o"); }
499         }
500     }
501 }
502
503 sub zone_check_local () {
504     zone_reset();
505     zone_servers_simplefind();
506     zone_servers_ok();
507 }
508
509 sub zone_servers_simplefind () {
510     my ($rcode,@nsnames,$ns,@soas,$origin);
511
512     ($rcode,@nsnames)= lookup($zone,'ns-','0');
513     foreach $ns (@nsnames) { zone_server_simple($ns,'NS',0); }
514
515     ($rcode,@soas)= lookup($zone,'soa','0');
516     die "multiple SOA RRs in set!  @soas ?" if @soas!=1;
517     $soas[0] =~ m/^(\S+)\s/ or die "SOA ? $_";
518     zone_server_simple(domain_canon($1),'SOA',1);
519 }
520
521 sub zone_server_simple ($$$) {
522     my ($name,$ww,$is_soa) = @_;
523     my ($rcode,@addrs,$addr);
524     ($rcode,@addrs)= lookup($name,'a','0');
525     foreach $addr (@addrs) { zone_server_addr($addr,$name,$ww,$ww,$is_soa); }
526 }
527
528 #-------------------- outputting
529
530 sub zone_output () {
531     my ($o,$m);
532
533     $o= "zone \"$zone\" {\n";
534     if ($$cfg{'s'} =~ m/p/) {
535         $o.= "    type master;\n";
536     } else {
537         $o.= "    type slave;\n".
538              "    masters {\n";
539         foreach $m (@{ $$cfg{'servers'} }) { $o.= "        $m;\n"; }
540         $o.= "    };\n";
541     }
542     $o.= "    file \"$$cfg{'file'}\";\n";
543     $o.= "};\n";
544     return $o;
545 }
546
547 sub output_files () {
548     my ($fn,$ofn,$mfn,$l,$dir, $maxmode,$h,@to_install);
549     
550     foreach $ofn (keys %output_contents) {
551         $fn= $ofn; $mfn= "output file $fn";
552         for (;;) {
553             if (!lstat $fn) {
554                 $! == &ENOENT or die "$quis: stat $mfn:\n $!\n";
555                 $maxmode= 0666;
556                 last;
557             } elsif (-f _) {
558                 $maxmode= (stat _)[2];
559                 last;
560             } elsif (-l _) {
561                 defined($l= readlink $fn)
562                     or die "$quis: readlink $mfn:\n $!\n";
563                 $dir= $fn =~ m,^.*/, ? $& : './';
564                 $fn= "$dir$l" unless $l =~ m,^/,;
565                 $mfn= "output file $fn (symlink target of $ofn)";
566             } else {
567                 die "$quis: output file $mfn exists but is not a file".
568                     " (or symlink to one)";
569             }
570         }
571         unlink "$fn.new" or $! == &ENOENT or
572             die "$quis: cannot clear out old .new version of $mfn:\n $!";
573         $h= new IO::File "$fn.new",'w',$maxmode
574             or die("$quis: create .new version of $mfn:\n $!");
575         print $h
576             "# generated by $quis, do not edit\n",
577             $output_contents{$ofn}
578             or die "$quis: write data to .new version of $mfn:\n $!";
579         $h->close
580             or die "$quis: close .new version of $mfn:\n $!";
581         push @to_install, $fn,$mfn;
582     }
583
584     while (($fn,$mfn, @to_install) = @to_install) {
585         rename "$fn.new",$fn
586             or die "$quis: install new version of $mfn:\n $!";
587     }
588 }
589
590 #-------------------- general utilities
591
592 sub debug_dump ($) {
593     my ($vn);
594     return unless $debug>1;
595     local $Data::Dumper::Terse=1;
596     foreach $vn (split /\s+/, $_[0]) {
597         print "$vn := ", eval "Dumper(\\$vn)";
598     }
599 }
600
601 sub debug_trace ($) {
602     return unless $debug;
603     print "D $_[0]\n";
604 }
605
606 sub lookup ($$$) {
607     my ($domain,$type,$okrcodes) = @_;
608     my ($c,$h,@result);
609     debug_trace("lookup ==> (->$okrcodes) $domain $type");
610     $h= new IO::Handle;
611
612     defined($c= open $h, "-|") or die "$quis: fork adnshost:\n $!\n";
613     if (!$c) {
614         exec 'adnshost','-Fi','+Do','+Dt','+Dc','-Cf',"-t$type",
615              '-',"$domain.";
616         die "$quis: exec adnshost:\n $!\n";
617     }
618     @result= $h->getlines();
619     $h->error and die "$quis: read from adnshost:\n $!\n";
620     chomp @result;
621     $!=0; $h->close;
622     die "$quis: lookup -t$type $domain $okrcodes failed $? $! @result\n"
623         if $! or $?>6 or index($okrcodes,$?)<0;
624     debug_trace("lookup <== $? @result");
625     return ($?,@result);
626 }
627
628
629 sub dig (&$$$) {
630     my ($eachrr, $qowner,$qtype,$qaddr) = @_;
631     # also pseudo-rr with type `flags:'
632     my ($h,$inmid,$irdata,$c);
633     local ($_);
634
635     debug_trace("dig ==> \@$qaddr $qowner $qtype");
636
637     $h= new IO::Handle;
638     defined($c= open $h, "-|") or die "$quis: fork dig:\n $!\n";
639     if (!$c) {
640         open STDERR, ">&STDOUT" or die $!;
641         exec ('dig',
642               '+nodef','+nosea','+nodebug','+norecurse',
643               "\@$qaddr",'-t',$qtype,$qowner);
644         die "$quis: exec dig:\n $!\n";
645     }
646     $inmid='';
647     for (;;) {
648         if (!defined($_= $h->getline())) {
649             $h->error() and die "$quis: read from dig:\n $!\n";
650             last;
651         }
652         chomp;
653         if (length $inmid) {
654             s/^\s+/ / or die "$inmid // $_ ?";
655             s/\;.*$//;
656             $_= $inmid.$_;
657             $inmid='';
658             s/$/ \(/ unless s/\s*\)\s*$//;
659         }
660         if (s/\s*\(\s*$//) { $inmid= $_; next; }
661         if (m/^\;\; flags\:( [-0-9a-z ]+)\;/) {
662             $dig_owner=''; $dig_type='flags:'; $dig_rdata= "$1 ";
663             debug_trace("dig  f: $dig_rdata");
664             &$eachrr;
665         } elsif (m/^\;/) {
666         } elsif (!m/\S/) {
667         } elsif (m/^([-.0-9a-z]+)\s+\d\w+\s+in\s+([a-z]+)\s+(\S.*)/i) {
668             $dig_owner=domain_canon($1); $dig_type=lc $2; $irdata=$3;
669             if ($dig_type eq 'a') {
670                 $irdata =~ m/^[.0-9]+$/ or die "$irdata ?";
671                 $dig_rdata= $&;
672             } elsif ($dig_type eq 'ns') {
673                 $irdata =~ m/^[-.0-9a-z]+$/i or die "bad nameserver $irdata ?";
674                 $dig_rdata= domain_canon($irdata);
675             } elsif ($dig_type eq 'soa') {
676                 $irdata =~ m/^([-.0-9a-z]+)\s+.*\s+(\d+)(?:\s+\d\w+){4}$/i
677                     or die "bad SOA $irdata ?";
678                 $dig_rdata= domain_canon($1).' '.$2;
679             } else {
680                 debug_trace("ignoring uknown RR type $dig_type");
681                 next;
682             }
683             debug_trace("dig  $dig_owner $dig_type $dig_rdata");
684             &$eachrr;
685         } else {
686             debug_trace("ignoring unknown dig output $_");
687         }
688     }
689     $h->close;
690     debug_trace("dig <== gave $?");
691 }
692
693 sub domain_canon ($) {
694     local ($_) = @_;
695     s/(.)\.$/$1/;
696     die "domain $_ ?" unless m/^[0-9a-z]/i;
697     return lc $_;
698 }