#!/usr/bin/perl # # $Cambridge: hermes/conf/bind/bin/nsdiff,v 1.41 2013/02/21 13:11:16 fanf2 Exp $ use warnings; use strict; use Getopt::Std; use POSIX; sub wail { warn "nsdiff: @_\n"; } sub fail { wail @_; exit 2; } # for named-compilezone $ENV{PATH} .= ":/sbin:/usr/sbin:/usr/local/sbin"; my $compilezone = 'named-compilezone -i local -k warn -n warn -o -'; sub usage { print STDERR < [old] Generate an `nsupdate` script that changes a zone from the "old" version into the "new" version, ignoring DNSSEC records. If the "old" file is omitted, `nsdiff` will AXFR the zone. options: -0 Allow a domain's updates to span packets -1 Abort if update doesn't fit in one packet -i regex Ignore records matching the pattern -S num|mode SOA serial number or update mode -q only output if zones differ -v [q][r] verbose query and/or reply -b address dig query source address -k keyfile dig query TSIG key -y [hmac:]name:key dig query TSIG key EOF exit 2; } my %opt; usage unless getopts '01i:S:qv:b:k:y:', \%opt; usage unless @ARGV == 2 || @ARGV == 3; my @digopts; for my $o (qw{ b k y }) { push @digopts, "-$o $opt{$o}" if exists $opt{$o}; } wail "ignoring dig options when loading old zone from file" if @digopts && @ARGV != 2; usage if $opt{q} && $opt{v}; usage if $opt{v} && $opt{v} !~ m{^[qr]*$}; my $quiet = $opt{q} ? '2>/dev/null' : ''; my $verbosity = exists $opt{v} ? $opt{v} : $quiet ? '' : 'r'; my $soamode = $opt{S} || 'serial'; my $soafun = $soamode =~ m{^[0-9]+$} ? sub { return $soamode } : { serial => sub { return 0 }, unix => sub { return time }, date => sub { return strftime "%Y%m%d00", gmtime }, }->{$soamode} or usage; my $soamin = $soafun->(); my $zone = shift; $zone =~ s{[.]?$}{.}; my $zonere = quotemeta $zone; my $domain = qr{(?:[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?[.])+}; my $dnssec = qr{^\S+\s+\d+\s+IN\s+(NSEC|NSEC3|NSEC3PARAM|RRSIG|DNSKEY|TYPE65534)\s+}; my $soare = qr{^$zonere\s+(\d+)\s+(IN\s+SOA\s+$domain\s+$domain) \s+(\d+)\s+(\d+\s+\d+\s+\d+\s+\d+\n)$}x; my $exclude = $opt{i} ? qr{$dnssec|$opt{i}} : qr{$dnssec}; fail "not a domain name: $zone" unless $zone =~ m{^$domain$}; # Check there is a SOA and remove DNSSEC records. # Store zone data in the keys of a hash. sub cleanzone { my ($soa,%zone) = shift; fail "missing SOA record" unless defined $soa and $soa =~ $soare; $zone{$_} = 1 for grep { not m{$exclude} } @_; return ($soa,\%zone); } sub axfrzone { my $zone = shift; my @soa = split ' ', qx{dig +short soa $zone}; my $master = $soa[0]; fail "could not get SOA record for $zone" unless defined $master and $master =~ m{^$domain$}; wail "loading zone $zone via AXFR from $master" unless $quiet; return cleanzone qx{dig @digopts +noadditional axfr $zone \@$master | $compilezone $zone /dev/stdin $quiet}; } sub loadzone { my ($zone,$file) = @_; wail "loading zone $zone from file $file" unless $quiet; return cleanzone qx{$compilezone -j $zone '$file' $quiet}; } my ($soa,$old) = (@ARGV == 1) ? axfrzone $zone : loadzone $zone, shift; my ($newsoa,$new) = loadzone $zone, shift; # Remove unchanged RRs, and save each name's deletions and additions. my (%del,%add); for my $rr (keys %$old) { delete $old->{$rr}; next if delete $new->{$rr}; my ($owner,$ttl,$data) = split ' ', $rr, 3; push @{$del{$owner}}, $data; } for my $rr (keys %$new) { delete $new->{$rr}; my ($owner,$data) = split ' ', $rr, 2; push @{$add{$owner}}, $data; } # For each owner name prepare deletion commands followed by addition # commands. This ensures TTL adjustments and CNAME/other replacements # are handled correctly. Ensure each owner's changes are not split below. my (@batch,@script); sub emit { if ($opt{0}) { push @script, splice @batch } else { push @script, join '', splice @batch } } sub update { my ($addel,$owner,$rrs) = @_; push @batch, map "update $addel $owner $_", sort @$rrs; } for my $owner (keys %del) { update 'delete', $owner, delete $del{$owner}; update 'add', $owner, delete $add{$owner} if exists $add{$owner}; emit; } for my $owner (keys %add) { update 'add', $owner, delete $add{$owner}; emit; } my $status = @script ? 1 : 0; if ($quiet) { wail "$zone has changes" if $status; exit $status; } # Emit commands in batches that fit within the 64 KiB DNS packet limit # assuming textual representation is not smaller than binary encoding. # Use a prerequisite based on the SOA record to catch races. my $maxlen = 65536; while (@script) { my ($length,$i) = (0,0); $length += length $script[$i++] while $length < $maxlen and $i < @script; my @batch = splice @script, 0, $length < $maxlen ? $i : $i - 1; fail "update does not fit in packet" if @batch == 0 or $opt{1} and @script != 0; $soa =~ $soare; print "prereq yxrrset $zone $2 $3 $4"; my $serial = $3 >= $soamin ? $3 + 1 : $soamin; $newsoa =~ $soare; print "update add ", $soa = "$zone $1 $2 $serial $4"; print @batch; print "show\n" if $verbosity =~ m{q}; print "send\n"; print "answer\n" if $verbosity =~ m{r}; } exit $status; __END__ =head1 NAME nsdiff - create "nsupdate" script from DNS zone file diffrences =head1 SYNOPSIS nsdiff [B<-b> I
] [B<-k> I] [B<-y> [I:]I:I] [B<-0>|B<-1>] [B<-q>|B<-v> [q][r]] [B<-i> I] [B<-S> I|I] > [I] > =head1 DESCRIPTION The B program examines the F and F versions of a DNS zone, and outputs the differences as a script for use by BIND's B program. It ignores DNSSEC-related differences, assuming that the name server has sole control over zone keys and signatures. The input files are typically in standard DNS master file format. They are passed through BIND's B program to convert them to canonical form, so they may also be in BIND's "raw" format and may have F<.jnl> update journals. If the F file is not specified, B will use B to transfer the zone from its master server as identified in the zone's SOA MNAME field. =head1 OPTIONS =over =item B<-0> Allow very large updates affecting one domain name to be split across multiple requests. =item B<-1> Abort if update does not fit in one request packet. =item B<-i> I Ignore more DNS records. By default, B strips out DNSSEC RRs before comparing zones. You can exclude irrelevant changes from the diff by supplying a I that matches the unwanted RRs. =item B<-S> B|B|B|I Choose the SOA serial number update mode: the default I just increments the serial number; I uses a number of the form YYYYMMDDnn and allows for up to 100 updates per day; I uses the UNIX "seconds since the epoch" value. You can also specify an explicit serial number value. In all cases, if the current serial number is larger than the target value it is just incremented. Serial number wrap-around is not supported. =item B<-q> Quiet / quick check. Output is suppressed unless the zones differ, in which case a short note is printed instead of an B script. =item B<-v> [q][r] Control verbosity. The B flag causes queries to be printed. The B flag causes responses to be printed. To make B quiet, use S>. =back The following options are passed to B to modify its SOA and AXFR queries: =over =item B<-b> I
Source address for B queries =item B<-k> I TSIG key file for B queries. =item B<-y> [I:]I:I Literal TSIG key for B queries. =back =head1 EXIT STATUS The B utility returns 0 if the zones are the same, 1 if they differ, and 2 if there was an error. =head1 DIAGNOSTICS =over =item C =item CzoneE>> Errors in the command line. =item CzoneE>> Failed to retreive the zone's SOA using B. =item C The output of B is incomplete, usually because the input file is erroneous. =item CzoneE> has changes> Printed instead of an B script when the B<-q> option is used. =item C The changes for one domain name did not fit in 64 KiB, or the B<-1> option was specified and all the changes did not fit in 64 KiB. =item C Warning emitted when the command line includes options for B as well as an old zone source file. =item CzoneE> from AXFR> =item CzoneE> from file I> Normal progress messages emitted before B invokes B, to explain the latter's diagnostics. =back =head1 EXAMPLE - DNSSEC It is easiest to deploy DNSSEC if you allow B to manage zone keys and signatures automatically, and feed in changes to zones using DNS update requests. However this is very different from the traditional way of manually maintaining zones in standard master file format. The B program bridges the gap between the two operational styles. To support this workflow you need BIND-9.7 or newer. You will continue maintaining your zone master file C<$sourcefile> as before, but it is no longer the same as the C<$workingfile> used by B. After you make a change, instead of using C, run C. Configure your zone as follows, to support DNSSEC and local dynamic updates: zone $zone { type master; file "$workingfile"; auto-dnssec maintain; update-policy local; }; To create DNSSEC keys for your zone, change to named's working directory and run these commands: dnssec-keygen -f KSK $zone dnssec-keygen $zone =head1 EXAMPLE - dynamic reverse DNS You have a reverse zone such as C<2.0.192.in-addr.arpa> which is mostly managed dynamically by a DHCP server, but which also has some static records (for network equipment, say). You can maintain the static part in a master file and feed any changes into the live dynamic zone by telling B to ignore the dynamic entries. Say all the static equipment has IP addresses between 192.0.2.250 and 192.0.2.255, then you can run the command pipeline: nsdiff -i '^(?!25\d\.)' 2.0.192.in-addr.arpa 2.0.192.static | nsupdate -l =head1 CAVEATS By default B does not maintain the transactional semantics of native DNS update requests when the diff is big: it applies large changes in multiple update requests. To minimise the problems this may cause, B ensures each domain name's changes are all in the same update request. There is still a small risk of clients not seeing a change applied atomically when that matters (e.g. altering an MX and creating the new target in the same transaction). You can avoid the risk by using the B<-1> option to prevent multi-packet updates, or by being careful about changes that depend on multiple domain names. The update requests emitted by B include SOA serial number prerequisite checks to ensure that the zone has not changed while it is running. This can happen even in simple setups if B happens to be re-signing the zone at the time you make an update. Unfortunately the DNS update protocol does not allow for good error reporting when a prerequisite check fails. You can use the following script to cope with this problem: while ! nsdiff -q $zone $source do nsdiff $zone $source | nsupdate -l done =head1 BUGS When updating a name's DNS records, B first deletes the old ones then adds the new ones. This ensures that CNAME replacements and TTL changes work correctly. However, this update strategy prevents you from replacing every record in a zone's apex NS RRset, because it isn't possible to delete all a zone's name servers. =head1 AUTHOR =over =item Written by Tony Finch =item at the University of Cambridge Computing Service. =item You may do anything with this. It has no warranty. =item L =back =head1 ACKNOWLEDGMENTS Thanks to Mike Bristow, Piete Brooks (University of Cambridge Computer Laboratory), and Terry Burton (University of Leicester) for providing useful feedback. =head1 SEE ALSO dig(1), nsupdate(1), perlre(1), named(8), named-compilezone(8) =cut