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


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;

######################################################################
# 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");

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

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',"1.1\nDNSSEC-Tools Version: 1.5"],

		['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 ouptut
	    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 convience 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 [-h] [-H] [-v] [-l LEVEL] [-r RULEFILES] [-i IGNORELIST]
         [-C] [-c configfile] 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.

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

=head1 OPTIONS

=over

=item -h

Displays a help message.

=item -v

Turns on more verbose output.  Multiple -v's will turn on increasing
amounts of output.

=item -q

Turns on more quiet output.

=item -l 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>

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 /usr/local/share/dnssec-tools/donuts/rules/*.txt and
$HOME/.dnssec-tools/donuts/rules/*.txt .

=item -i 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 -L

Include rules that require live queries of data.  Generally, these
rules concentrate on pulling remote DNS data to test;
for example, parent/child zone relationships.

=item -c I<CONFIGFILE>

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

=item -C

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

=item -t I<INTERFACE>

Specifies that B<tcpdump> should be started on I<INTERFACE> (e.g.,
"eth0") just before 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>

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>

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 -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 -F LIST

=item --features=LIST

The --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 --features flag to turn them on.  The LIST argument should be
a comma separated list.  Example usage:

  --features live,data_check

Features available in the default rule set:

=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.

=back

=item --show-gui

[alpha code]

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

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

=item --live

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

=back

=head1 COPYRIGHT

Copyright 2004-2009 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 donuts:

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

General DNS and DNSSEC usage:

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

=cut
