1#!/usr/bin/perl
2# $Id: axfr 1815 2020-10-14 21:55:18Z willem $
3
4use strict;
5use warnings;
6use vars qw($opt_f $opt_q $opt_s $opt_D);
7use File::Basename;
8use Getopt::Std;
9use Net::DNS;
10use Storable;
11
12#------------------------------------------------------------------------------
13# Read any command-line options and check syntax.
14#------------------------------------------------------------------------------
15
16getopts("fqsD:");
17
18die "Usage: ", basename($0), " [ -fqs ] [ -D directory ] [ \@nameserver ] zone\n"
19	unless (@ARGV >= 1) && (@ARGV <= 2);
20
21#------------------------------------------------------------------------------
22# Get the nameserver (if specified) and set up the zone transfer directory
23# hierarchy.
24#------------------------------------------------------------------------------
25
26my $nameserver = ($ARGV[0] =~ /^@/) ? shift @ARGV : "";
27$nameserver =~ s/^@//;
28
29my $zone = shift @ARGV;
30my $basedir = defined $opt_D ? $opt_D : $ENV{"HOME"} . "/.dns-zones";
31my $zonedir = join("/", reverse(split(/\./, $zone)));
32my $zonefile = $basedir . "/" . $zonedir . "/axfr";
33
34# Don't worry about the 0777 permissions here - the current umask setting
35# will be applied.
36unless (-d $basedir) {
37	mkdir($basedir, 0777) or die "can't mkdir $basedir: $!\n";
38}
39
40my $dir = $basedir;
41my $subdir;
42foreach my $subdir (split(m#/#, $zonedir)) {
43	$dir .= "/" . $subdir;
44	unless (-d $dir) {
45		mkdir($dir, 0777) or die "can't mkdir $dir: $!\n";
46	}
47}
48
49#------------------------------------------------------------------------------
50# Get the zone.
51#------------------------------------------------------------------------------
52
53my $res = Net::DNS::Resolver->new;
54$res->nameservers($nameserver) if $nameserver;
55
56my (@zone, $zoneref);
57
58if (-e $zonefile && !defined $opt_f) {
59	$zoneref = retrieve($zonefile) || die "couldn't retrieve zone from $zonefile: $!\n";
60
61	#----------------------------------------------------------------------
62	# Check the SOA serial number if desired.
63	#----------------------------------------------------------------------
64
65	if (defined $opt_s) {
66		my($serial_file, $serial_zone);
67
68		my $rr;
69		foreach my $rr (@$zoneref) {
70			if ($rr->type eq "SOA") {
71				$serial_file = $rr->serial;
72				last;
73			}
74		}
75		die "no SOA in $zonefile\n" unless defined $serial_file;
76
77		my $soa = $res->query($zone, "SOA");
78		die "couldn't get SOA for $zone: ", $res->errorstring, "\n"
79			unless defined $soa;
80
81		foreach my $rr ($soa->answer) {
82			if ($rr->type eq "SOA") {
83				$serial_zone = $rr->serial;
84				last;
85			}
86		}
87
88		if ($serial_zone != $serial_file) {
89			$opt_f = 1;
90		}
91	}
92} else {
93	$opt_f = 1;
94}
95
96if (defined $opt_f) {
97	@zone = $res->axfr($zone);
98	die "couldn't transfer zone: ", $res->errorstring, "\n" unless @zone;
99	store \@zone, $zonefile or die "couldn't store zone to $zonefile: $!\n";
100	$zoneref = \@zone;
101}
102
103#------------------------------------------------------------------------------
104# Print the records in the zone.
105#------------------------------------------------------------------------------
106
107unless ($opt_q) {
108	$_->print for @$zoneref
109}
110
111__END__
112
113=head1 NAME
114
115axfr - Perform a DNS zone transfer
116
117=head1 SYNOPSIS
118
119B<axfr> S<[ B<-fqs> ]> S<[ B<-D> I<directory> ]> S<[ B<@>I<nameserver> ]>
120I<zone>
121
122=head1 DESCRIPTION
123
124B<axfr> performs a DNS zone transfer, prints each record to the standard
125output, and stores the zone to a file.  If the zone has already been
126stored in a file, B<axfr> will read the file instead of performing a
127zone transfer.
128
129Zones will be stored in a directory hierarchy.  For example, the
130zone transfer for foo.bar.com will be stored in the file
131$HOME/.dns-zones/com/bar/foo/axfr.  The directory can be changed
132with the B<-D> option.
133
134This programs requires that the Storable module be installed.
135
136=head1 OPTIONS
137
138=over 4
139
140=item B<-f>
141
142Force a zone transfer, even if the zone has already been stored
143in a file.
144
145=item B<-q>
146
147Be quiet -- don't print the records from the zone.
148
149=item B<-s>
150
151Perform a zone transfer if the SOA serial number on the nameserver
152is different than the serial number in the zone file.
153
154=item B<-D> I<directory>
155
156Store zone files under I<directory> instead of the default directory
157(see L<"FILES">).
158
159=item B<@>I<nameserver>
160
161Query I<nameserver> instead of the default nameserver.
162
163=back
164
165=head1 FILES
166
167=over 4
168
169=item B<$HOME/.dns-zones>
170
171Default directory for storing zone files.
172
173=back
174
175=head1 AUTHOR
176
177Michael Fuhr <mike@fuhr.org>
178
179=head1 SEE ALSO
180
181L<perl(1)>, L<check_soa>, L<check_zone>, L<mresolv>, L<mx>, L<perldig>,
182L<Net::DNS>, L<Storable>
183
184=cut
185