diff options
author | Sergey Poznyakoff <gray@gnu.org.ua> | 2012-03-26 11:55:58 +0300 |
---|---|---|
committer | Sergey Poznyakoff <gray@gnu.org.ua> | 2012-03-26 11:58:25 +0300 |
commit | 9d6e6d1c7bb737da4b9bef81d612ffa702ab2795 (patch) | |
tree | 8b2ec7c629d2cd883bc04080447f16b4df8c2bc9 /dnsdbck | |
download | dnstools-9d6e6d1c7bb737da4b9bef81d612ffa702ab2795.tar.gz dnstools-9d6e6d1c7bb737da4b9bef81d612ffa702ab2795.tar.bz2 |
Initial commit.
Diffstat (limited to 'dnsdbck')
-rw-r--r-- | dnsdbck/MANIFEST | 3 | ||||
-rw-r--r-- | dnsdbck/Makefile.PL | 15 | ||||
-rwxr-xr-x | dnsdbck/dnsdbck | 785 |
3 files changed, 803 insertions, 0 deletions
diff --git a/dnsdbck/MANIFEST b/dnsdbck/MANIFEST new file mode 100644 index 0000000..fd61d5c --- /dev/null +++ b/dnsdbck/MANIFEST @@ -0,0 +1,3 @@ +MANIFEST +Makefile.PL +dnsdbck diff --git a/dnsdbck/Makefile.PL b/dnsdbck/Makefile.PL new file mode 100644 index 0000000..4e54f1b --- /dev/null +++ b/dnsdbck/Makefile.PL @@ -0,0 +1,15 @@ +# -*- perl -*- +use ExtUtils::MakeMaker; + +# See lib/ExtUtils/MakeMaker.pm for details of how to influence +# the contents of the Makefile that is written. +WriteMakefile( + 'NAME' => 'dnsdbck', + 'FIRST_MAKEFILE' => 'Makefile', + 'VERSION' => '1.00', + 'EXE_FILES' => [ 'dnsdbck' ], + 'PREREQ_PM' => { 'Getopt::Long' => 2.34, + 'DBI' , => 1.607, + 'Net::DNS' => 0.66, + 'Net::CIDR' => 0.14 } +); diff --git a/dnsdbck/dnsdbck b/dnsdbck/dnsdbck new file mode 100755 index 0000000..cf741ce --- /dev/null +++ b/dnsdbck/dnsdbck @@ -0,0 +1,785 @@ +#! /usr/bin/perl +# Copyright (C) 2011 Sergey Poznyakoff <gray@gnu.org> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +use strict; +use Getopt::Long qw(:config gnu_getopt no_ignore_case); +use DBI; +use POSIX qw(strftime setuid setgid); +use File::Temp qw(tempfile); +use Socket; +use Net::DNS; +use Net::CIDR; +use Pod::Usage; +use Pod::Man; + +# Options: +my $sys_config_file = "/etc/dnsdbck.conf"; # Configuration file name +my $debug; # Debug mode indicator. +my $logfile; # Name of the logfile. +my $dry_run; # Dry-run mode. +my $help; # Show help and exit. +my $man; # Show man and exit. +my $my_cnf; # Name of the mysql option file. +my $all_zones; # Process all zones. +my $author; # Author name for INSERT statements. + +my $username; + +my $sqlfilename; # Name of the output SQL file. +my $sqlfile; # Its handle. + +# Global vars: +my $descr = "check and repair the DNS database"; +my $script; # This script name. + +my %debug_level = ( 'GENERAL' => 0, + 'SQL' => 0, + 'DNS' => 0, + 'MISSING' => 0 ); + +# FIXME: these three should be configurable too +my %ns_name = ( 'ns1.farlep.net' => 1, 'ns3.farlep.net' => 1 ); +my $primary_ns = 'ns1.farlep.net'; +my $responsible_person = 'hostmaster.ns1.farlep.net'; + +my @create_soa_allow_list; # list of CIDRs for which creating SOA records + # is allowed. + +my $dbd; # Database connection descriptor. + +our $sys_dbname; +our $sys_dbhost; +our $sys_dbport; +our $sys_dbuser; +our $sys_dbpasswd; +our $sys_dbparams; + +my %ignored_zone; +my %ignored_host; +my %nosoa_warning; # Used to avoid duplicate 'no SOA' warnings. +## + +sub logit { + print LOG "@_\n"; +} + +sub loginit { + if ($logfile and (!-e $logfile or -w $logfile)) { + print STDERR "$script: logging to $logfile\n"; + open(LOG, ">$logfile"); + } else { + open(LOG, ">&STDERR"); + } +} + +sub logdone { +} + +sub debug { + my $category = shift; + my $level = shift; +# print STDERR "$category: $debug_level{$category} >= $level\n"; + if ($debug_level{$category} >= $level) { + print LOG "$script: DEBUG[$category]: @_\n"; + } +} + +sub read_config_file($) { + my $config_file = shift; + print STDERR "reading $config_file\n" if ($debug); + open(FILE, "<", $config_file) or die("cannot open $config_file: $!"); + while (<FILE>) { + chomp; + s/^\s+//; + s/\s+$//; + s/\s+=\s+/=/; + s/#.*//; + next if ($_ eq ""); + unshift(@ARGV, "--$_"); + } +} + +## Returns a hash of lists of entries +# arg0 : table +# arg1 : criterion +# arg2 : fields to show (* for all) +sub GetDBHashArray { + my $table = $_[0]; + my $criterion; + my $fields = "*"; + my $hop; + my @ret; + + $criterion = "WHERE ".$_[1] if $_[1]; + $fields = $_[2] if $_[2]; + + my $query = "SELECT ".$fields." FROM ".$table." ".$criterion; + debug('SQL', 2, "Query $query"); + $hop = $dbd->prepare($query); + $hop->execute; + while (my $ref = $hop->fetchrow_hashref) { + push(@ret, $ref); + } + $hop->finish; + return @ret; +} + +## Returns a list of entries +# arg0 : table +# arg1 : criterion +# arg2 : fields to show (* for all) +sub GetDB { + my $table = $_[0]; + my $criterion; + my $fields = "*"; + my $hop; + my @ret; + + $criterion = "WHERE ".$_[1] if $_[1]; + $fields = $_[2] if $_[2]; + + my $query = "SELECT ".$fields." FROM ".$table." ".$criterion; + debug('SQL', 2, "Query $query"); + $hop = $dbd->prepare($query); + $hop->execute; + while (my (@line) = $hop->fetchrow_array) { + push(@ret, join(",", map {defined $_ ? $_ : "0"} @line)); + + } + $hop->finish; + return @ret; +} + +sub matches_direct($) { + my $arg = $_[0]; + return 1 if ($#ARGV == -1); + foreach my $zone (@ARGV) { + return 1 if (substr($arg, - (length($zone) + 1)) eq "$zone."); + } + return 0; +} + +my %soa_records; +sub has_soa($) { + my $zone = $_[0]; + $zone =~ s/\.$//; + unless (defined($soa_records{$zone})) { + my @soa = GetDB("dns_soa", + "type='SOA' AND zone='".$zone."'"); + $soa_records{$zone} = ($#soa >= 0); + } + return $soa_records{$zone}; +} + +sub build_insert_query($$$$$$) { + my ($zone, $host, $type, $data, $view, $ttl) = @_; + return sprintf("INSERT INTO dns_records (author,zone,host,type,data,view,ttl)" + . " VALUES('%s','%s','%s','%s','%s','%s','%s')", + $author, + $zone, $host, $type, $data, $view, $ttl); +} + +sub read_from_file($%) { + my ($file, $hashref) = @_; + open(FILE, "<", $file) + or die("Cannot open file $file for reading"); + while (<FILE>) { + chomp; + s/^\s+//; + s/\s+$//; + s/#.*//; + next if ($_ eq ""); + $hashref->{$_} = 1; + } + close(FILE); +} + +sub build_delete_query($) { + my ($serial_id) = @_; + return "DELETE FROM dns_records WHERE serial=$serial_id"; +} + +sub build_soa($$) { + my ($key,$view) = @_; + + my $query = sprintf("INSERT INTO dns_soa (zone,type,data,serial,resp_person,view) VALUES('%s','SOA','%s.','%s','%s.','%s');\n", + $key, $primary_ns, + strftime('%Y%m%d00', localtime), + $responsible_person, + $view); + foreach my $ns (keys %ns_name) { + $query .= sprintf("INSERT INTO dns_soa (zone,type,data,view) VALUES('%s','NS','%s.','%s');\n", $key, $ns, $view); + } + return $query; +} +####### + +my $resolver; + +sub check_zone_ns($) { + my ($zone) = @_; + $resolver = Net::DNS::Resolver->new if (!$resolver); + debug('DNS', 1, "querying NSs for $zone"); + my $query = $resolver->query($zone, "NS"); + if ($query) { + foreach my $rr (grep { $_->type eq 'NS' } $query->answer) { + debug('DNS', 2, "$zone: ns=".$rr->nsdname); + return 1 if ($ns_name{$rr->nsdname}); + } + } else { + debug('DNS', 1, "query failed: ", $resolver->errorstring); + return 2 if (($resolver->errorstring eq "NXDOMAIN") or + ($resolver->errorstring eq "NOERROR")); + return -1; + } + debug('DNS', 1, "$zone: not ours"); + return 0; +} + +sub check_host_name($$) { + my ($addr, $name) = @_; + $name =~ s/\.$//; + $resolver = Net::DNS::Resolver->new if (!$resolver); + debug('DNS', 1, "matching PTRs for $addr against $name"); + my $query = $resolver->query($addr, "PTR"); + if ($query) { + foreach my $rr (grep { $_->type eq 'PTR' } $query->answer) { + my $s; + debug('DNS', 2, "$addr: ptr=".$rr->name."; string=".$rr->rdatastr); + ($s = $rr->rdatastr) =~ s/\.$//; + return 1 if ($s eq $name); + return 2 if ((length($s) > length($name) + and substr($s, - (length($name) + 1)) eq ".$name") or + (length($name) > length($s) + and substr($name, - (length($s) + 1)) eq ".$s")) + } + } else { + debug('DNS', 1, "query failed: ", $resolver->errorstring); + return -2 if ($resolver->errorstring eq "NXDOMAIN"); + return -1; + } + debug('DNS', 1, "$addr: no match"); + return 0; +} + +my @private_network_cidr_list; + +sub private_network($) { + my $arg = shift; + + if ($#private_network_cidr_list == -1) { + @private_network_cidr_list = + Net::CIDR::cidradd("10.0.0.0/8", @private_network_cidr_list); + @private_network_cidr_list = + Net::CIDR::cidradd("172.16.0.0/12", @private_network_cidr_list); + @private_network_cidr_list = + Net::CIDR::cidradd("192.168.0.0/16", @private_network_cidr_list); + } + if ($arg =~ /.*\.in-addr\.arpa$/) { + my @octet = split(/\./, $arg); + $arg = "$octet[2].$octet[1].$octet[0].0/24"; + } + return Net::CIDR::cidrlookup($arg, @private_network_cidr_list); +} + +# Return true if creating SOA record for reverse $zone is allowed. +sub create_soa_allowed($) { + my $arg = shift; + + if ($arg =~ /.*\.in-addr\.arpa$/) { + my @octet = split(/\./, $arg); + $arg = "$octet[2].$octet[1].$octet[0].0/24"; + } + return Net::CIDR::cidrlookup($arg, @create_soa_allow_list); +} + +############# + +($script = $0) =~ s/.*\///; + +my $home; + +eval { + my @ar = getpwuid($<); + $home = $ar[7]; +}; + +if ($ENV{'DNSDBCK_CONF'}) { + read_config_file($ENV{'DNSDBCK_CONF'}); +} elsif (-e "$home/.dnsdbck.conf") { + read_config_file("$home/.dnsdbck.conf"); +} elsif (-e "$sys_config_file") { + read_config_file("$sys_config_file"); +} + +GetOptions("help|h" => \$help, + "man" => \$man, + "debug|d:s" => sub { + if (!$_[1]) { + foreach my $key (keys %debug_level) { + $debug_level{$key} = 1; + } + } else { + foreach my $cat (split(/,/, $_[1])) { + my @s = split(/[:=]/, $cat, 2); + $s[0] =~ tr/[a-z]/[A-Z]/; + if (defined($debug_level{$s[0]})) { + $debug_level{$s[0]} = + ($#s == 1) ? $s[1] : 1; + } else { + print STDERR "$script: no such category: $s[0]\n"; + exit(1); + } + } + } + }, + "create-reverse-soa=s" => sub { + foreach my $cidr (split(/,/, $_[1])) { +# unless (Net::CIDR::cidrvalidate($cidr)) { +# print STDERR "$script: invalid CIDR: $cidr\n"; +# exit(1); +# } + @create_soa_allow_list = + Net::CIDR::cidradd($cidr, @create_soa_allow_list); + } + }, + "dry-run|n" => \$dry_run, + "my-cnf|c=s" => \$my_cnf, + "author|a=s" => \$author, + "outfile|o=s" => \$sqlfilename, + "all|A" => \$all_zones, + "ignore-zone=s" => sub { + foreach my $zone (split(/,/, $_[1])) { + $ignored_zone{$zone} = 1; + } + }, + "ignore-zones-from=s" => sub { + read_from_file($_[1], \%ignored_zone); + }, + "ignore-host=s" => sub { + foreach my $host (split(/,/, $_[1])) { + $ignored_host{$host} = 1; + } + }, + "ignore-hosts-from=s" => sub { + read_from_file($_[1], \%ignored_host); + }, + "log-file|l=s" => \$logfile, + "user-name|u=s" => \$username + ) or exit(1); + +pod2usage(-message => "$script: $descr", -exitstatus => 0) if $help; +pod2usage(-exitstatus => 0, -verbose => 2) if $man; + +if ($username) { + my ($login,$pw,$uid,$gid,$quota,$comment,$gecos,$dir) = getpwnam($username) + or die("no such user: $username"); + setgid($gid); + setuid($uid); + die("cannot switch to $username privileges") + unless ($< == $uid and $( == $gid); + $home = $dir; +} + +unless (defined($author)) { + my ($login) = getpwuid($<); + $author = $login; +} + +if ($#ARGV == -1 and !$all_zones) { + print STDERR "$script: no zones specified, try `$script --help' for more info\n"; + exit(1); +} + +if (defined($sqlfilename)) { + $dry_run = 1; + open($sqlfile, "+>", $sqlfilename) or + die("cannot open file $sqlfilename: $!"); + print $sqlfile strftime "-- SQL script generated by $script on %c.\n", + localtime; +} else { + ($sqlfile, $sqlfilename) = tempfile(UNLINK => 1); +} + +loginit(); + +debug('SQL', 1, "connecting to the database"); + +my $arg = ""; +$arg .= ":database=$sys_dbname" if ($sys_dbname); +$arg .= ":host=$sys_dbhost" if ($sys_dbhost); +$arg .= ":port=$sys_dbport" if ($sys_dbport); +$arg .= ":$sys_dbparams" if ($sys_dbparams); + +if (!$arg) { + $my_cnf = "$home/.my.cnf" if (!$my_cnf); + debug('SQL', 1, "using mysql option file $my_cnf"); + $arg = ":;mysql_read_default_file=$my_cnf"; +} +$arg = 'DBI:mysql'.$arg; + +$dbd = DBI->connect($arg, + $sys_dbuser, $sys_dbpasswd, + { RaiseError => 1, AutoCommit => 1}) or exit(1); + +my $criterion = "type='A'"; +if ($#ARGV >= 0) { + $criterion .= " AND zone IN (" + . join(',', map { s/[\\\"\']/\\$&/g; "'".$_."'" } @ARGV) + . ")"; +} + +my %reverse_zone; +my $count; +my $insert_count = 0; +my $delete_count = 0; +debug('GENERAL', 1, "collecting reverse zones"); +foreach my $ref (GetDBHashArray("dns_records", + $criterion, + "zone,host,data,view,ttl,serial")) { + my %row = %{$ref}; + $row{'data'} =~ s/\s*$//; + $row{'host'} =~ s/\s*$//; + my @octets = split(/\./, $row{'data'}); + my $rev = "$octets[2].$octets[1].$octets[0].in-addr.arpa"; + debug('GENERAL', 10, "reverse zone $rev"); + if (!$reverse_zone{$rev}) { + $reverse_zone{$rev} = {}; + } + my $dirzone; + if ($row{'host'} eq "@") { + $dirzone = $row{'zone'}."."; + } else { + $dirzone = $row{'host'}.".".$row{'zone'}."."; + } + $reverse_zone{$rev}{$octets[3]} = [$dirzone, $row{'view'}, $row{'ttl'}, $row{'serial'}]; + ++$count; +} +debug('GENERAL', 1, + "collected $count addresses in " . keys(%reverse_zone) . " reverse zones"); + + +REVLOOP: +while (my ($key, $value) = each(%reverse_zone)) { + my %revhash = %{$value}; + + debug('GENERAL', 2, + "checking reverse zone $key (".keys(%revhash)." hosts)"); + + next if ($ignored_zone{$key}); + unless (has_soa($key)) { + if (private_network($key) and create_soa_allowed($key)) { + debug('GENERAL', 1, "creating SOA/NS for $key"); + my $query = build_soa($key, 'internal'); + print $sqlfile "$query"; + } else { + while (my ($host, $val) = each(%revhash)) { + my @ar = @{$val}; + my $hostname = $ar[0]; + + my $canon_hostname; + ($canon_hostname = $hostname) =~ s/\.$//; + next if ($ignored_host{$canon_hostname}); + $canon_hostname =~ s/[^\.]+\.//; + next if ($ignored_zone{$canon_hostname}); + + my $fullkey = "$host.$key"; + my $msg = "$ar[3]: $hostname => $fullkey: no corresponding reverse record and no SOA for $key in the database"; + my $rc = check_host_name($fullkey, $hostname); + if ($rc == 1) { + debug('GENERAL', 2, "$msg, but resolve is OK"); + } elsif ($rc == 2) { + debug('GENERAL', 2, "$msg, but resolves to a subdomain, so OK"); + } elsif ($rc == 0) { + logit("$msg, but the reverse resolves"); + } else { + logit($msg); + } + } + next; + } + } + + $criterion = "zone='$key' AND type='PTR'"; + DBLOOP: + foreach my $ref (GetDBHashArray("dns_records", + $criterion, + "host,data,view,ttl,serial")) { + my %row = %{$ref}; + next unless matches_direct($row{'data'}); + + if (defined($revhash{$row{'host'}})) { + delete $revhash{$row{'host'}}; + } else { + my $fullkey = $row{'host'}.".".$key; + my $revhost; + ($revhost = $row{'data'}) =~ s/\.$//; + next if ($ignored_host{$revhost} or + $ignored_zone{$revhost} or + $ignored_host{$fullkey}); + debug('MISSING', 1, + $row{'serial'}.": $fullkey => " + .$row{'data'} + ." direct record missing"); + + my $dir; + my $pref = ""; + ($dir = $row{'data'}) =~ s/[^\.]+\.//; + $dir =~ s/\.$//; + next if ($ignored_zone{$dir}); + + unless (has_soa($revhost) or has_soa($dir)) { + my $msg = $row{'serial'} + .": $fullkey => " + .$row{'data'} + .": no SOA for $dir in the database"; + + my ($name,$aliases,$addrtype,$length,@addrs) + = gethostbyname($row{'data'}); + if (defined(@addrs)) { + foreach my $addr (@addrs) { + my @oct = split(/\./, inet_ntoa($addr)); + my $revaddr = "$oct[3].$oct[2].$oct[1].$oct[0].in-addr.arpa"; + debug('DNS', 2, "$row{'data'} resolves to $revaddr"); +# print STDERR "$revaddr eq $fullkey\n"; + if ($revaddr eq $fullkey) { + debug('GENERAL', 2, "$msg, but backresolve is OK"); + next DBLOOP; + } + } + } + + my $nsres = check_zone_ns($dir); + if ($nsres == 1) { + $msg .= ", but the zone is handled by our NSs"; + } elsif ($nsres == 0) { + $msg .= " and the zone is NOT handled by our NSs: deleted"; + my $query = build_delete_query($row{'serial'}); + print $sqlfile "$query;\n"; + $delete_count++; + logit($msg); + next; + } elsif ($nsres == 2) { + $msg .= " and the zone does not exist: deleted"; + my $query = build_delete_query($row{'serial'}); + print $sqlfile "$query;\n"; + $delete_count++; + logit($msg); + next; + } else { + $msg .= " (resolve error)"; + } + + logit($msg); + + $pref = "-- "; + } + + my @octets = split(/\./, $key); + my $query = build_insert_query($dir, + substr($row{'data'}, 0, + length($row{'data'}) + - length($dir)-2), + "A", + "$octets[2].$octets[1].$octets[0]." + .$row{'host'}, + $row{'view'}, + $row{'ttl'}); + print $sqlfile "$pref$query;\n"; + $insert_count++ unless ($pref); + } + } + + # Generate missing reverse entries + while (my ($host, $val) = each(%revhash)) { + my @ar = @{$val}; + debug('MISSING', 1, + "$ar[3]: $ar[0] => $host.$key: reverse record missing"); + my $query = build_insert_query($key, + $host, + "PTR", + $ar[0], + $ar[1], + $ar[2]); + print $sqlfile "$query;\n"; + $insert_count++; + } +} + +if ($insert_count + $delete_count) { + debug('GENERAL', 1, "updating database"); + unless ($dry_run) { + seek($sqlfile, 0, 0); + while (<$sqlfile>) { + next if (/^--/); + s/;$//; + debug('SQL', 2, "Query: $_\n"); + $dbd->do($_); + } + debug('GENERAL', 1, + "inserted: $insert_count, deleted: $delete_count records"); + } +} else { + debug('GENERAL', 1, "nothing to update in the database"); +} + +debug('SQL', 1, "closing database"); +$dbd->disconnect(); +logdone(); +debug('GENERAL', 1, "finished"); + +__END__ + +=head1 DNSDBCK + +dnsdbck - check and, if possible, repair the DNS database + +=head1 SYNOPSIS + +dnsdbck [I<options>] [B<zone>] [B<zone>...] + +=head1 DESCRIPTION + +B<Dnsdbck> checks the DNS database for consistency and repairs it, when +possible. For each record in a specified set of zones, it checks whether +a corresponding reverse exists. If it does not, B<dnsdbck> tries to +create it, if the corresponding C<in-addr.arpa> zone is present in the +database or if its IP address falls within CIDRs set using the +B<--create-reverse-soa> option (see below). In the latter case, +corresponding B<SOA> and B<NS> records will be created. + +=head1 OPTIONS + +=over 4 + +=item B<--all>, B<-a> + +Process all zones. When this option is used, any B<zone> arguments are +ignored. + +=item B<--create-reverse-soa>=I<cidr>,[I<cidr>...] + +Create SOA and NS records for reverse zones matching given CIDRs. + +=item B<--ignore-zone>=I<zone>[,I<zone>...] + +Ignore changes to the listed zones. + +=item B<--ignore-zone-from>=I<FILE> + +Read the list of zones to ignore from I<FILE>. The file format is: one +zone per line, UNIX comments and empty lines are ignored. + +=item B<--ignore-host>=I<host>[,I<host>...] + +Ignore changes to the listed hostnames. + +=item B<--ignore-host-from>=I<FILE> + +Read the list of hostnames to ignore from I<FILE>. The file format is: one +host per line, UNIX comments and empty lines are ignored. + +=item B<--my-cnf>=I<FILE>, B<-c> I<FILE> + +Use I<FILE> as MySQL options file. Default is "$HOME/.my.cnf". + +=item B<--author>=I<NAME>, B<-a> I<NAME> + +Use I<NAME> for the B<author> column. Default is your login name. + +=item B<--user-name>=I<NAME>, B<-u> I<NAME> + +Switch to the privileges of user I<NAME> after startup. + +=item B<--outfile>=I<FILE>, B<-o> I<FILE> + +Write SQL instructions to repair the database to I<FILE>. Implies +B<--dry-run>. + +=item B<--log-file>=I<FILE>, B<-l> I<FILE> + +Write diagnostic output to I<FILE>, instead of standard error. + +=item B<--dry-run>, B<-n> + +Do not try to repair the database. + +=item B<--debug>[=I<spec>[,I<spec>...]], B<-d>[I<spec>[,I<spec>...]] + +Set debugging level. I<spec> is either B<category> or B<category>=B<level>, +B<category> is a debugging category name and B<level> is a decimal +verbosity level. Valid categories are: C<GENERAL>, C<SQL>, C<DNS> and +C<MISSING> (all case-insensitive). If B<level> is not supplied, 1 is used +instead. + +=item B<--help>, B<-h> + +Show a terse help summary and exit. + +=item B<--man> + +Prints the manual page and exits. + +=back + +=head1 CONFIGURATION + +The program reads its configuration from one of the following locations: + +=over 4 + +=item B<a.> File name given by C<DNSDBCK_CONF> environment variable (if set) + +=item B<b.> B<~>/.dnsdbck.conf + +=item B<c.> /etc/dnsdbck.conf + +=back + +First of these files that exists is read. It is an error, if the +B<$DNSDBCK_CONF> variable is set, but points to a file that does not exist. +It is not an error if B<$DNSDBCK_CONF> is not set and neither of the two +remaining files exist. It is, however, an error if any of the file exists, +but is not readable. + +The configuration file uses usual UNIX configuration format. Empty +lines and UNIX comments are ignored. Each non-empty line is either an +option name, or option assignment, i.e. B<opt>=B<val>, with any amount of +optional whitespace around the equals sign. Valid option names are +the same as long command line options, but without the leading B<-->. +For example: + + all + ignore-zones-from = /etc/dns/zone.ignore + ignore-hosts-from = /etc/dns/host.ignore + my-cnf = /etc/dns/my.cnf + +=head1 ENVIRONMENT + +=over 4 + +=item DNSDBCK_CONF + +The name of the configuration file to read, instead of the default +F</etc/dnsdbck.conf>. + +=back + +=head1 AUTHOR + +Sergey Poznyakoff <gray@gnu.org> + +=cut |