diff options
author | Sergey Poznyakoff <gray@gnu.org.ua> | 2012-07-07 21:00:00 +0300 |
---|---|---|
committer | Sergey Poznyakoff <gray@gnu.org.ua> | 2012-07-07 21:06:59 +0300 |
commit | 70ca1af7054fbee093ba2029e0989052cc993b19 (patch) | |
tree | cc6bbe4e5312221f63d401e3d2ba5876bb4ed8f8 /axfr2acl | |
parent | d39b0783d77ba756681c539b3ee19b8997494317 (diff) | |
download | dnstools-70ca1af7054fbee093ba2029e0989052cc993b19.tar.gz dnstools-70ca1af7054fbee093ba2029e0989052cc993b19.tar.bz2 |
New program: axfr2acl
* Makefile (SUBDIRS): Add axfr2acl
(dist): Fix rule.
* axfr2acl/.gitignore: New file.
* axfr2acl/GNUmakefile: New file.
* axfr2acl/Makefile.PL: New file.
* axfr2acl/axfr2acl: New file.
* rpsl2acl/rpsl2acl: Fix docs.
Diffstat (limited to 'axfr2acl')
-rw-r--r-- | axfr2acl/.gitignore | 1 | ||||
-rw-r--r-- | axfr2acl/GNUmakefile | 2 | ||||
-rw-r--r-- | axfr2acl/Makefile.PL | 15 | ||||
-rwxr-xr-x | axfr2acl/axfr2acl | 540 |
4 files changed, 558 insertions, 0 deletions
diff --git a/axfr2acl/.gitignore b/axfr2acl/.gitignore new file mode 100644 index 0000000..f3c7a7c --- /dev/null +++ b/axfr2acl/.gitignore @@ -0,0 +1 @@ +Makefile diff --git a/axfr2acl/GNUmakefile b/axfr2acl/GNUmakefile new file mode 100644 index 0000000..e61b3c3 --- /dev/null +++ b/axfr2acl/GNUmakefile @@ -0,0 +1,2 @@ +include ../Make.vars +include ../Make.rules diff --git a/axfr2acl/Makefile.PL b/axfr2acl/Makefile.PL new file mode 100644 index 0000000..2ad8d9a --- /dev/null +++ b/axfr2acl/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' => 'axfr2acl', + 'FIRST_MAKEFILE' => 'Makefile', + 'VERSION' => '1.00', + 'EXE_FILES' => [ 'axfr2acl' ], + 'PREREQ_PM' => { 'Getopt::Long' => 2.34, + 'IO' => 1.2301, + 'Digest::MD5' => 2.50, + 'Net::DNS' => 0.66 } +); diff --git a/axfr2acl/axfr2acl b/axfr2acl/axfr2acl new file mode 100755 index 0000000..32b4ad3 --- /dev/null +++ b/axfr2acl/axfr2acl @@ -0,0 +1,540 @@ +#! /usr/bin/perl +# Copyright (C) 2012 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 Socket; +use Digest::MD5; +use Net::DNS; +use Net::CIDR; +use Pod::Usage; +use Pod::Man; +use POSIX qw(strftime); + +# Global vars +my $sys_config_file = "/etc/axfr2acl.conf"; # Configuration file name +my $descr = "create a BIND-style ACL containing all A records from a zone"; +my $script; # This script name. +my %debug_level = ( 'GENERAL' => 0, 'DNS' => 0); +my @netlist; +my @oldlist; + +# Options: +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 @zones; +my $outfile = "netlist"; +my $aclname; # Name of a bind-style ACL +my $comment; # Initial comment line +my $update; +my %oldserial; # Old serial numbers +my %serial; # Zone serial numbers + +# "Initial" are networks supplied by "--add-network" and "--from-file" options. +my $initnetctx; # MD5 context for tracking changes in initial networks +my $oldsig; # Old MD5 signature of the initial networks +my $netsig; # Current MD5 signature of the initial networks + +# Return codes: +# 0 - OK, nothing changed +# 1 - OK, list updated +# 2 - General error +# 3 - Usage error + +############# + +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 abend($) { + my $msg = shift; + logit($msg); + debug('GENERAL', 1, "ABEND"); + logdone(); + exit(2); +} + +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, "--$_"); + } +} + +sub note_init_network($) { + $initnetctx->add($_); +} + +sub networks_from_file($) { + my ($file) = @_; + open(FILE, "<", $file) + or abend("Cannot open file $file for reading"); + while (<FILE>) { + chomp; + s/^\s+//; + s/\s+$//; + s/;$//; + s/#.*//; + next if ($_ eq ""); + note_init_network($_); + @netlist = Net::CIDR::cidradd($_,@netlist); + } + close(FILE); +} + +sub read_acl($) { + my ($file) = @_; + open(FILE, "<", $file) or return; + my $line=1; + my $zone; + my $sn; + while (<FILE>) { + chomp; + s/^\s+//; + s/\s+$//; + s/;$//; + if (($zone, $sn) = ($_ =~ /^#serial\s+(\S+)\s+([0-9]+)/)) { + $oldserial{$zone} = $sn; + next; + } + if (($sn) = ($_ =~ /^#netsig\s+(\S+)/)) { + $oldsig = $sn; + } + s/#.*//; + next if ($_ eq ""); + next if /^acl/; + next if /}/; + abend("$file:$line: invalid CIDR: $_") unless (Net::CIDR::cidrvalidate($_)); + @oldlist = Net::CIDR::cidradd($_,@oldlist); + $line++; + } + sort @oldlist; + close(FILE); +} + +########### +($script = $0) =~ s/.*\///; + +my $home; + +eval { + my @ar = getpwuid($<); + $home = $ar[7]; +}; + +if ($ENV{'AXFR2ACL_CONF'}) { + read_config_file($ENV{'AXFR2ACL_CONF'}); +} elsif (-e "$home/.axfr2acl.conf") { + read_config_file("$home/.axfr2acl.conf"); +} elsif (-e "$sys_config_file") { + read_config_file("$sys_config_file"); +} + +$initnetctx = Digest::MD5->new; + +GetOptions("help|h" => \$help, + "man" => \$man, + "dry-run|n" => \$dry_run, + "debug|d:s" => sub { + if (!$_[1]) { + foreach my $key (keys %debug_level) { + $debug_level{$key} = 1; + } + } elsif ($_[1] =~ /^[0-9]+/) { + 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 { + abend("no such category: $s[0]"); + } + } + } + }, + "log-file|l=s" => \$logfile, + "outfile|o=s" => \$outfile, + "acl=s" => \$aclname, + "comment=s" => \$comment, + "add-network=s" => sub { + foreach my $cidr (split(/,/, $_[1])) { + note_init_network($cidr); + @netlist = Net::CIDR::cidradd($cidr,@netlist); + } + }, + "from-file|T=s" => sub { + networks_from_file($_[1]); + }, + "zones|z=s" => sub { + foreach my $rs (split(/,/, $_[1])) { + push(@zones,$rs); + } + }, + "update|u" => \$update + ) or exit(3); + +pod2usage(-message => "$script: $descr", -exitstatus => 0) if $help; +pod2usage(-exitstatus => 0, -verbose => 2) if $man; + +loginit(); +debug('GENERAL', 1, "startup"); + +abend("No zones given") if ($#zones == -1); + +$netsig = $initnetctx->b64digest; +read_acl($outfile) if ($update); + +# Determine initial update status: +# The output needs to be updated if either the --update flag is *not* +# given (i.e. the user wants unconditional update), or if the MD5 signatures +# of added network differ. +# +# The initial update status will be corrected in the loop below, based on the +# SOA of the networks involved. +# +my $need_update = !$update; +if (!$need_update) { + $need_update = $netsig ne $oldsig; + debug('GENERAL', 1, + "update forced because initial networks changed") if ($need_update); +} + +# Create resolvers and collect serial numbers +my %resolver; + +foreach my $zone (@zones) { + my $res = Net::DNS::Resolver->new; + debug('DNS', 1, "querying SOA for $zone"); + my $query = $res->query($zone, "SOA"); + unless ($query) { + print STDERR "$script: cannot get SOA of $zone: " . $res->errorstring . "\n"; + next; + } + $resolver{$zone} = $res; + my $rr = (grep { $_->type eq 'SOA' } $query->answer)[0]; + debug('DNS', 2, "zone $zone serial ".$rr->serial); + $need_update = 1 + if ($update && (!defined($oldserial{$zone}) || + $oldserial{$zone} < $rr->serial)); + delete $oldserial{$zone}; + $serial{$zone} = $rr->serial; +} + +if ($update and keys(%oldserial)) { + debug('GENERAL', 1, "some zones removed: forcing update"); + $need_update = 1; +} + +if ($need_update) { + foreach my $zone (@zones) { + my $res; + my $rr; + + $res = $resolver{$zone}; + next unless ($res); + + debug('DNS', 1, "querying NSs for $zone"); + my $query = $res->query($zone, "NS"); + unless ($query) { + print STDERR "$script: cannot get NS records for $zone: " . + $res->errorstring . "\n"; + next; + } + + foreach $rr (grep { $_->type eq 'NS' } $query->answer) { + $res->nameservers($rr->nsdname); + debug('DNS', 2, "$zone NS ". $rr->nsdname); + } + + debug('DNS', 1, "Transferring $zone"); + my @records = grep { $_->type eq 'A' } $res->axfr($zone); + debug('DNS', 1, "Got $#records records"); + + foreach my $rr (@records) { + @netlist = Net::CIDR::cidradd($rr->address,@netlist); + } + } +} + +if (!$need_update) { + debug('GENERAL', 1, "shutdown: list unchanged"); + logdone(); + exit(1); +} + +sort @netlist; + +if ($update) { + my %oldset = map { $_ => $_ } @oldlist; + $update = 0; + foreach my $net (@netlist) { + if (!$oldset{$net}) { + $update = 1; + last; + } else { + delete $oldset{$net}; + } + } + unless ($update or keys(%oldset) > 0) { + if ($need_update && !$dry_run) { + debug('GENERAL', 1, "list unchanged; proceeding to save serials"); + } else { + debug('GENERAL', 1, "shutdown: list unchanged"); + logdone(); + exit(1); + } + } +} + +if ($dry_run) { + print join("\n",@netlist)."\n"; +} else { + my $file; + my $indent = ""; + + debug('GENERAL',1,"writing output file $outfile"); + + open($file, ">", $outfile) or + abend("cannot open $outfile for writing: $!"); + if ($comment) { + foreach my $line (split(/\n/, $comment)) { + print $file "# $line\n"; + } + } + print $file strftime "# network list created by $script on %c\n", + localtime; + foreach my $zone (keys %serial) { + print $file "#serial $zone $serial{$zone}\n"; + } + print $file "#netsig $netsig\n" if ($netsig); + + if (defined($aclname)) { + print $file "acl $aclname {\n"; + $indent = "\t"; + } + foreach my $cidr (@netlist) { + print $file "${indent}${cidr};\n"; + } + print $file "};\n" if (defined($aclname)); + close($file); +} + +debug('GENERAL', 1, "shutdown"); +logdone(); + +########### + +__END__ +=head1 NAME + +axfr2acl - create a BIND ACL containing "A" records from a set of zones + +=head1 SYNOPSIS + +axfr2acl [I<options>] + +=head1 DESCRIPTION + +B<Axfr2acl> collects all B<A> records from a set of supplied DNS zones +and writes out a DNS ACL containing all of them. If possible, the +addresses are compressed into CIDRs. The resulting list is sorted +lexicographically. + +The resulting ACL is normally written to a file, either as a list of CIDRs +or as a BIND B<acl> statement, if the ACL name is given. In both cases, the +file is sutable for inclusion in the BIND configuration file. If the file +already exists when the command is invoked, its contents is recorded and +is used subsequently to determine whether it has changed. The utility will +actually modify the output file only if the constructed list differs from +the one it contained initially. It will also avoid running zone transfers +if the serial records of all involved zones did not change since the last +run. + +The program exits with code 0 if the file is up to date, 1 if it has +successfully updated the file, 2 if some error ocurred and 3 if the +command line usage was incorrect. + +=head1 OPTIONS + +The following option control the output: + +=over 4 + +=item B<--acl>=I<name> + +Format output as a B<bind> ACL statement with the given I<name>. + +=item B<--comment>=I<string> + +Print I<string> as the heading comment to the output. The argument can +consist of multiple lines. A C<#> sign will be printed before each of +them. + +=item B<--outfile>=I<FILE>, B<-o> I<FILE> + +Write the result to I<FILE>, instead of the default C<netlist>. + +=back + +The following options control the selection of DNS zones and initial +contents of the output list: + +=over 4 + +=item B<--add-network>=I<arg> + +Add given CIDRs to the output list. Argument is a comma-separated list +of CIDRs. + +=item B<--from-file>=I<FILE>, B<-T> I<FILE> + +Populate the output list with CIDRs read from I<FILE>. The file must +list each CIDR on a separate line. Empty lines and comments (introduced +by C<#> sign) are ignored. + +=item B<--zones>=I<zonelist>, B<-z> I<zonelist> + +Defines a list of zones to query. I<Zonelist> is a comma-separated list +of zone names. + +=back + +Options controlling log and debug output: + +=over 4 + +=item B<--log-file>=I<FILE>, B<-l> I<FILE> + +Write diagnostic output to I<FILE>, instead of standard error. + +=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> and C<DNS>. + +=item B<--dry-run>, B<-n> + +Don't create output file. Instead print the result on the standard +output. + +=back + +Informational options: + +=over 4 + +=item B<--help>, B<-h> + +Shows 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.> The file name given by C<AXFR2ACL_CONF> environment variable (if set) + +=item B<b.> B<~>/.axfr2acl.conf + +=item B<c.> /etc/axfr2acl.conf + +=back + +The first existing file from this list is used. It is an error, if the +B<$AXFR2ACL_CONF> variable is set, but points to a file that does not exist. +It is not an error, if B<$AXFR2ACL_CONF> is not set and neither of the two +remaining files exist. It is, however, an error if any of these file exists, +but is not readable. + +The configuration file uses a 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 the long command line options, but without the leading B<-->. +For example: + + zones = example.net,example.com + aclname = mynets + add-network = 10.0.0.0/8 + outfile = networks.inc + +=head1 ENVIRONMENT + +=over 4 + +=item AXFR2ACL_CONF + +The name of the configuration file to read, instead of the default +F</etc/axfr2acl.conf>. + +=back + +=head1 SEE ALSO + +B<rpsl2acl>(1). + +=head1 AUTHOR + +Sergey Poznyakoff <gray@gnu.org> + +=cut + |