#!/usr/bin/perl use strict; use warnings; use Martnet::DDNS; use File::Temp qw/tempfile/; # Optional positional "force" flag (legacy behavior): # sync-slave [force] # If more than 10 changes would be made, the script will refuse unless force is set. my $force = shift; my $ddns = Martnet::DDNS->new(); # Pull all control-plane entries, then filter out datasets that are not zones. # In the new model, _config is metadata and must never appear in slave zone stanzas. # Also guard against any malformed 'master' that could result in invalid BIND config. my @vh_all = $ddns->get(); my @vh = grep { $_->{type} ne '_config' && defined($_->{zone}) && $_->{zone} ne '' && defined($_->{master}) && $_->{master} ne '' && $_->{master} !~ /^\s*[\{\[]/ # never allow JSON into masters { ... } } @vh_all; my %vhh = map { $_->{zone} => 1 } @vh; my @all = parse_slavefile("/etc/bind/martnet.slave.zones.9"); my %allh = all_zones_hash(@all); # For each managed zone, see if it's already present with the same masters list. my $changecount = 0; foreach my $i (@vh) { unless (contains_zone($i, @all)) { print "don't have $i->{zone}\n"; $changecount++; } } # For each zone currently in the file, see if it's still managed. foreach my $z (keys %allh) { $changecount++ unless ($vhh{$z}); } die "Cowardly refusing to make a big update automatically [$changecount]" if ($changecount > 10 && !$force); if ($changecount) { do_rewrite(@vh); } exit 0; sub parse_slavefile { my ($f) = @_; my @ret; open(my $fh, $f) || die "Can't open $f: $!"; while (<$fh>) { # Example generated line: # zone "example.com" { type slave; file "..."; masters { 1.2.3.4;5.6.7.8; }; allow-notify {...}; }; if (/^zone\s+\"([^\"]+)\"\s+\{.+masters\s?\{\s?([^\}]+);\s?\};/) { push(@ret, { zone => $1, master => $2 }); } } close $fh; return @ret; } sub do_rewrite { my (@vh) = @_; my ($fh, $path) = tempfile(); print "Differences found; rewriting slave file.\n"; foreach my $i (sort { $a->{zone} cmp $b->{zone} } @vh) { die "No master(s) found for slave zone $i->{zone}" unless defined($i->{master}) && $i->{master} ne '' && $i->{master} !~ /^\s*[\{\[]/; print $fh "zone \"$i->{zone}\" { type slave; file \"/var/cache/bind/db.$i->{zone}\"; masters { $i->{master}; }; allow-notify {key \"notify-key\";}; };\n"; } close $fh; print "Installing new slave host list\n"; my $dest = "/etc/bind/martnet.slave.zones.9"; my ($old_uid, $old_gid) = (0, 0); if (-e $dest) { $old_uid = (lstat $dest)[4]; $old_gid = (lstat $dest)[5]; } system("install -o $old_uid -g $old_gid $path $dest") == 0 or die "install failed: $?"; print "Reloading DNS files\n"; system("/usr/sbin/rndc reload") == 0 or die "rndc reload failed: $?"; } sub all_zones_hash { my (@zl) = @_; my %ret; foreach my $i (@zl) { $ret{$i->{zone}}++; } return %ret; } sub contains_zone { my ($zone, @zl) = @_; foreach my $i (@zl) { if ($i->{zone} eq $zone->{zone}) { print "m: '$i->{master}' ne '$zone->{master}'\n" unless ($i->{master} eq $zone->{master}); return 1 if ($i->{master} eq $zone->{master}); } } return 0; }