#!/usr/bin/perl
# Copyright 2004-2012 SPARTA, Inc.  All rights reserved.
# See the COPYING file included with the DNSSEC-Tools package for details.

#
# If we're executing from a packed environment, make sure we've got the
# library path for the packed modules.
#
BEGIN {
    if ($ENV{'PAR_TEMP'}) {
	unshift @INC, ("$ENV{'PAR_TEMP'}/inc/lib");
    }
}


use Net::DNS;
use Net::DNS::ZoneFile::Fast;
use Net::DNS::SEC::Tools::Donuts::Rule;
use Net::DNS::SEC::Tools::QWPrimitives;
use Net::DNS::SEC::Tools::BootStrap;
use Net::DNS::SEC::Tools::conf;

######################################################################
# detect needed perl module requirements
#
dnssec_tools_load_mods('Date::Parse' => "");
use Data::Dumper;

my $have_qw = 
  eval {
      require QWizard;
     };
my $qw;

use strict;

use Config;

our @guiargs;

my %opts = (l => 5,
	    c => $ENV{'HOME'} . "/.donuts.conf",
	    T => 'port 53 || ip[6:2] & 0x1fff != 0',
	    o => '%d.%t.pcap',
	    r => "/usr/local/share/dnssec-tools/donuts/rules/*.txt," .
	    $ENV{'HOME'} . "/.dnssec-tools/donuts/rules/*.txt");

my $TCPDUMP = "tcpdump";

# override some defaults if we're a self-extracting perl archive with
# the files contained within the archive.
if (runpacked()) {
    $opts{'r'} = $ENV{'PAR_TEMP'} . "/inc/rules/" . "*.txt,";

    setconffile("$ENV{'PAR_TEMP'}/inc/dnssec-tools.conf");

    $TCPDUMP = "./tcpdump";
}

our (@rules, %rules, $rf, $current_zone_file, %nrecs,
    @ignorelist, $netdns, $netdns_error, %outstructure, $current_domain,
    $globalrulecount, $globalrrset, $globalnrecs,
    $globalerrcount, @filestested);


