aboutsummaryrefslogtreecommitdiff
path: root/axfr2acl
diff options
context:
space:
mode:
authorSergey Poznyakoff <gray@gnu.org.ua>2012-07-07 21:00:00 +0300
committerSergey Poznyakoff <gray@gnu.org.ua>2012-07-07 21:06:59 +0300
commit70ca1af7054fbee093ba2029e0989052cc993b19 (patch)
treecc6bbe4e5312221f63d401e3d2ba5876bb4ed8f8 /axfr2acl
parentd39b0783d77ba756681c539b3ee19b8997494317 (diff)
downloaddnstools-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/.gitignore1
-rw-r--r--axfr2acl/GNUmakefile2
-rw-r--r--axfr2acl/Makefile.PL15
-rwxr-xr-xaxfr2acl/axfr2acl540
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
+

Return to:

Send suggestions and report system problems to the System administrator.