├── .gitignore ├── MANIFEST ├── MANIFEST.SKIP ├── Makefile.PL ├── README.pod ├── debian ├── changelog ├── control ├── copyright ├── nsdiff.docs ├── rules └── source │ └── format ├── lib └── DNS │ └── nsdiff.pm ├── nsdiff ├── nspatch ├── nsvi └── reversion.sh /.gitignore: -------------------------------------------------------------------------------- 1 | nsdiff-*.tar.gz 2 | nsdiff-*.tar.xz 3 | nsdiff-*.zip 4 | DNS-nsdiff-*.tar.gz 5 | DNS-nsdiff-*.tar.xz 6 | DNS-nsdiff-*.zip 7 | MYMETA.* 8 | Makefile 9 | Makefile.old 10 | pm_to_blib 11 | blib 12 | web 13 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | MANIFEST 2 | MANIFEST.SKIP 3 | Makefile.PL 4 | README.pod 5 | lib/DNS/nsdiff.pm 6 | nsdiff 7 | nspatch 8 | nsvi 9 | -------------------------------------------------------------------------------- /MANIFEST.SKIP: -------------------------------------------------------------------------------- 1 | .git 2 | reversion.sh 3 | debian 4 | web 5 | Makefile$ 6 | MYMETA.* 7 | blib 8 | .*~ 9 | .*\.tar\..* 10 | -------------------------------------------------------------------------------- /Makefile.PL: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: 0BSD OR MIT-0 2 | 3 | use 5.10.0; 4 | use ExtUtils::MakeMaker; 5 | 6 | # suppress warning about the ExtUtils::MakeMaker bug 7 | # that we fixed with the MAN3PODS setting below 8 | $SIG{__WARN__} = sub { 9 | print STDERR @_ unless "@_" eq "WARNING: Older versions of ExtUtils::MakeMaker may errantly install README.pod as part of this distribution. It is recommended to avoid using this path in CPAN modules.\n" 10 | }; 11 | 12 | WriteMakefile( 13 | NAME => 'DNS::nsdiff', 14 | VERSION => "1.85", 15 | ABSTRACT => 16 | "create an 'nsupdate' script from DNS zone file differences", 17 | EXE_FILES => [qw[ nsdiff nspatch nsvi ]], 18 | 19 | # to stop MakeMaker from installing the README as a man page 20 | # we have to list the section 3 pages explicitly 21 | MAN3PODS => { 22 | 'lib/DNS/nsdiff.pm' => 'blib/man3/DNS::nsdiff.3pm', 23 | }, 24 | 25 | AUTHOR => 'Tony Finch ', 26 | LICENSE => 'unrestricted', # cc0 not allowed by CPAN::Meta::Spec 27 | MIN_PERL_VERSION=> '5.10.0', 28 | META_MERGE => { 29 | 'meta-spec' => { version => 2 }, 30 | 'resources' => { 31 | repository => { 32 | type => 'git', 33 | url => 'git://dotat.at/nsdiff.git', 34 | web => 'https://dotat.at/cgi/git/nsdiff.git', 35 | } 36 | } 37 | } 38 | ); 39 | 40 | sub MY::postamble { 41 | return <<'MAKE_FRAG'; 42 | html:: 43 | mkdir -p web 44 | for f in nsdiff nspatch nsvi; \ 45 | do pod2html --noindex $$f >web/$$f.html; \ 46 | done 47 | pod2html --noindex README.pod >web/README.html 48 | ln -sf README.html web/index.html 49 | rm -f pod2htm?.tmp 50 | 51 | deb:: 52 | grep "1.85" debian/changelog 53 | mkdir -p web 54 | dpkg-buildpackage -uc -A 55 | mv ../nsdiff_* web/. 56 | debian/rules clean 57 | 58 | upload:: html 59 | git push --tags github trunk 60 | git push --tags chiark trunk 61 | if [ -f DNS-nsdiff-*.tar.gz ]; \ 62 | then mv DNS-nsdiff-*.tar.gz web/.; \ 63 | fi 64 | cp nsdiff web/. 65 | rsync -ilt web/nsdiff web/*.html web/*.tar.gz web/nsdiff_* \ 66 | chiark:public-html/prog/nsdiff/ 67 | 68 | MAKE_FRAG 69 | } 70 | -------------------------------------------------------------------------------- /README.pod: -------------------------------------------------------------------------------- 1 | =head1 nsdiff 2 | 3 | =head2 create an "nsupdate" script from DNS zone file differences 4 | 5 | The B program examines the old and new versions of a DNS zone, 6 | and outputs the differences as a script for use by BIND's B 7 | program. It provides a bridge between static zone files and dynamic 8 | updates. 9 | 10 | The B script is a wrapper around C that 11 | checks and reports errors in a manner suitable for running from B. 12 | 13 | The B script makes it easy to edit a dynamic zone. 14 | 15 | =over 16 | 17 | I - JP Mens 18 | 19 | =back 20 | 21 | If you use BIND 9.7 or 9.8, you can use B as an alternative 22 | to the DNSSEC C feature which appeared in BIND 9.9. 23 | The server updates the DNSSEC records dynamically, but you can 24 | continue to manage the unsigned static zone file as before and use 25 | C<`nsdiff | nsupdate`> to push changes to the server. 26 | 27 | There are other situations where you have a zone which is partly 28 | dynamic and partly static, for example, a reverse DNS zone mostly 29 | updated by a DHCP server, which also has a few static entries. You can 30 | use B to update the static part of the zone. 31 | 32 | =head2 Dependencies 33 | 34 | To run nsdiff you need perl-5.10 or newer, and BIND version 9.7 or 35 | newer, specifically the B, B, and B 36 | utilities. 37 | 38 | =head2 Install 39 | 40 | To install, run: 41 | 42 | perl Makefile.PL 43 | make install 44 | 45 | To install in a particular place, use something like 46 | 47 | perl Makefile.pl PREFIX=${HOME} 48 | 49 | =head2 Downloads 50 | 51 | =over 52 | 53 | =item Documentation 54 | 55 | The nsdiff homepage is L 56 | 57 | Read the nsdiff manual: L 58 | 59 | Read the nspatch manual: L 60 | 61 | Read the nsvi manual: L 62 | 63 | =item Code 64 | 65 | Download the bare nsdiff perl source: L 66 | 67 | Download the source distribution: 68 | 69 | =over 70 | 71 | =item 72 | 73 | L 74 | 75 | =back 76 | 77 | =item Source repositories 78 | 79 | You can clone or browse the repository from: 80 | 81 | =over 82 | 83 | =item 84 | 85 | L 86 | 87 | =item 88 | 89 | L 90 | 91 | =item 92 | 93 | L 94 | 95 | =back 96 | 97 | =back 98 | 99 | =head2 Feedback 100 | 101 | Please send bug reports or patches to me at . 102 | 103 | Any contribution that you want included in `nsdiff` must be licensed 104 | under 0BSD and/or MIT-0, and must include a `Signed-off-by:` line to 105 | certify that you wrote it or otherwise have the right to pass it on 106 | as a open-source patch, according to the Developer's Certificate of 107 | Origin 1.1. 108 | 109 | =over 110 | 111 | =item 112 | 113 | 0BSD: L 114 | 115 | =item 116 | 117 | MIT-0 L 118 | 119 | =item 120 | 121 | DCO: L 122 | 123 | =back 124 | 125 | =head2 Licence 126 | 127 | Copyright 2011-2024 Tony Finch 128 | 129 | Permission is hereby granted to use, copy, modify, and/or 130 | distribute this software for any purpose with or without fee. 131 | 132 | This software is provided 'as is', without warranty of any kind. 133 | In no event shall the authors be liable for any damages arising 134 | from the use of this software. 135 | 136 | SPDX-License-Identifier: 0BSD OR MIT-0 137 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | nsdiff (1.85-1) unstable; urgency=low 2 | 3 | * Fix dependencies and update package build versions 4 | 5 | -- Tony Finch Tue, 26 Mar 2024 10:03:07 +0000 6 | 7 | nsdiff (1.85) unstable; urgency=low 8 | 9 | * Change from CC0 to 0BSD OR MIT-0 10 | 11 | * Fix interactive confirmation in 'nsvi -n' 12 | 13 | * Only bump the serial number when required 14 | 15 | * Forward -g from nsvi to nsupdate 16 | 17 | -- Tony Finch Wed, 20 Mar 2024 09:46:15 +0000 18 | 19 | nsdiff (1.82-1) unstable; urgency=low 20 | 21 | * Better package name and more accurate dependencies. 22 | 23 | -- Tony Finch Sat, 4 Jul 2020 01:33:00 +0100 24 | 25 | nsdiff (1.82) unstable; urgency=low 26 | 27 | * Initial Debian package. 28 | 29 | -- Tony Finch Fri, 3 Jul 2020 23:24:31 +0100 30 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: nsdiff 2 | Section: net 3 | Priority: optional 4 | Maintainer: Tony Finch 5 | Build-Depends: debhelper (= 13) 6 | Build-Depends-Indep: perl 7 | Standards-Version: 4.6.2 8 | Homepage: https://dotat.at/prog/nsdiff/ 9 | 10 | Package: nsdiff 11 | Architecture: all 12 | Depends: ${misc:Depends}, ${perl:Depends}, bind9-dnsutils, bind9-utils 13 | Description: create an 'nsupdate' script from DNS zone file differences 14 | The nsdiff program examines the old and new versions of a DNS zone, 15 | and outputs the differences as a script for use by BIND's nsupdate 16 | program (in Debian's dnsutils package). 17 | . 18 | The nspatch script is a wrapper around `nsdiff | nsupdate` that 19 | checks and reports errors in a manner suitable for running from cron. 20 | . 21 | The nsvi script makes it easy to edit a dynamic zone. 22 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Source: https://metacpan.org/release/DNS-nsdiff 3 | Upstream-Contact: Tony Finch 4 | Upstream-Name: DNS-nsdiff 5 | 6 | Files: * 7 | Copyright: 2011-2024 Tony Finch 8 | License: 0BSD OR MIT-0 9 | Permission is hereby granted to use, copy, modify, and/or 10 | distribute this software for any purpose with or without fee. 11 | 12 | This software is provided 'as is', without warranty of any kind. 13 | In no event shall the authors be liable for any damages arising 14 | from the use of this software. 15 | -------------------------------------------------------------------------------- /debian/nsdiff.docs: -------------------------------------------------------------------------------- 1 | README.pod 2 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | %: 4 | dh $@ 5 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /lib/DNS/nsdiff.pm: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: 0BSD OR MIT-0 2 | 3 | package DNS::nsdiff; 4 | 5 | our $VERSION = "1.85"; 6 | 7 | =head1 NAME 8 | 9 | DNS::nsdiff - the nsdiff, nspatch, and nsvi scripts 10 | 11 | =head1 DESCRIPTION 12 | 13 | This is a stub module for overview documentation. There are three 14 | scripts in the DNS::nsdiff distribution: 15 | 16 | =over 17 | 18 | =item B 19 | 20 | The B program examines the old and new versions of a DNS zone, 21 | and outputs the differences as a script for use by BIND's B 22 | program. It provides a bridge between static zone files and dynamic 23 | updates. 24 | 25 | =item B 26 | 27 | The B script is a wrapper around C that 28 | checks and reports errors in a manner suitable for running from B. 29 | 30 | =item B 31 | 32 | The B script makes it easy to edit a dynamic zone. 33 | 34 | =back 35 | 36 | =head1 VERSION 37 | 38 | This is DNS::nsdiff-1.85 39 | 40 | Written by Tony Finch 41 | at Cambridge University Information Services. 42 | You may do anything with this. It has no warranty. 43 | 44 | =head1 SEE ALSO 45 | 46 | nsdiff(1), nspatch(1), nsvi(1), nsupdate(1) 47 | 48 | =cut 49 | 50 | 1; 51 | -------------------------------------------------------------------------------- /nsdiff: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | # SPDX-License-Identifier: 0BSD OR MIT-0 3 | 4 | use warnings; 5 | use strict; 6 | 7 | use Getopt::Std; 8 | use POSIX; 9 | 10 | sub wail { warn "nsdiff: @_\n"; } 11 | sub fail { wail @_; exit 2; } 12 | 13 | # for named-compilezone 14 | $ENV{PATH} .= ":/sbin:/usr/sbin:/usr/local/sbin"; 15 | my $compilezone = 'named-compilezone -i local -k warn -n warn -o -'; 16 | 17 | sub version { 18 | while () { 19 | print if m{^=head1 VERSION} ... m{^=head1 } 20 | and not m{^=head1 }; 21 | } 22 | exit; 23 | } 24 | 25 | sub usage { 26 | print STDERR < [old] [new] 28 | Generate an `nsupdate` script that changes a zone from the 29 | "old" version into the "new" version, ignoring DNSSEC records. 30 | If the "old" file is omitted and there is no -s option, `nsdiff` 31 | will AXFR the zone from the server in the zone's SOA MNAME field. 32 | options: 33 | -h display full documentation 34 | -V display version information 35 | -0 allow a domain's updates to span packets 36 | -1 abort if update doesn't fit in one packet 37 | -c compare records case-insensitively 38 | -C do not ignore CDS/CDNSKEY records 39 | -d ignore DS records 40 | -D do not ignore DNSKEY records 41 | -i regex ignore records matching the pattern 42 | -m server[#port] from where to AXFR new version of the zone 43 | -s server[#port] from where to AXFR old version of the zone 44 | -S num|mode SOA serial number or update mode 45 | -q only output if zones differ 46 | -u tell nsupdate to send to -s server 47 | -v [q][r] verbose query and/or reply 48 | -b address AXFR query source address 49 | -k keyfile AXFR query TSIG key 50 | -y [hmac:]name:key AXFR query TSIG key 51 | EOF 52 | exit 2; 53 | } 54 | my %opt; 55 | usage unless getopts '-hV01cCdDi:m:s:S:quv:b:k:y:', \%opt; 56 | version if $opt{V}; 57 | exec "perldoc -oterm -F $0" if $opt{h}; 58 | usage if @ARGV < 1 || @ARGV > 3; 59 | 60 | my @digopts; 61 | for my $o (qw{ b k y }) { 62 | push @digopts, "-$o $opt{$o}" if exists $opt{$o}; 63 | } 64 | wail "ignoring dig options when loading zones from files" 65 | if @digopts && @ARGV == 3; 66 | wail "ignoring -m option when loading new zone from file" 67 | if $opt{m} && @ARGV > 1; 68 | fail "need -m option when there are no input files" 69 | unless $opt{m} || @ARGV > 1; 70 | usage if $opt{u} && !$opt{s}; 71 | 72 | usage if $opt{q} && $opt{v}; 73 | usage if $opt{v} && $opt{v} !~ m{^[qr]*$}; 74 | my $quiet = $opt{q} ? '2>/dev/null' : ''; 75 | my $verbosity = exists $opt{v} ? $opt{v} : $quiet ? '' : 'r'; 76 | 77 | $opt{$_} and $opt{$_} =~ s{#}{ } for qw{ s m }; # for nsupdate server command 78 | 79 | my $secRRtypes = qr{NSEC|NSEC3|NSEC3PARAM|RRSIG}; 80 | $secRRtypes = qr{$secRRtypes|CDS|CDNSKEY} unless $opt{C}; 81 | $secRRtypes = qr{$secRRtypes|DNSKEY} unless $opt{D}; 82 | $secRRtypes = qr{$secRRtypes|DS} if $opt{d}; 83 | 84 | my $soamode = $opt{S} || 'file'; 85 | my $soafun = $soamode =~ m{^[0-9]+$} ? 86 | sub { return $soamode } : { 87 | serial => sub { return 0 }, 88 | file => sub { return $_[0] }, 89 | master => sub { return $_[0] }, # compat 90 | unix => sub { return time }, 91 | date => sub { return strftime "%Y%m%d00", gmtime }, 92 | }->{$soamode} or usage; 93 | 94 | my $zone = shift; $zone =~ s{[.]?$}{.}; 95 | my $zonere = quotemeta $zone; 96 | my $hostname = qr{(?:[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?[.])+}; 97 | my $rname = qr{(?:[^;.\\\s]|\\.)+[.]$hostname|[.]}; 98 | my $soare = qr{^$zonere\s+(\d+)\s+(IN\s+SOA\s+$hostname\s+$rname) 99 | \s+(\d+)\s+(\d+\s+\d+\s+\d+\s+\d+\n)$}x; 100 | my $dnssec = qr{^\S+\s+\d+\s+IN\s+($secRRtypes)\s+}; 101 | my $exclude = $opt{i} ? qr{$dnssec|$opt{i}} : qr{$dnssec}; 102 | 103 | # Check there is a SOA and remove DNSSEC records. 104 | # Store zone data in the keys of a hash. 105 | 106 | sub cleanzone { 107 | my ($soa,%zone) = shift; 108 | fail "missing SOA record" unless defined $soa and $soa =~ $soare; 109 | $zone{$_} = 1 for grep { not m{^;|$exclude}o } @_; 110 | return ($soa,\%zone); 111 | } 112 | 113 | sub axfrzone { 114 | my $zone = shift; 115 | my $primary = shift; 116 | wail "loading zone $zone via AXFR from $primary" unless $quiet; 117 | $primary =~ s{^(.*) (\d+)$}{-p $2 \@$1} or $primary = '@'.$primary; 118 | return cleanzone qx{dig @digopts $primary +noadditional axfr $zone | 119 | $compilezone $zone /dev/stdin $quiet}; 120 | } 121 | 122 | sub loadzone { 123 | my ($zone,$file) = @_; 124 | wail "loading zone $zone from file $file" unless $quiet; 125 | return cleanzone qx{$compilezone -j $zone '$file' $quiet}; 126 | } 127 | 128 | sub mname { 129 | my $zone = shift; 130 | my @soa = split ' ', qx{dig +short soa $zone}; 131 | my $primary = $soa[0]; 132 | fail "could not get SOA record for $zone" 133 | unless defined $primary and $primary =~ m{^$hostname$}; 134 | return $primary; 135 | } 136 | 137 | my ($soa,$old) = (@ARGV < 2) 138 | ? axfrzone $zone, $opt{s} || mname $zone 139 | : loadzone $zone, shift; 140 | my ($newsoa,$new) = (@ARGV < 1) 141 | ? axfrzone $zone, $opt{m} 142 | : loadzone $zone, shift; 143 | 144 | # Does the SOA need to be updated? 145 | $soa =~ $soare; 146 | my $oldserial = $3; 147 | my $oldsoa = "$1 $2 $4"; 148 | $newsoa =~ $soare; 149 | my $newserial = $3; 150 | my $upsoa = $oldsoa ne "$1 $2 $4" 151 | || ($soamode =~ m{file|master} && $oldserial < $newserial); 152 | # The serial number in the update might depend on the new SOA serial number. 153 | my $soamin = $soafun->($newserial); 154 | 155 | # Remove unchanged RRs, and save each name's deletions and additions. 156 | 157 | my (%del,%add,%uc); 158 | 159 | map { $uc{lc $_} = $_ } keys %$new if $opt{c}; 160 | 161 | for my $rr (keys %$old) { 162 | delete $old->{$rr}; 163 | next if $uc{lc $rr} and delete $new->{delete $uc{lc $rr}}; 164 | next if delete $new->{$rr}; 165 | my ($owner,$ttl,$data) = split ' ', $rr, 3; 166 | push @{$del{$owner}}, $data; 167 | } 168 | for my $rr (keys %$new) { 169 | delete $new->{$rr}; 170 | my ($owner,$data) = split ' ', $rr, 2; 171 | push @{$add{$owner}}, $data; 172 | } 173 | 174 | # For each owner name prepare deletion commands followed by addition 175 | # commands. This ensures TTL adjustments and CNAME/other replacements 176 | # are handled correctly. Ensure each owner's changes are not split below. 177 | 178 | my (@batch,@script); 179 | 180 | sub emit { 181 | if ($opt{0}) { push @script, splice @batch } 182 | else { push @script, join '', splice @batch } 183 | } 184 | sub update { 185 | my ($addel,$owner,$rrs) = @_; 186 | push @batch, map "update $addel $owner $_", sort @$rrs; 187 | } 188 | for my $owner (keys %del) { 189 | update 'delete', $owner, delete $del{$owner}; 190 | update 'add', $owner, delete $add{$owner} if exists $add{$owner}; 191 | emit; 192 | } 193 | for my $owner (keys %add) { 194 | update 'add', $owner, delete $add{$owner}; 195 | emit; 196 | } 197 | 198 | my $status = ($upsoa or @script) ? 1 : 0; 199 | if ($quiet) { 200 | wail "$zone has changes" if $status; 201 | exit $status; 202 | } 203 | 204 | # Emit commands in batches that fit within the 64 KiB DNS packet limit 205 | # assuming textual representation is not smaller than binary encoding. 206 | # Use a prerequisite based on the SOA record to catch races. 207 | 208 | my $maxlen = 65536; 209 | while ($upsoa or @script) { 210 | my ($length,$i) = (0,0); 211 | $length += length $script[$i++] while $length < $maxlen and $i < @script; 212 | my @batch = splice @script, 0, $length < $maxlen ? $i : $i - 1; 213 | fail "update does not fit in packet" 214 | if not $upsoa and @batch == 0 215 | or $opt{1} and @script != 0; 216 | print "server $opt{s}\n" if $opt{u}; 217 | $soa =~ $soare; 218 | print "prereq yxrrset $zone $2 $3 $4"; 219 | my $serial = $3 >= $soamin ? $3 + 1 : $soamin; 220 | $newsoa =~ $soare; 221 | print "update add ", $soa = "$zone $1 $2 $serial $4"; 222 | print @batch; 223 | print "show\n" if $verbosity =~ m{q}; 224 | print "send\n"; 225 | print "answer\n" if $verbosity =~ m{r}; 226 | undef $upsoa; 227 | } 228 | 229 | exit $status; 230 | 231 | __END__ 232 | 233 | =head1 NAME 234 | 235 | nsdiff - create "nsupdate" script from DNS zone file differences 236 | 237 | =head1 SYNOPSIS 238 | 239 | nsdiff [B<-hV>] [B<-b> I
] [B<-k> I] [B<-y> [I:]I:I] 240 | [B<-0>|B<-1>] [B<-q>|B<-v> [q][r]] [B<-cCdD>] [B<-i> I] [B<-S> I|I] 241 | [B<-u>] [B<-s> I] [B<-m> I] > [I] [I] 242 | 243 | =head1 DESCRIPTION 244 | 245 | The B program examines the F and F versions of a DNS 246 | zone, and outputs the differences as a script for use by BIND's 247 | B program. It ignores DNSSEC-related differences, assuming 248 | that the name server has sole control over zone keys and signatures. 249 | 250 | The input files are typically in standard DNS zone file format. They 251 | are passed through BIND's B program to convert them 252 | to canonical form, so they may also be in BIND's "raw" format and may 253 | have F<.jnl> update journals. 254 | 255 | If the F file is not specified, B will use B to transfer 256 | the zone from the server given by the B<-s> option, or if the B<-s> option 257 | is missing it will get the server from the zone's SOA MNAME field. If both 258 | F and F files are not specified, B will transfer the new 259 | version of the zone from the server given by the B<-m> option. 260 | 261 | The SOA serial number has special handling: any difference between the 262 | F and F serial numbers is ignored (except in B<-S file> mode), 263 | because background DNSSEC signing activity can increment the serial number 264 | unpredictably. When the zones differ, B sets the serial number 265 | according to the B<-S> option, and it uses the F serial number to 266 | protect against conflicting updates. 267 | 268 | =head1 OPTIONS 269 | 270 | =over 271 | 272 | =item B<-h> 273 | 274 | Display this documentation. 275 | 276 | =item B<-V> 277 | 278 | Display version information. 279 | 280 | =item B<-0> 281 | 282 | Allow very large updates affecting one domain name to be split across 283 | multiple requests. 284 | 285 | =item B<-1> 286 | 287 | Abort if update does not fit in one request packet. 288 | 289 | =item B<-C> 290 | 291 | Do not ignore CDS or CDNSKEY records. They are normally managed by 292 | B with the C<-P sync> and C<-D sync> options, but you 293 | can use this option if you are managing them some other way. In that 294 | case, your un-signed zone file should include the complete CDS and/or 295 | CDNSKEY RRset(s); if not, B will delete the records. 296 | 297 | =item B<-c> 298 | 299 | Compare records case-insensitively. Can be helpful if the B 300 | target server does not preserve the case of domain names. However with 301 | this option, B does not correctly handle records that only 302 | differ in case. 303 | 304 | =item B<-D> 305 | 306 | Do not ignore DNSKEY records. It is sometimes necessary to take manual 307 | control over a zone's DNSKEY RRset, for instance to include a foreign 308 | DNSKEY records during migration to or from another hosting provider. 309 | If you use this option your un-signed zone file should include the 310 | complete DNSKEY RRset; if not, nsdiff will try to delete the DNSKEY 311 | records. Normally B will reject the update, unless the zone is 312 | configured with the I option. 313 | 314 | =item B<-d> 315 | 316 | Ignore DS records. This option is useful if you are managing secure 317 | delegations on the signing server (via nsupdate) rather than in the 318 | source zone. 319 | 320 | =item B<-i> I 321 | 322 | Ignore more DNS records. By default, B strips out DNSSEC RRs 323 | (except for DS) before comparing zones. You can exclude irrelevant 324 | changes from the diff by supplying a I that matches the 325 | unwanted RRs. 326 | 327 | =item B<-m> I[#I] 328 | 329 | Transfer the new version of the zone from the server given in this option, 330 | for example, a back-end hidden primary server. You can specify the server 331 | host name or IP address, optionally followed by a "#" and the port number. 332 | 333 | =item B<-s> I[#I] 334 | 335 | Transfer the old version of the zone from the server given in this option, 336 | using the same syntax as the B<-m> option. 337 | 338 | =item B<-S> B|B|B|B|I 339 | 340 | Choose the SOA serial number update mode: the default I takes 341 | the serial number from the I input zone; I uses a number of 342 | the form YYYYMMDDnn and allows for up to 100 updates per day; 343 | I just increments the serial number in the I input zone; 344 | I uses the UNIX "seconds since the epoch" value. You can also 345 | specify an explicit serial number value. In all cases, if the I 346 | input zone serial number is larger than the target value it is just 347 | incremented. Serial number wrap-around is not supported. 348 | 349 | =item B<-q> 350 | 351 | Quiet / quick check. Output is suppressed unless the zones differ, in 352 | which case a short note is printed instead of an B script. 353 | 354 | =item B<-u> 355 | 356 | Tell B to send the update message to the server specified in the 357 | B<-s> option. 358 | 359 | =item B<-v> [q][r] 360 | 361 | Control verbosity. 362 | The B flag causes queries to be printed. 363 | The B flag causes responses to be printed. 364 | To make B quiet, use S>. 365 | 366 | =back 367 | 368 | The following options are passed to B to modify its SOA and AXFR 369 | queries: 370 | 371 | =over 372 | 373 | =item B<-b> I
374 | 375 | Source address for B queries 376 | 377 | =item B<-k> I 378 | 379 | TSIG key file for B queries. 380 | 381 | =item B<-y> [I:]I:I 382 | 383 | Literal TSIG key for B queries. 384 | 385 | =back 386 | 387 | =head1 EXIT STATUS 388 | 389 | The B utility returns 0 if the zones are the same, 1 if they 390 | differ, and 2 if there was an error. 391 | 392 | =head1 DIAGNOSTICS 393 | 394 | =over 395 | 396 | =item C 397 | 398 | =item CzoneE>> 399 | 400 | Errors in the command line. 401 | 402 | =item CzoneE>> 403 | 404 | Failed to retreive the zone's SOA using B when trying to obtain 405 | the server MNAME from which to AXFR the zone. 406 | 407 | =item C 408 | 409 | The output of B is incomplete, 410 | usually because the input file is erroneous. 411 | 412 | =item CzoneE> has changes> 413 | 414 | Printed instead of an B script when the B<-q> option is 415 | used. 416 | 417 | =item C 418 | 419 | The changes for one domain name did not fit in 64 KiB, or the B<-1> 420 | option was specified and all the changes did not fit in 64 KiB. 421 | 422 | =item C 423 | 424 | Warning emitted when the command line includes options for B 425 | as well as zone source files. 426 | 427 | =item C 428 | 429 | =item C 430 | 431 | The B<-m> I option is required when there are no file arguments, 432 | and ignored otherwise. 433 | 434 | =item CzoneE> via AXFR from I> 435 | 436 | =item CzoneE> from file I> 437 | 438 | Normal progress messages emitted before B invokes 439 | B, to explain the latter's diagnostics. 440 | 441 | =back 442 | 443 | =head1 EXAMPLE - DNSSEC 444 | 445 | It is easiest to deploy DNSSEC if you allow B to manage zone keys 446 | and signatures automatically, and feed in changes to zones using DNS 447 | update requests. However this is very different from the traditional way 448 | of manually maintaining zones in standard DNS zone file format. The 449 | B program bridges the gap between the two operational styles. 450 | 451 | To support this workflow you need BIND-9.7 or newer. You will continue 452 | maintaining your zone file C<$sourcefile> as before, but it is no 453 | longer the same as the C<$workingfile> used by B. After you make 454 | a change, instead of using C, run C. 456 | 457 | Configure your zone as follows, to support DNSSEC and local dynamic updates: 458 | 459 | zone $zone { 460 | type primary; 461 | file "$workingfile"; 462 | auto-dnssec maintain; 463 | update-policy local; 464 | }; 465 | 466 | To create DNSSEC keys for your zone, change to named's working directory 467 | and run these commands: 468 | 469 | dnssec-keygen -f KSK $zone 470 | dnssec-keygen $zone 471 | 472 | =head1 EXAMPLE - bump-in-the-wire signing 473 | 474 | A common arrangement for DNSSEC is to have a primary server that is 475 | oblivious to DNSSEC, a signing server which transfers the zone from the 476 | primary and adds the DNSSEC records, and a number of secondary servers 477 | which transfer the zone from the signer and which are the public 478 | authoritative servers. 479 | 480 | You can implement this with B, which handles the transfer of the 481 | zone from the primary to the signer. No modifications to the primary are 482 | necessary. You set up the signer as in the previous section. To transfer 483 | changes from the primary to the signer, run the following on the signer: 484 | 485 | nsdiff -m $primary -s $signer $zone | nsupdate -l 486 | 487 | =head1 EXAMPLE - dynamic reverse DNS 488 | 489 | You have a reverse zone such as C<2.0.192.in-addr.arpa> which is 490 | mostly managed dynamically by a DHCP server, but which also has some 491 | static records (for network equipment, say). You can maintain the 492 | static part in a DNS zone file and feed any changes into the live 493 | dynamic zone by telling B to ignore the dynamic entries. Say 494 | all the static equipment has IP addresses between 192.0.2.250 and 495 | 192.0.2.255, then you can run the command pipeline: 496 | 497 | nsdiff -i '^(?!25\d\.)' 2.0.192.in-addr.arpa 2.0.192.static | 498 | nsupdate -l 499 | 500 | =head1 CAVEATS 501 | 502 | By default B does not maintain the transactional semantics of 503 | native DNS update requests when the diff is big: it applies large changes 504 | in multiple update requests. To minimise the problems this may cause, 505 | B ensures each domain name's changes are all in the same update 506 | request. There is still a small risk of clients not seeing a change applied 507 | atomically when that matters (e.g. altering an MX and creating the new 508 | target in the same transaction). You can avoid the risk by using the B<-1> 509 | option to prevent multi-packet updates, or by being careful about changes 510 | that depend on multiple domain names. 511 | 512 | The update requests emitted by B include SOA serial number 513 | prerequisite checks to ensure that the zone has not changed while it is 514 | running. This can happen even in simple setups if B happens to be 515 | re-signing the zone at the time you make an update. Unfortunately the DNS 516 | update protocol does not allow for good error reporting when a prerequisite 517 | check fails. You can use B to cope with this problem. 518 | 519 | =head1 BUGS 520 | 521 | When updating a name's DNS records, B first deletes the old 522 | ones then adds the new ones. This ensures that CNAME replacements and 523 | TTL changes work correctly. However, this update strategy prevents you 524 | from replacing every record in a zone's apex NS RRset in one update, 525 | because it isn't possible to delete all a zone's name servers. 526 | 527 | =head1 VERSION 528 | 529 | This is nsdiff-1.85 530 | 531 | Written by Tony Finch 532 | at Cambridge University Information Services. 533 | You may do anything with this. It has no warranty. 534 | 535 | =head1 ACKNOWLEDGMENTS 536 | 537 | Thanks to Athanasius, Mike Bristow, Piete Brooks (University of 538 | Cambridge Computer Laboratory), Terry Burton (University of 539 | Leicester), Owen Dunn (University of Cambridge Faculty of 540 | Mathematics), Martin Hartl (Barracuda), JP Mens, Mohamad Shidiq 541 | Purnama (PANDI), and Jordan Rieger (webnames.ca) for providing useful 542 | feedback. 543 | 544 | =head1 SEE ALSO 545 | 546 | nspatch(1), nsupdate(1), nsvi(1), dig(1), 547 | named(8), named-compilezone(8), perlre(1) 548 | 549 | =cut 550 | -------------------------------------------------------------------------------- /nspatch: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | # SPDX-License-Identifier: 0BSD OR MIT-0 3 | 4 | use warnings; 5 | use strict; 6 | 7 | use File::Temp qw(tempfile); 8 | use Getopt::Std; 9 | use POSIX; 10 | 11 | sub wail { warn "nspatch: @_\n"; } 12 | sub fail { die "nspatch: @_\n"; } 13 | sub fale { die "nspatch: @_: $!\n"; } 14 | 15 | sub version { 16 | while () { 17 | print if m{^=head1 VERSION} ... m{^=head1 } 18 | and not m{^=head1 }; 19 | } 20 | exit; 21 | } 22 | 23 | sub usage { 24 | print STDERR <; 58 | } 59 | sub dupout ($$) { 60 | no strict 'refs'; 61 | my ($dst,$src) = @_; 62 | open $dst, '>&', $src or fale 'dup'; 63 | } 64 | sub mktmp { 65 | return tempfile('nspatch.XXXXXXXXXX', 66 | TMPDIR => 1, UNLINK => 1); 67 | } 68 | sub tmpf { 69 | my $dup = shift; 70 | my ($fh,$name) = mktmp; 71 | dupout $dup, $fh; 72 | return $name; 73 | } 74 | 75 | dupout 'XSTDOUT', 'STDOUT'; 76 | dupout 'XSTDERR', 'STDERR'; 77 | sub runtmp { 78 | wail "running @_" if $opt{v}; 79 | my $out = tmpf 'STDOUT'; 80 | my $err = tmpf 'STDERR'; 81 | my $x = system @_; 82 | dupout 'STDOUT', 'XSTDOUT'; 83 | dupout 'STDERR', 'XSTDERR'; 84 | return ($out, $err, $x); 85 | } 86 | 87 | my ($dashh,$dash) = mktmp; 88 | print $dashh "----\n"; 89 | close $dashh; 90 | 91 | RETRY: for (;;) { 92 | my ($diffout,$differr,$diffx) = runtmp @nsdiff; 93 | system 'cat', $dash, $diffout, $dash, $differr, $dash 94 | if $opt{v} or ($diffx != 0 && $diffx != 256); 95 | exit 0 if $diffx == 0; 96 | fail "bad exit status from @nsdiff" if $diffx != 256; 97 | open STDIN, '<', $diffout or fale 'open'; 98 | my ($upout,$uperr,$upx) = runtmp @nsupdate; 99 | system 'cat', $dash, $upout, $dash, $uperr, $dash if $opt{v}; 100 | exit 0 if $upx == 0; 101 | if (slurp($uperr) eq "update failed: NXRRSET\n" and $opt{r}--) { 102 | wail "trying again" if $opt{v}; 103 | next RETRY; 104 | } else { 105 | system 'cat', $dash, $diffout, $dash, $differr, 106 | $dash, $upout, $dash, $uperr, $dash unless $opt{v}; 107 | fail "bad exit status from @nsupdate"; 108 | } 109 | } 110 | 111 | __END__ 112 | 113 | =head1 NAME 114 | 115 | nspatch - run `nsdiff | nsupdate` with error handling 116 | 117 | =head1 SYNOPSIS 118 | 119 | nspatch [B<-hVv>] [B<-r> I] 120 | -- [nsdiff options] -- [nsupdate options] 121 | 122 | =head1 DESCRIPTION 123 | 124 | The B utility runs `C` and checks that 125 | both programs complete successfully. It suppresses their output unless 126 | there is an error, in a manner suitable for running from B. 127 | 128 | The B script produced by B includes a prerequisite 129 | check to detect and fail if there is a concurrent update. These 130 | failures are detected by B which retries the update. 131 | 132 | Rather than using a pipe, B uses temporary files to store the 133 | output of B and B. 134 | 135 | =head1 OPTIONS 136 | 137 | =over 138 | 139 | =item B<-h> 140 | 141 | Display this documentation. 142 | 143 | =item B<-V> 144 | 145 | Display version information. 146 | 147 | =item B<-r> I 148 | 149 | If the update fails because of a concurrent update, B will 150 | retry up to I times. The default retry I is 2. 151 | 152 | =item B<-v> 153 | 154 | Turn on verbose mode, so the output from B and B is 155 | printed even if they are successful. (By default it is suppressed.) 156 | 157 | The verbose option is passed on to B. If B is not 158 | given the B<-v> option, it passes the B<-v ''> option to B. If 159 | B is given the B<-v> option, it passes the B<-v 'qr'> option 160 | to B. 161 | 162 | =back 163 | 164 | =head1 EXIT STATUS 165 | 166 | The B utility returns 0 if no change is required or if the 167 | update is successful, or 1 if there is an error. 168 | 169 | =head1 ENVIRONMENT 170 | 171 | =over 172 | 173 | =item C 174 | 175 | Location for temporary files. 176 | 177 | =back 178 | 179 | =head1 VERSION 180 | 181 | This is nspatch-1.85 182 | 183 | Written by Tony Finch 184 | at Cambridge University Information Services. 185 | You may do anything with this. It has no warranty. 186 | 187 | =head1 SEE ALSO 188 | 189 | nsdiff(1), nsupdate(1), cron(8) 190 | 191 | =cut 192 | -------------------------------------------------------------------------------- /nsvi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | # SPDX-License-Identifier: 0BSD OR MIT-0 3 | 4 | use warnings; 5 | use strict; 6 | 7 | use File::Temp qw(tempfile); 8 | use Getopt::Std; 9 | use POSIX; 10 | 11 | sub wail { warn "nsvi: @_\n"; } 12 | sub fail { die "nsvi: @_\n"; } 13 | sub fale { die "nsvi: @_: $!\n"; } 14 | 15 | sub version { 16 | while () { 17 | print if m{^=head1 VERSION} ... m{^=head1 } 18 | and not m{^=head1 }; 19 | } 20 | exit; 21 | } 22 | 23 | sub usage { 24 | print STDERR < 26 | Transfer a zone from its server, edit it, then 27 | upload the edits using `nsdiff` and `nsupdate`. 28 | nsvi options: 29 | -h display full documentation 30 | -V display version information 31 | -n interactive confirmation 32 | -v turn on verbose output 33 | -01cCdD nsdiff options 34 | -S num|mode SOA serial number or mode 35 | -s server[#port] where to AXFR and UPDATE the zone 36 | -g use GSS-TSIG for UPDATE 37 | -k keyfile AXFR and UPDATE TSIG key 38 | -y [hmac:]name:key AXFR and UPDATE TSIG key 39 | EOF 40 | exit 1; 41 | } 42 | my %opt; 43 | usage unless getopts '-hV01cCdDgk:ns:S:vy:', \%opt; 44 | version if $opt{V}; 45 | exec "perldoc -oterm -F $0" if $opt{h}; 46 | usage if @ARGV != 1; 47 | my $zone = shift; 48 | 49 | $opt{v} = 'qr' if $opt{v}; 50 | 51 | my @dig = qw{dig +multiline +onesoa +nocmd +nostats +noadditional}; 52 | push @dig, map "-$_$opt{$_}", 53 | grep $opt{$_}, qw{k y}; 54 | if ($opt{s} and $opt{s} =~ m{^(.*)#(\d+)$}) { 55 | push @dig, "-p$2", "\@$1"; 56 | } elsif ($opt{s}) { 57 | push @dig, "\@$opt{s}"; 58 | } else { 59 | push @dig, "\@localhost"; 60 | } 61 | 62 | my @nsdiff = qw{nsdiff}; 63 | push @nsdiff, map "-$_", 64 | grep $opt{$_}, qw{0 1 c d D}; 65 | push @nsdiff, map "-$_$opt{$_}", 66 | grep $opt{$_}, qw{k s S v y}; 67 | push @nsdiff, "-slocalhost" unless $opt{s}; 68 | push @nsdiff, "-u" if $opt{s}; 69 | 70 | my @nsupdate = qw{nsupdate}; 71 | push @nsupdate, map "-$_", 72 | grep $opt{$_}, qw{g}; 73 | push @nsupdate, map "-$_$opt{$_}", 74 | grep $opt{$_}, qw{k y}; 75 | push @nsupdate, "-l" unless $opt{s}; 76 | 77 | my $secRRtypes = qr{NSEC|NSEC3|NSEC3PARAM|RRSIG}; 78 | $secRRtypes = qr{$secRRtypes|CDS|CDNSKEY} unless $opt{C}; 79 | $secRRtypes = qr{$secRRtypes|DNSKEY} unless $opt{D}; 80 | $secRRtypes = qr{$secRRtypes|DS} if $opt{d}; 81 | 82 | my $nl = qr{(?:;[^\n]*)?\n}; 83 | my $rdata = qr{(?:[^()\n]+ 84 | |(?:[(] 85 | (?:[^()\n]+|$nl)+ 86 | [)])+ 87 | )+$nl}x; 88 | my $dnssec = qr{(?m)^\S+\s+\d+\s+IN\s+($secRRtypes)\s+$rdata}; 89 | 90 | print "@dig axfr $zone" if $opt{v}; 91 | my $axfr = qx{@dig axfr $zone}; 92 | fail "failed to @dig axfr $zone" unless $axfr and $? == 0; 93 | 94 | $axfr =~ s{$dnssec}{}g; 95 | 96 | my ($fh,$fn) = tempfile("$zone.XXXXXXXXXX", 97 | TMPDIR => 1, UNLINK => 1); 98 | print $fh $axfr; 99 | close $fh; 100 | 101 | my $vi = $ENV{VISUAL} || $ENV{EDITOR} || "vi"; 102 | 103 | sub prompt { 104 | print shift; 105 | system "stty -icanon"; 106 | sysread STDIN, my $key, 1; 107 | system "stty icanon"; 108 | print "\n"; 109 | return $key; 110 | } 111 | 112 | sub retry { 113 | wail shift; 114 | my $key = prompt "re-edit and try again? (y/N) "; 115 | next RETRY if $key =~ m{[Yy]}; 116 | exit 1; 117 | } 118 | 119 | RETRY: for (;;) { 120 | system "$vi $fn"; 121 | fail "failed to $vi $fn" unless $? == 0; 122 | 123 | print "@nsdiff $zone $fn" if $opt{v}; 124 | my $diff = qx{@nsdiff $zone $fn}; 125 | if ($? == 0) { 126 | wail "no change"; 127 | exit 0; 128 | } 129 | retry "failed to @nsdiff $zone $fn" 130 | unless $diff and $? == 256; 131 | if ($opt{n}) { 132 | print "$diff\n"; 133 | my $key = prompt "make update, edit again, or quit? (u/e/Q) "; 134 | next RETRY if $key =~ m{[EeRr]}; 135 | exit 1 unless $key =~ m{[UuYy]}; 136 | } 137 | open my $ph, '|-', @nsupdate 138 | or retry "pipe to @nsupdate: $!"; 139 | print $ph $diff; 140 | last if close $ph; 141 | retry "pipe to @nsupdate: $!" if $!; 142 | retry "failed to @nsupdate"; 143 | } 144 | 145 | print "done\n" if $opt{v}; 146 | exit 0; 147 | 148 | __END__ 149 | 150 | =encoding utf8 151 | 152 | =head1 NAME 153 | 154 | nsvi - transfer a zone, edit it, then upload the edits 155 | 156 | =head1 SYNOPSIS 157 | 158 | nsvi [B<-01cCdDghvV>] [B<-k> I] [B<-y> [I:]I:I] 159 | [B<-S> I|I] [B<-s> I] > 160 | 161 | =head1 DESCRIPTION 162 | 163 | The B program makes an AXFR request for the zone, runs your 164 | editor so you can make whatever changes you require, then it runs 165 | B | B to push those changes to the server. 166 | 167 | Automatically-maintained DNSSEC records are stripped from the zone 168 | before it is passed to your editor, and you do not need to manually 169 | adjust the SOA serial number. 170 | 171 | =head1 OPTIONS 172 | 173 | Most B options are passed to B and some to B. 174 | 175 | =over 176 | 177 | =item B<-h> 178 | 179 | Display this documentation. 180 | 181 | =item B<-V> 182 | 183 | Display version information. 184 | 185 | =item B<-v> 186 | 187 | Verbose mode. 188 | 189 | =item B<-n> 190 | 191 | Interactive confirmation. 192 | 193 | When you quit the editor, you will be shown the changes, then asked 194 | whether to make the update (press B or B), edit again (press 195 | B or B), or quit (press another key). 196 | 197 | =item B<-01cCdD> 198 | 199 | =item B<-S> B|I 200 | 201 | These options are passed to B. 202 | For details see the nsdiff manual. 203 | 204 | =item B<-s> I[#I] 205 | 206 | Transfer the zone from the server given in this option, and send the 207 | update request to the same place. You can specify the server host name 208 | or IP address, optionally followed by a "#" and the port number. 209 | 210 | If you do not use the B<-s> option, the zone will be transferred 211 | from I, and B will use B B<-l> to update 212 | the zone. 213 | 214 | =item B<-g> 215 | 216 | Passed to B to use GSS-TSIG for UPDATE. 217 | 218 | =item B<-k> I 219 | 220 | TSIG key file, passed to B, B, and B. 221 | 222 | =item B<-y> [I:]I:I 223 | 224 | Literal TSIG key, passed to B, B, and B. 225 | 226 | =back 227 | 228 | =head1 ENVIRONMENT 229 | 230 | =over 231 | 232 | =item B 233 | 234 | Location for temporary files. 235 | 236 | =item B 237 | 238 | =item B 239 | 240 | Which editor to use. C<$VISUAL> is used if it is set, 241 | otherwise C<$EDITOR>, otherwise B. 242 | 243 | =back 244 | 245 | =head1 VERSION 246 | 247 | This is nsvi-1.85 248 | 249 | Written by Tony Finch 250 | at Cambridge University Information Services. 251 | You may do anything with this. It has no warranty. 252 | 253 | =head1 ACKNOWLEDGMENTS 254 | 255 | Thanks to Tristan Le Guern for the B<-n> option and Mantas Mikulėnas 256 | for the B<-g> option. Thanks to David McBride and Petr Menšík for 257 | providing useful feedback. 258 | 259 | =head1 SEE ALSO 260 | 261 | nsdiff(1), nsupdate(1), dig(1). 262 | 263 | =cut 264 | -------------------------------------------------------------------------------- /reversion.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # SPDX-License-Identifier: 0BSD OR MIT-0 3 | 4 | case $# in 5 | (1) N=$1 6 | ;; 7 | (2) N=$2 8 | ;; 9 | (*) echo 1>&2 'usage: reversion.sh [--commit] ' 10 | exit 1 11 | ;; 12 | esac 13 | 14 | perl -pi -e 's{(ns(diff|patch|vi)-)[0-9.]+[0-9]}{${1}'$N'}' \ 15 | README.pod nsdiff nspatch nsvi lib/DNS/nsdiff.pm 16 | 17 | perl -pi -e 's{(VERSION\s+=>?\s+|\tgrep\s+)"[0-9.]+[0-9]"}{${1}"'$N'"}' \ 18 | Makefile.PL lib/DNS/nsdiff.pm 19 | 20 | case $# in 21 | (1) git diff 22 | ;; 23 | (2) V=nsdiff-$N 24 | git commit -a -m $V 25 | git tag -a -m $V $V 26 | ;; 27 | esac 28 | --------------------------------------------------------------------------------