├── .gitignore ├── Makefile ├── README.md ├── debian ├── changelog ├── control ├── copyright ├── rules └── source │ └── format └── dnsvi /.gitignore: -------------------------------------------------------------------------------- 1 | dnsvi.1 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX = /usr 2 | POD2MAN=pod2man --date " " --center "" -r "" 3 | 4 | all: dnsvi.1 5 | 6 | dnsvi.1: dnsvi 7 | $(POD2MAN) --quotes=none --section 1 $< $@ 8 | 9 | install: 10 | install -D dnsvi $(DESTDIR)$(PREFIX)/bin/dnsvi 11 | install -D -m644 dnsvi.1 $(DESTDIR)$(PREFIX)/share/man/man1/dnsvi.1 12 | 13 | clean: 14 | rm -f dnsvi.1 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | dnsvi - edit dynamic DNS zones in vi 2 | ==================================== 3 | 4 | dnsvi is a frontend for nsupdate. Given a DNS zone name, it uses dig -t AXFR to 5 | get all the records in a zone. It then spawns your favorite editor, and upon 6 | completion, builds a list of "update add" and "update delete" statements to 7 | feed to nsupdate. 8 | 9 | Usage: 10 | 11 | ./dnsvi [-igl] [-k keyfile] [-p port] [@nameserver] 12 | 13 | Dependencies: 14 | 15 | * perl 16 | * Sort::Naturally (Debian: libsort-naturally-perl) 17 | * dig, nsupdate (Debian: dnsutils) 18 | * some $EDITOR (Default: sensible-editor) 19 | 20 | Screenshot 21 | ---------- 22 | 23 | $ dnsvi -k dyn.df7cb.de.key @ns.df7cb.de dyn.df7cb.de 24 | [...vi...] 25 | nsupdate commands queued: 26 | server ns.df7cb.de 27 | zone dyn.df7cb.de 28 | update delete fermi.dyn.df7cb.de. IN A 127.0.0.1 29 | update add lehmann.dyn.df7cb.de. 600 IN A 127.0.0.1 30 | update add volta.dyn.df7cb.de. 2419200 IN SSHFP 3 1 DC66C1C5E9ED611FBDF0A9E1F701B1F8C38A6C1D 31 | send 32 | answer 33 | 34 | [S]end, [e]dit, send and edit [a]gain, [q]uit: [s] 35 | 36 | IDN Support 37 | ----------- 38 | 39 | If you are updating a punycode domain and you are using bind9 >= 9.14.0, you 40 | need to specify `-i` to be able to update the zone, otherwise there will be a 41 | mix of punycode and non-punycode domains which nsupdate will refuse. 42 | 43 | Author 44 | ------ 45 | 46 | Christoph Berg 47 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | dnsvi (1.5) unstable; urgency=medium 2 | 3 | * Depend on bind9-dnsutils instead of dnsutils. (Closes: #1094943) 4 | 5 | -- Christoph Berg Mon, 10 Feb 2025 10:15:18 +0100 6 | 7 | dnsvi (1.4) unstable; urgency=medium 8 | 9 | [ Debian Janitor ] 10 | * Update standards version, no changes needed. 11 | * Bump debhelper from old 10 to 13. 12 | 13 | [ Christoph Berg ] 14 | * Update package URLs. 15 | 16 | -- Christoph Berg Tue, 31 Aug 2021 16:43:51 +0200 17 | 18 | dnsvi (1.3) unstable; urgency=medium 19 | 20 | * Add --noidnout option. Patch by Richard Schwab, thanks! 21 | 22 | -- Christoph Berg Sun, 28 Jul 2019 22:36:45 +0200 23 | 24 | dnsvi (1.2) unstable; urgency=medium 25 | 26 | * Team upload. 27 | * Support GSS-TSIG/Kerberos using -g. Patch by Christian Haase, thanks! 28 | 29 | -- Christoph Berg Wed, 06 Sep 2017 12:05:16 +0200 30 | 31 | dnsvi (1.1) unstable; urgency=medium 32 | 33 | * New upstream version. 34 | 35 | [ Bernhard Schmidt ] 36 | * Set zone for nsupdate explicitly. 37 | * Allow port specification. 38 | 39 | -- Christoph Berg Tue, 20 Jun 2017 17:11:54 +0200 40 | 41 | dnsvi (1.0) unstable; urgency=medium 42 | 43 | * Initial release. 44 | 45 | -- Christoph Berg Wed, 24 Feb 2016 12:28:41 +0100 46 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: dnsvi 2 | Section: admin 3 | Priority: optional 4 | Maintainer: Christoph Berg 5 | Standards-Version: 4.7.0 6 | Build-Depends: debhelper-compat (= 13) 7 | Homepage: https://github.com/df7cb/dnsvi 8 | Vcs-Git: https://github.com/df7cb/dnsvi.git 9 | Vcs-Browser: https://github.com/df7cb/dnsvi 10 | 11 | Package: dnsvi 12 | Architecture: all 13 | Depends: ${shlibs:Depends}, 14 | ${misc:Depends}, 15 | bind9-dnsutils, 16 | vim | editor, 17 | libsort-naturally-perl, 18 | sensible-utils 19 | Description: edit dynamic DNS zones using vi 20 | dnsvi is a frontend for nsupdate. Given a DNS zone name, it uses dig -t AXFR 21 | to get all the records in a zone. It then spawns your favorite editor, and 22 | upon completion, builds a list of "update add" and "update delete" statements 23 | to feed to nsupdate. 24 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: dnsvi 3 | Source: https://github.com/df7cb/dnsvi 4 | 5 | Files: * 6 | Copyright: 2014-2016 (C) Christoph Berg 7 | License: MIT 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | . 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | . 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | %: 4 | dh $@ 5 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /dnsvi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | # Copyright (c) 2014-2019 Christoph Berg 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | use strict; 24 | use warnings; 25 | use File::Temp qw(tempfile); 26 | use Getopt::Long; 27 | use Sort::Naturally; 28 | 29 | ## option parsing 30 | 31 | sub usage { 32 | my $exit = shift; 33 | print "Usage: $0 [-l] [-k keyfile] [-g] [-p port] [\@nameserver] [-i] \n"; 34 | exit $exit; 35 | } 36 | 37 | my $keyfile = ''; 38 | my $local; 39 | my $gssapi; 40 | my $nameserver; 41 | my $port; 42 | my $noidnout; 43 | 44 | Getopt::Long::config('bundling'); 45 | if (!GetOptions ( 46 | '-h' => sub { usage(0) }, 47 | '--help' => sub { usage(0) }, 48 | '-k=s' => \$keyfile, 49 | '--key-file=s' => \$keyfile, 50 | '-l' => \$local, 51 | '-g' => \$gssapi, 52 | '--gssapi' => \$gssapi, 53 | '-p=i', => \$port, 54 | '--port=i', => \$port, 55 | '-i', => \$noidnout, 56 | '--noidnout', => \$noidnout, 57 | )) { 58 | usage(1); 59 | }; 60 | 61 | if (@ARGV > 0 and $ARGV[0] =~ /^@(.+)/) { 62 | $nameserver = $1; 63 | shift; 64 | } 65 | if (@ARGV != 1) { 66 | usage(1); 67 | } 68 | my $zone = shift; 69 | $zone =~ s/\.$//; # remove trailing dot 70 | 71 | my @dig = ("dig", "-t", "AXFR", "+nostats", $zone); 72 | push @dig, "\@$nameserver" if ($nameserver); 73 | push @dig, "\@localhost" if ($local); 74 | push @dig, "-k", $keyfile if ($keyfile); 75 | push @dig, "-p", $port if ($port); 76 | push @dig, "+noidnout" if ($noidnout); 77 | 78 | my @nsupdate = qw(nsupdate); 79 | push @nsupdate, "-l" if ($local); 80 | push @nsupdate, "-k", $keyfile if ($keyfile); 81 | push @nsupdate, "-g" if ($gssapi); 82 | 83 | ## functions 84 | 85 | # remove all $keys from d 86 | sub prune ($$) 87 | { 88 | my ($d, $key) = @_; 89 | foreach my $name (keys %$d) { 90 | foreach my $class (keys %{$d->{$name}}) { 91 | foreach my $type (keys %{$d->{$name}->{$class}}) { 92 | my $rrset = $d->{$name}->{$class}->{$type}; 93 | foreach my $data (keys %$rrset) { 94 | delete $rrset->{$data}->{$key}; 95 | } 96 | } 97 | } 98 | } 99 | } 100 | 101 | # parse one AXFR output line, return interesting lines (or undef) 102 | sub parse ($$) 103 | { 104 | my ($zone, $line) = @_; 105 | my ($name, $ttl, $class, $type, $data) = split /\s+/, $line, 5; 106 | if (not defined $data) { 107 | print STDERR "Couldn't parse line $.: $line\n"; 108 | return undef; 109 | } 110 | if ($type =~ /^(RRSIG|NSEC|NSEC3|TSIG|TYPE65534)$/) { 111 | # ignore signatures that are generated automatically anyway 112 | return undef; 113 | } 114 | if ($name eq "$zone.") { 115 | $name = '@'; 116 | $line =~ s/^\Q$zone\E\.\s/@\t/; 117 | } else { 118 | $name =~ s/\.\Q$zone\E\.$//; 119 | $line =~ s/\.\Q$zone\E\.\s/\t/; 120 | } 121 | return ($line, $name, $ttl, $class, $type, $data); 122 | } 123 | 124 | # load AXFR output 125 | sub load_file ($$$$) 126 | { 127 | my ($zone, $d, $key, $fh) = @_; 128 | my $rrs = 0; 129 | while (my $line = <$fh>) { 130 | chomp $line; 131 | # dirac.df7cb.de. 7200 IN CNAME dirac.dyn.df7cb.de. 132 | next if ($line =~ /^(;|$)/); 133 | my ($line2, $name, $ttl, $class, $type, $data) = parse ($zone, $line); 134 | next unless (defined $data); 135 | $d->{$name}->{$class}->{$type}->{$data}->{$key} = $ttl; 136 | $rrs++; 137 | } 138 | close $fh; 139 | return $rrs; 140 | } 141 | 142 | sub write_file ($$$) 143 | { 144 | my ($d, $key, $fh_out) = @_; 145 | my ($name_w, $class_w, $type_w, $ttl_w) = (1, 1, 1, 1); 146 | 147 | print $fh_out "; $zone - vim:ft=bindzone:\n"; 148 | 149 | # calculate lengths of output columns 150 | foreach my $name (keys %$d) { 151 | $name_w = length $name if (length $name > $name_w); 152 | foreach my $class (keys %{$d->{$name}}) { 153 | $class_w = length $class if (length $class > $class_w); 154 | foreach my $type (keys %{$d->{$name}->{$class}}) { 155 | $type_w = length $type if (length $type > $type_w); 156 | foreach my $data (keys %{$d->{$name}->{$class}->{$type}}) { 157 | next unless (exists $d->{$name}->{$class}->{$type}->{$data}->{$key}); 158 | my $ttl = $d->{$name}->{$class}->{$type}->{$data}->{$key}; 159 | $ttl_w = length $ttl if (length $ttl > $ttl_w); 160 | } 161 | } 162 | } 163 | } 164 | 165 | foreach my $name (nsort keys %$d) { 166 | foreach my $class (nsort keys %{$d->{$name}}) { 167 | foreach my $type (nsort keys %{$d->{$name}->{$class}}) { 168 | foreach my $data (nsort keys %{$d->{$name}->{$class}->{$type}}) { 169 | next unless (exists $d->{$name}->{$class}->{$type}->{$data}->{$key}); 170 | my $ttl = $d->{$name}->{$class}->{$type}->{$data}->{$key}; 171 | printf $fh_out "%-${name_w}s %-${ttl_w}s %-${class_w}s %-${type_w}s %s\n", 172 | $name, $ttl, $class, $type, $data; 173 | } 174 | } 175 | } 176 | } 177 | 178 | close $fh_out; 179 | } 180 | 181 | sub compare ($$$$) 182 | { 183 | my ($zone, $d, $key1, $key2) = @_; 184 | my @cmds; 185 | foreach my $name (nsort keys %$d) { 186 | my $fqdn = $name; 187 | $fqdn = "$zone." if ($fqdn eq '@'); 188 | $fqdn .= ".$zone." unless ($fqdn =~ /\.$/); 189 | foreach my $class (nsort keys %{$d->{$name}}) { 190 | foreach my $type (nsort keys %{$d->{$name}->{$class}}) { 191 | my $rrset = $d->{$name}->{$class}->{$type}; 192 | foreach my $data (nsort keys %$rrset) { 193 | my $rr = $rrset->{$data}; 194 | if (exists $rr->{$key1} and exists $rr->{$key2} and $rr->{$key1} == $rr->{$key2}) { 195 | # nothing to do 196 | next; 197 | } 198 | if (exists $rr->{$key1}) { # removed (or changed) 199 | push @cmds, "update delete $fqdn $class $type $data\n"; 200 | } 201 | if (exists $rr->{$key2}) { # added (or changed) 202 | push @cmds, "update add $fqdn $rr->{$key2} $class $type $data\n"; 203 | } 204 | } 205 | } 206 | } 207 | } 208 | return \@cmds; 209 | } 210 | 211 | ## main 212 | 213 | my ($fh, $filename) = tempfile( "$zone.XXXXXX", TMPDIR => 1, UNLINK => 1 ); 214 | my $d = {}; 215 | my ($key1, $key2) = (1, 2); 216 | 217 | open F, "-|", @dig; 218 | my $rrs = load_file ($zone, $d, $key1, *F); 219 | my $rc = $? >> 8; 220 | if ($rrs == 0 or $rc > 0) { 221 | open F, $filename; 222 | print STDERR ; 223 | close F; 224 | exit ($rc || 1); 225 | } 226 | write_file ($d, $key1, $fh); 227 | 228 | do { 229 | my $mtime = (stat $filename)[9]; 230 | my $editor = $ENV{EDITOR} || 'sensible-editor'; 231 | system $editor, $filename; 232 | 233 | if ((stat $filename)[9] == $mtime) { 234 | print "No change.\n"; 235 | exit 0; 236 | } 237 | 238 | open F, $filename or die "$filename: $!"; 239 | load_file ($zone, $d, $key2, *F); 240 | 241 | my $cmds = compare ($zone, $d, $key1, $key2); 242 | 243 | unless (@$cmds) { 244 | print "No change.\n"; 245 | exit 0; 246 | } 247 | unshift @$cmds, "zone $zone\n"; 248 | if ($port) { 249 | unshift @$cmds, "server $nameserver $port\n" if ($nameserver); 250 | } else { 251 | unshift @$cmds, "server $nameserver\n" if ($nameserver); 252 | } 253 | push @$cmds, "send\nanswer\n\n"; 254 | print "nsupdate commands queued:\n"; 255 | print @$cmds; 256 | 257 | print "[S]end, [e]dit, send and edit [a]gain, [q]uit: [s] "; 258 | my $response = ; 259 | print "\n"; 260 | 261 | if ($response =~ /^(s|y|$)/i) { 262 | open F, "|-", @nsupdate; 263 | print F @$cmds; 264 | close F; 265 | my $rc = $? >> 8; 266 | exit 0 if ($rc == 0); 267 | print "nsupdate returned $rc, press enter to edit again "; 268 | ; 269 | print "\n"; 270 | } elsif ($response =~ /^e/i) { 271 | } elsif ($response =~ /^a/i) { 272 | open F, "|-", @nsupdate; 273 | print F @$cmds; 274 | close F; 275 | my $rc = $? >> 8; 276 | print "nsupdate returned $rc, press enter to edit again "; 277 | ; 278 | print "\n"; 279 | if ($rc == 0) { 280 | $key1++; 281 | $key2++; 282 | } 283 | } elsif ($response =~ /^q/i) { 284 | exit 0; 285 | } 286 | prune ($d, $key2); 287 | } while (1); 288 | 289 | __END__ 290 | 291 | =head1 NAME 292 | 293 | dnsvi - Edit dynamic DNS zones using vi 294 | 295 | =head1 SYNOPSIS 296 | 297 | B [B<-igl>] [B<-k> I] [B<-p> I] [B<@>I] I 298 | 299 | =head1 DESCRIPTION 300 | 301 | B is a frontend for B. Given a DNS zone name, it uses 302 | B to get all the records in a zone. It then spawns your favorite 303 | editor, and upon completion, builds a list of "B" and 304 | "B" statements to feed to nsupdate. 305 | 306 | =head1 OPTIONS 307 | 308 | =over 4 309 | 310 | =item B<-i> B<--noidnout> 311 | 312 | If you are updating a punycode domain and you are using bind9 >= 9.14.0, you 313 | need to specify B<-i> to be able to update the zone, otherwise there will be a 314 | mix of punycode and non-punycode domains which nsupdate will refuse. 315 | 316 | =item B<-l> 317 | 318 | Use B as nameserver and pass B<-l> (local) to B. 319 | 320 | =item B<-k> I 321 | 322 | Use I for B and B. 323 | 324 | =item B<-g> 325 | 326 | Use B credentials for B. See B<-g> in B(1) for details. 327 | 328 | =item B<-p> I 329 | 330 | Use I for B and B (default: 53). 331 | 332 | =item B<@>I 333 | 334 | Query I for zone data and send updates there. 335 | 336 | =back 337 | 338 | =head1 ENVIRONMENT 339 | 340 | =over 4 341 | 342 | =item B 343 | 344 | Editor to use instead of B. 345 | 346 | =back 347 | 348 | =head1 EXAMPLE 349 | 350 | $ dnsvi -k dyn.df7cb.de.key @ns.df7cb.de dyn.df7cb.de 351 | [...vi...] 352 | nsupdate commands queued: 353 | server ns.df7cb.de 354 | zone dyn.df7cb.de 355 | update delete fermi.dyn.df7cb.de. IN A 127.0.0.1 356 | update add lehmann.dyn.df7cb.de. 600 IN A 127.0.0.1 357 | update add volta.dyn.df7cb.de. 2419200 IN SSHFP 3 1 DC66C1C5E9ED611FBDF0A9E1F701B1F8C38A6C1D 358 | send 359 | answer 360 | 361 | [S]end, [e]dit, send and edit [a]gain, [q]uit: [s] 362 | 363 | =head1 SEE ALSO 364 | 365 | L, L. 366 | 367 | =head1 AUTHOR 368 | 369 | Christoph Berg Lmyon@debian.orgE> 370 | --------------------------------------------------------------------------------