my %primaries = 
  (display_errors =>
   {
    title => 'Zone Errors',
    introduction => 'Below are the errors found when analyzing the zones',
    leftside => 
    ["Browse Results", 
     { type => 'tree',
       name => 'showthis',
       refresh_on_change => 1,
       expand_all => 2,
       root => 'Errors',
       parent => \&get_gui_parent,
       children => \&get_gui_children },
    ],
    questions =>
    [{ type => 'table',
       name => 'results',
       text => sub { ((qwparam('showthis') eq 'Errors' ||
		       qwparam('showthis') eq '') ?
		      'Summary:' : 'Results:') },
       values => sub {
	   if (qwparam('showthis') =~ /::/) {
	       my $tab;
	       my ($spot, $value) = (qwparam('showthis') =~ /(.*)::(.*)/);
	       foreach my $data (@{$outstructure{$spot}{$value}}) {
		   push @$tab, $data;
	       }
	       return [$tab];
	   } elsif (qwparam('showthis') eq 'Errors' ||
		    qwparam('showthis') eq '') {
	       my $tab;

	       push @$tab, ['results on testing:', join(', ',@filestested)];
	       push @$tab, ['rules considered:', (1+$#rules)];
	       push @$tab, ['rules tested:',     $globalrulecount];
	       push @$tab, ['records analyzed:', ($globalrrset)];
	       push @$tab, ['names analyzed:',   $globalnrecs];
	       push @$tab, ['errors found:',     $globalerrcount];
	       return [$tab];
	   }
	   return [[[""]]];
       }
     }]
   });

DTGetOptions(\%opts,
		['GUI:VERSION',"DNSSEC-Tools Version: 1.12"],

		['GUI:screen',"Rule Set Configuration:"],
		["l|level=i", "The maximum rule level to run (default = 5)",
		 helpdesc => '(higher number = include more nit-picking tests)',
		 question => { type => 'menu', values => [1..9] }],

		['GUI:separator','Output Format Options:'],
		["show-gui", "Display the results in a browsable window.",
		 nocgi => 1,
		 question => { type => 'checkbox', default => 1, indent => 1}],
		["v|verbose+",
		 "Verbose output (show extra processing information).  Use multiple times for increasing amounts of output.",
		 question => { type => 'checkbox', default => 1, indent => 1 }],
		["q|quiet", "Quiet output (Do not print summary information)",
		 indent => 1],

		['GUI:separator','Advanced Options:'],
		['GUI:guionly',{type => 'checkbox',
				values => [1,0],
				default => 0,
				indent => 1,
				text => 'Show Advanced Options',
				name => 'advanced'}],

		"",
		['GUI:guionly',{type => 'button',
				values => 'Help',
				default => 1,
				nocgi => 1,
				text => 'Display Help Options',
				name => 'displayhelp'},
		],

		['GUI:screen','Advanced Configuration:', doif => 'advanced'],
		['GUI:separator','Rules Selection Configuration:'],
		["r|rules=s", "glob pattern for rule files to load",
		 indent => 1,
		 doif =>
		 sub { ref($_[1]->{'generator'}) !~ /HTML/}, # not safe for web
		],
		["i|ignore=s", "Regular expression for rules to ignore",
		 indent => 1],
		["f|features=s", "Extra features to turn on",
		 helpdesc => '(comma separated)',
		 indent => 1],

		['GUI:separator',"Configuration Files:", nocgi => 1],
		["C|no-config","Do not load personal configuration files",
		 indent => 1, nocgi => 1],
		["c|config-file=s",
		 "Use an alternate personal configuration file",
		 indent => 1,
		 doif =>
		 sub { ref($_[1]->{'generator'}) !~ /HTML/}, # not safe for web
		],

		['GUI:otherargs_text',"FILE DOMAIN [FILE DOMAIN...]"],
		['GUI:otherargs_required',1],
		
		['GUI:screen',"Extra Live Query Options:",
		 doif => sub { 
		     $_[1]->qwparam('live') && !$_[1]->qwparam('displayhelp') &&
		       ref($_[1]->{'generator'}) !~ /HTML/  # not safe for web
			 ;
		 }
		],
		["t|tcpdump-capture=s",
		 "Start tcpdump on interface STRING during run"],
		["T|tcpdump-filter=s",
		 "Use tcpdump filter (default: port 53 or fragments)"],
		["o|tcpdump-output-file=s",
		 "Save tcpdump results to file STRING."],

		['GUI:screen',"Help Options:",
		 doif => 'displayhelp'],
		["R|help-rules", 'Show the rules that donuts checks'],
		["F|help-features",
		 "Show available additional features of available rules."],
		["H|help-config",
		 'Show configuration tokens supported by the rules'],
		
		['GUI:nootherargs',1],
		['GUI:submodules','getzonefiles','getzonenames'],
		['GUI:otherprimaries',
		 dnssec_tools_get_qwprimitives(%primaries)],
	       ) || exit;

push @main::ARGV, @guiargs;

if (!$opts{'R'} && !$opts{'F'} && !$opts{'H'} && 
    ($#ARGV == -1 || $#ARGV % 2 != 1)) {
    print STDERR "\nUsage Error: $0 called with wrong number of arguments\n";
    print STDERR "  file and zone name arguments are both needed\n";
    print STDERR "  (EG: $0 FILE1 example.com FILE2 other.example.com)\n\n";
    exit 1;
}

#
# initialize ignore list
#
if ($opts{'i'}) {
    @ignorelist = split(/,\s*/, $opts{'i'});
}

#
# create the feature set
#
my %features;
if ($opts{'f'}) {
    foreach my $feat (split(/,\s*/, $opts{'f'})) {
	$features{$feat} = 1;
    }
}

#
# initialize our resolver
#
if ($features{'live'}) {
    use Net::DNS;
    $netdns = Net::DNS::Resolver->new;
}

#
# load rule files
#   (comma separated list)
#
foreach_rule_file(
    sub {
        my $rf = shift;
	print STDERR "--- loading rule file $rf\n    rules:" if ($opts{'v'});
	if ($rf =~ /\.pl$/) {
	    do $rf;
	} else {
	    parse_rule_file($rf);
	}
	print STDERR "\n" if ($opts{'v'});
    }
);

#
# load optional user-config file
#
if ($opts{'c'} && !$opts{'C'} && -f $opts{'c'}) {
    parse_user_config($opts{'c'});
}

#
# display config file help
#
if ($opts{'H'}) {
    maybe_output_to_web();
    print STDERR "$0 configuration tokens for loaded rules:\n\n";
    printf STDERR sprintf("%-20s %-15s%s\n",
			  "RULE NAME", "TOKEN", "DESCRIPTION");
    printf STDERR sprintf("%-20s %-15s%s\n", "_" x 19, "_" x 13, 
			  "_" x (80-20-15-2));
    foreach my $rule (@rules) {
	$rule->print_help();
    }
    exit;
}

#
# display a list of rules
#
if ($opts{'R'}) {
    maybe_output_to_web();
    print STDERR "\n$0 rules:\n\n";
    printf STDERR "RULE NAME\n  DESCRIPTION...\n";
    printf STDERR "_" x 75 . "\n";
    foreach my $rule (@rules) {
	$rule->print_description() if (!$rule->{'internal'});
    }
    exit;
}

#
# display a list of rules
#
if ($opts{'F'}) {
    maybe_output_to_web();
    print STDERR "\n$0 feature list:\n";
    print STDERR "  (Turn these on using the --features flag)\n\n";
    my %shown;
    foreach my $rule (@rules) {
	if (exists($rule->{'feature'}) &&
	    !exists($shown{$rule->{'feature'}})) {
	    print "  ", $rule->{'feature'},"\n";
	    $shown{$rule->{'feature'}} = 1;
	}
    }
    exit;
}

#
# must specify at least one zone file
#
exit() if ($opts{'h'} || $#ARGV == -1);

#
# load zone files
#
my $exitcode = 0;
my $parseerror;
my $errcount;

maybe_output_to_web();
while ($#ARGV > -1) {
    $errcount = 0;
    my $rulecount = 0;
    my ($rulesrun, $errorsfound);
    $current_zone_file = shift;
    push @filestested, $current_zone_file;
    $current_domain = shift;
    $current_domain =~ s/\.$//;  # remove potential trailing dot
    %nrecs = ();

    #
    # Parse the file into an array
    #
    $parseerror = 0;
    my $rrset = Net::DNS::ZoneFile::Fast::parse(file => $current_zone_file,
						origin => "$current_domain.",
						soft_errors => 1,
						on_error =>\&print_parse_error);
    next if ($parseerror);
    if (!$rrset) {
	print STDERR "WARNING: failed to read $current_zone_file for an unknown reason\n";
	print STDERR "$@\n" if ($@);
	next;
    }

    #
    # Start collecting TCPDUMP data if requested
    #
    my $tcpdumpproc;
    if ($opts{'t'}) {
	my $file = $opts{'o'};
	$file =~ s/\%t/time()/eg; # replace %t with epoch
	$file =~ s/\%d/$current_domain/g; # replace %d with domain
	my @args = ("-i", $opts{t},
		    "-f", $opts{T},
		    "-s", 4096,
		    "-w", $file);
	if ($tcpdumpproc = fork()) {
	    # parent
	    sleep(2);  # wait for child to get going
	    print STDERR "--- Starting tcpdump\n" if ($opts{v});
	} else {
	    # child

	    # close stderr/out since we don't want the output
	    close(STDOUT);
	    close(STDERR);

	    open(STDOUT,">/dev/null");
	    open(STDERR,">/dev/null");

	    # exec tcpdump
	    exec($TCPDUMP, @args);
	}
    }

    #
    # call each rule on each record
    #
    print STDERR "--- Analyzing individual records in $current_zone_file\n" if ($opts{'v'});
    my $firstrun = 1;
    foreach my $rec (@$rrset) {
	foreach my $r (@rules) {
	    ($rulesrun, $errorsfound) =
	      $r->test_record($rec, $current_zone_file,
			      $opts{'l'}, \%features, $opts{'v'});
	    $errcount += $errorsfound;
	    $rulecount += $rulesrun if ($firstrun);
	}
	push @{$nrecs{$rec->name}{$rec->type}}, $rec;
	$firstrun = 0;
    }


    #
    # call each ruletype=name rule on each name set of records
    #
    print STDERR "--- Analyzing records for each name in $current_zone_file\n"  if ($opts{'v'});
    $firstrun = 1;
    foreach my $namerec (keys(%nrecs)) {
	foreach my $r (@rules) {
	    ($rulesrun, $errorsfound) =
	      $r->test_name($nrecs{$namerec}, $namerec,
			    $current_zone_file,
			    $opts{'l'}, \%features, $opts{'v'});
	    $errcount += $errorsfound;
	    $rulecount += $rulesrun if ($firstrun);
	}
	$firstrun = 0;
    }

    #
    # stop tcpdump if we had started it
    #

    if ($tcpdumpproc) {
	print STDERR "--- Stopping tcpdump.\n" if ($opts{v});
	kill(15, $tcpdumpproc);
	sleep(1);
	kill(9, $tcpdumpproc);
    }

    # collect global stats for gui display
    $globalrulecount += $rulecount if ($globalrulecount == 0);
    $globalrrset += 1 + $#$rrset;
    my @a = keys(%nrecs);
    $globalnrecs += 1 + $#a;
    $globalerrcount += $errcount;

    if ($opts{'v'}) {
	print "results on testing $current_domain:\n";
	print "  rules considered:\t" . (1+$#rules) . "\n";
	print "  rules tested:\t\t$rulecount\n";
	print "  records analyzed:\t" . (1+$#$rrset) . "\n";
	print "  names analyzed:\t" . (1+$#a) . "\n";
	print "  errors found:\t\t$errcount\n";
    } else {
	print "$errcount errors found in $current_zone_file\n"
	  if (!$opts{'q'});
    }
    if ($#rules == -1) {
	print "\nWARNING: no rules found to be executed!!!\n";
	print "WARNING: (maybe use the --rules switch to fix this?)\n";
    }
    if ($#$rrset == -1) {
	print "\nWARNING: no records found to be analyzed in $current_domain!!!\n";
    }
    if ($#a == -1) {
	print "\nWARNING: no names found to be analyzed in $current_domain!!!\n";
    } 
   if ($errcount) {
	$exitcode = 1;
    }
}

if ($opts{'show-gui'}) {
    setup_gui();
    display_gui_results();
}
exit($exitcode);

######################################################################
#
# GUI support (requires the QWizard module)
#

#
# setup: creates the qwizard instance and needed primaries
#
sub setup_gui {
    return if (!$have_qw);
    import QWizard;

    # the primaries
    $qw = $Getopt::Long::GUI_qw || new QWizard();
    $qw->merge_primaries(\%primaries);
}

#
# calls QWizard
#
sub display_gui_results {
    return if (!$have_qw);
    $qw->reset_qwizard();
    $qw->{'generator'}{'noheaders'} = 1;
    $qw->magic('display_errors');
}

#
# returns the parent of a given node
#
sub get_gui_parent {
    my ($wiz, $name) = @_;
    return if ($name eq 'Errors');
    return 'Errors' if ($name eq 'By Record Name' || $name eq 'By Rule Type');
    return 'By Record Name' if ($name =~ /^location::/);
    return 'By Rule Type' if ($name =~ /^rulename::/);
}

#
# returns the children of a given node
#
sub get_gui_children {
    my ($wiz, $name) = @_;
    return ['By Record Name', 'By Rule Type'] if ($name eq 'Errors');

    if ($name eq 'By Record Name') {
	my @ret;
	map { push @ret, { name => 'location::' . $_,
			   label => $_ }
	  } keys(%{$outstructure{'location'}});
	return \@ret;
    }

    if ($name eq 'By Rule Type') {
	my @ret;
	map { push @ret, { name => 'rulename::' . $_,
			   label => $_ }
	  } keys(%{$outstructure{'rulename'}});
	return \@ret;
    }
    return;
}


#######################################################################

sub foreach_rule_file {
    my ($cb) = @_;
    foreach my $rfexp (split(/,\s*/, $opts{'r'})) {
	my @rfs = glob($rfexp);
	foreach $rf (@rfs) {
	    next if (! -f $rf || $rf =~ /.bak$/ || $rf =~ /~$/);
	    $cb->($rf);
	}
    }
}

sub add_rule {
    my $rule = shift;

    # ignore certain rules
    if ($opts{'i'}) {
	foreach my $i (@ignorelist) {
	    if ($rule->{'name'} =~ /$i/) {
		return;
	    }
	}
    }

    # merge in default values
    my %defaultrule = ( level => 5 );
    foreach my $key (keys(%defaultrule)) {
	$rule->{$key} = $defaultrule{$key} if (!exists($rule->{$key}));
    }

    # check rule validity for required fields
    if (!$rule->{'name'}) {
	warn "no name for a rule in file $rf\n";
    }
    if (!$rule->{'test'}) {
	warn "no test defined for a rule in file $rf\n";
    }

    print STDERR " $rule->{'name'}" if ($opts{'v'});

    if ($opts{'show-gui'}) {
	$rule->{'gui'} = \%outstructure;
    }

    # remember the rule
    $rule = new Net::DNS::SEC::Tools::Donuts::Rule($rule);
    push @rules, $rule;
    $rules{$rule->{name}} = $rule;
}

# parses a text based rule file
sub parse_rule_file {
    my $file = $_[0];
    my ($rule, $err);
    open(RF, $file);
    my $nextline;
    my $count;
    my $ruledef;

    $err = 0;
    while (($_ = $nextline) || ($_ = <RF>)) {
	$nextline = undef;
	$count++;
	next if (/^\s*#/);

	$ruledef .= $_;

	# deal with multi-line records
	if (/(<|)(test|init)(>|:)/) {
	    my $type = $2;
	    my $xmllike = 0;
	    $xmllike = 1 if ($1 eq '<');

	    # collect code
	    my $code;
	    while (<RF>) {
		# rule code must begin with white space
		$count++;
		last if ((!$xmllike && (!/^\s/ || /^\s*$/)) ||
			 ($xmllike && /<\/(test|init)>/));
		$code .= $_;
		$ruledef .= $_;
	    }
	    $ruledef .= $_;

	    # evaluate it
	    if ($type eq 'init') {
		eval("$code");
	    # if error, mention it
		if ($@) {
		    warn "broken code in $file:$count rule '$rule->{name}': $@";
		    print STDERR "IN CODE:\n  $code\n" if ($opts{'v'});
		    $err = 1;
		}
	    } else {
		$rule->{'test'} = $code;
	    }

	    if (!/^\s/ && !/<\/(test|init)/) {
		$count--;
		$nextline = $_;
	    }
	} elsif (/^\s*help:\s*(\w+):\s*(.*)/) {
	    push @{$rule->{'help'}}, { token => $1, description => $2 };
	} elsif (/^\s*(\w+):\s*(.*\S)\s*$/) {
	    $rule->{$1} = $2;
	} elsif (!/^\s*$/) {
	    print STDERR "illegal rule in $file:$count for rule $rule->{name}";
	}

	if ($rule && !exists($rule->{'code_file'})) {
	    $rule->{'code_file'} = $file;
	    $rule->{'code_line'} = $count;
	}

	# end of rule (can get here from inside a test end too, hence
	# not an else clause above)
	if (/^\s*$/) {
	    if ($rule && !$err) {
		$rule->{'ruledef'} = $ruledef if ($opts{'v'} >= 2);
		$ruledef = '';
		add_rule($rule);
	    }
	    $rule = undef;
	    $err = 0;
	}
    }
    if ($rule && !$err) {
	$rule->{'ruledef'} = $ruledef if ($opts{'v'} >= 2);
	add_rule($rule);
    }
}

sub parse_user_config {
    my ($file) = @_;
    open(I,$file);
    my $line;
    my $name;
    while (<I>) {
	$line++;
	next if (/^\s*#/);
	if (/^\s*$/) {
	    $name = undef;
	    next;
	}
	if (/^name:\s*(.*)/) {
	    $name = $1;
	    if (!exists($rules{$name})) {
		print STDERR "$file:$line warning: no such rule: $name\n";
	    }
	    next;
	}
	if (!$name) {
	    close(I);
	    print STDERR
	      "Error in $file at line $line: no rule name found yet\n";
	    exit;
	}
	if (/^(test|init):/) {
	    close(I);
	    print STDERR
	      "Error in $file at line $line: Illegal token in config file.\n";
	    exit;
	}
	if (!/^(\w+):\s*(.*)$/) {
	    close(I);
	    print STDERR
	      "Error in $file at line $line: Illegal definition.\n";
	    exit;
	}
	if (exists($rules{$name})) {
	    $rules{$name}->config($1, $2);
	}
    }
}

#
# subroutines for doing live queries on running systems
#

sub get_query {
    my ($name, $type, $resolver) = @_;
    $resolver = $netdns if (!$resolver);
    my $query = $resolver->query($name, $type);
    if ($query) {
	return $query;
    } else {
	# print STDERR "DNS error " . $resolver->errorstring . "\n";
	$netdns_error = $resolver->errorstring;
	return;
    }
}

sub live_query {
    my $query = get_query(@_);
    if ($query) {
	return $query->answer;
    }
    return ();
}

#
# returns 0 when arrays of records are identical.
# returns -1 if the arrays are non-equal length
# returns the index+1 where the arrays differ otherwise.
sub compare_arrays {
    my ($a1, $b1, $sortfun) = @_;
    $sortfun = sub { $a cmp $b } if (!$sortfun);
    return -1 if ($#$a1 != $#$b1);
    my @a = sort $sortfun @$a1;
    my @b = sort $sortfun @$b1;

    for (my $i = 0; $i <= $#a && $i <= $#b; $i++) {
	if ($a[$i]->string() ne $b[$i]->string()) {
	    return $i+1;
	}
    }
    return $#a+1 if ($#a < $#b);
    return $#b+1 if ($#a < $#a);
    return 0;
}

sub compare_RR_arrays {
    my ($a1, $b1, $hashval) = @_;
    return -1 if ($#$a1 != $#$b1);
    my @a = sort @$a1;
    my @b = sort @$b1;

    for (my $i = 0; $i <= $#$a1; $i++) {
	print STDERR "$a[$i]{$hashval} ne $b[$i]{$hashval}\n";
	if ($a[$i]{$hashval} ne $b[$i]{$hashval}) {
	    return $i;
	}
    }
    return;
}

sub print_parse_error {
    my ($line, $err) = @_;
    $errcount++;
    print STDERR "$current_zone_file:$line $err\n";
}

#
# setup for printing some things to a web page instead
#
sub maybe_output_to_web {
    #
    # some stuff for web purposes should be redirected to the screen
    #
    if (defined($Getopt::GUI::Long::GUI_qw) && $Getopt::GUI::Long::GUI_qw->{'generator'} =~ /HTML/) {
	close STDERR;
	*STDERR = *STDOUT;  # alias stderr to stdout so it'll go to the web
	print "<br /><pre>\n\n";
    }
}

# this is merely a convenience function for rule authors to place into
# rules so a break point can be put on the function to stop in a
# particular location within a rule definition.
sub break_here {
    my $x = 1;
}

=pod

=head1 NAME

donuts - analyze DNS zone files for errors and warnings

=head1 SYNOPSIS

  donuts [-v] [-l LEVEL] [-r RULEFILES] [-i IGNORELIST]
         [-C] [-c configfile] [-h] [-H] ZONEFILE DOMAINNAME...

=head1 DESCRIPTION

B<donuts> is a DNS lint application that examines DNS zone files
looking for particular problems.  This is especially important for
zones making use of DNSSEC security records, since many subtle
problems can occur.  The default mode of operation assumes you want to
check for DNSSEC-related issues; to turn off the invocation of the
DNSSEC-related rules run B<donuts> with "-i DNSSEC".

If the B<Text::Wrap> Perl module is installed, B<donuts> will give better
output formatting.

=head1 OPTIONS

=head2 Rule Set Configuration:

=over

=item -l I<LEVEL>

=item --level=I<LEVEL>

Sets the level of errors to be displayed.  The default is level 5.
The maximum value is level 9, which displays many debugging results.
You probably want to run no higher than level 8.

=item -r I<RULEFILES>

=item --rules=I<RULEFILES>

A comma-separated list of rule files to load.  The strings will be
passed to I<glob()> so * wildcards can be used to specify multiple files.

Defaults to B</usr/local/share/dnssec-tools/donuts/rules/*.txt> and
B<$HOME/.dnssec-tools/donuts/rules/*.txt>.

=item -i I<IGNORELIST>

=item --ignore=I<IGNORELIST>

A comma-separated list of regex patterns which are checked against
rule names to determine if some should be ignored.  Run with I<-v> to
figure out rule names if you're not sure which rule is generating
errors you don't wish to see.

=item -f LIST

=item --features=LIST

The I<--features> option specifies additional rule features that should
be executed.  Some rules are turned off by default because they are
more intensive or require a live network connection, for instance.
Use the I<--features> flag to turn them on.  The LIST argument should be
a comma-separated list.  Example usage:

  --features live,nsec_check

Features available in the default rule set distributed with B<donuts>:

=over

=item live

The I<live> feature allows rules that need to perform live DNS queries
to run.  Most of these I<live> rules query parent and children of the
current zone, when appropriate, to see that the parent/child
relationships have been built properly.  For example, if you have a
DS record which authenticates the key used in a child zone the I<live>
feature will let a rule run which checks to see if the child is
actually publishing the DNSKEY that corresponds to the test zone's DS
record.

=item nsec_check

This checks all the NSEC or NSEC3 records (as appropriate for the
zone) to ensure the chain is complete and that no-overlaps exist.  It
is fairly memory- and cpu-intensive in large zones.

=back

=back

=head2 Configuration File Options:

=over

=item -c I<CONFIGFILE>

=item --config-file=I<CONFIGFILE>

Parse a configuration file to change constraints specified by rules.
This defaults to B<$HOME/.donuts.conf>.

=item -C

=item --no-config

Don't read user configuration files at all, such as those specified by
the I<-c> option or the B<$HOME/.donuts.conf> file.

=back

=head2 Extra Live Query Options:

Live Queries are enabled through the use of the I<-f live> arguments.
These options are only useful if that feature has been enabled.

=over

=item -t I<INTERFACE>

=item --tcpdump-capture=I<INTERFACE>

Specifies that B<tcpdump> should be started on I<INTERFACE> (e.g.,
"eth0") just before B<donuts> begins its run of rules for each domain
and will stop it just after it has processed the rules.  This is
useful when you wish to capture the traffic generated by the I<live>
feature, described above.

=item -T I<FILTER>

=item --tcpdump-filter=I<FILTER>

When B<tcpdump> is run, this I<FILTER> is passed to it for purposes of
filtering traffic.  By default, this is set to I<port 53 || ip[6:2] &
0x1fff != 0>, which limits the traffic to traffic destined to port 53
(DNS) or fragmented packets.

=item -o I<FILE>

=item --tcpdump-output-file=I<FILE>

Saves the B<tcpdump>-captured packets to I<FILE>.  The following
special fields can be used to help generate unique file names:

=over

=item %d

This is replaced with the current domain name being analyzed (e.g.,
"example.com").

=item %t

This is replaced with the current epoch time (i.e., the number of
seconds since Jan 1, 1970).

=back

This field defaults to I<%d.%t.pcap>.

=item --show-gui

[alpha code]

Displays a browsable GUI screen showing the results of the B<donuts> tests.

The B<QWizard> and B<Gtk2> Perl modules must be installed for this to work.

=back

=head2 Help Options

=over

=item -H

Displays the personal configuration file rules and tokens that are
acceptable in a configuration file.  The output will
consist of a rule name, a token, and a description of its meaning.

Your configuration file (e.g., B<$HOME/.donuts.conf>) may have lines in it
that look like this:

  # change the default minimum number of legal NS records from 2 to 1
  name: DNS_MULTIPLE_NS
  minnsrecords: 1

  # change the level of the following rule from 8 to 5
  name: DNS_REASONABLE_TTLS
  level: 5

This allows you to override certain aspects of how rules are executed.

=item -R

Displays a list of all known rules along with their description (if
available).

=item -h

Displays a help message.

=item --help

Displays a help message more tailored to people who prefer long-style
options.

=item -q

Turns on a quieter output mode where only the errors and warnings are
shown.  IE, the summary line of "N errors found ..." is not shown.

-q is ignored if a -v argument is present; the -v argument requests a
longer output summary and thus it doesn't make sense to use them both
at the same time.

=item -v

Turns on more verbose output.  Multiple I<-v>'s will turn on increasing
amounts of output.  The number of -v's will dictate output:

=over

=item 1

Describes which rules are being loaded and extra detail for rules that found errors (rule Level and extra text detail)

=item 2

Even more detail about rules that found errors: file name, file line
number, rule type.

=item 3

Shows extra detail on the record text being analyzed (the detail is
not always available, however).

=item 4

Even more detail about rules that found errors: dumps the rule code itself.

=item 5

Even more detail about rules that found errors: dumps the internal
rule structure.

=back

=back

=head2 Obsolete Options

=over

=item -L

Obsolete command line option.  Please use I<--features live> instead.

=back

=head1 EXAMPLES

Run B<donuts> in its default mode on the I<example.com> zone which is
contained in the B<db.example.com> file:

  % donuts db.example.com example.com

Run B<donuts> with significantly more output, both in terms of verbosity
and in terms of the number of rules that are run to analyze the file:

  % donuts -v -v --level 9 db.example.com example.com

=head1 COPYRIGHT

Copyright 2004-2012 SPARTA, Inc.  All rights reserved.
See the COPYING file included with the DNSSEC-Tools package for details.

=head1 AUTHOR

Wes Hardaker <hardaker@users.sourceforge.net>

=head1 SEE ALSO

For more information on the dnssec-tools project:

  http://www.dnssec-tools.org/

For writing rules that can be loaded by B<donuts>:

  B<Net::DNS::SEC::Tools::Donuts::Rule>, 

General DNS and DNSSEC usage:

  B<Net::DNS>, B<Net::DNS::SEC>

=cut
