package Martnet::DDNS; use strict; use warnings; use Net::DNS; use File::Temp qw/tempfile/; use Memoize; memoize('_gethosts'); our $VERSION = '0.4'; our $misterdns = '198.251.79.234'; sub new { my $me = shift; my %opts = @_; return bless { 'keyfile' => '/etc/bind/Kprivate.invalid.+157+14348.key', %opts }, $me; } sub _validateTypeOrDie { my ($t) = @_; die "Invalid type" unless ($t =~ /^_(vhosts|pureslave|custom|dnssec)$/); } sub _fqdn { my ($dom, $type) = @_; $type ||= '_vhosts'; _validateTypeOrDie($type); return $dom . "$type.private.invalid."; } sub _validateOrDie { my ($dom) = @_; die "No domain provided" unless $dom; die "Invalid domain name (must end in a dot)" unless ($dom =~ /^[a-zA-Z0-9\.\-\_]+\.$/); } sub _lookupOrDie { my ($dom, $type) = @_; _validateOrDie($dom); my $fqdn = _fqdn($dom, $type); my $res = Net::DNS::Resolver->new; my $query = $res->query($fqdn, "TXT"); if ($query) { foreach my $rr (grep { $_->type eq 'TXT' } $query->answer) { # We found a record; that's all that I care about. #print $rr->nsdname, "\n"; return 1; } } else { die "query failed: ", $res->errorstring, "\n"; } die "Failed to find existing DNS record for $fqdn"; } sub __docmd { my ($this, $cmd) = @_; my ($tmpfh, $filename) = tempfile(); close $tmpfh; my $fh; open($fh, "|nsupdate -k $this->{keyfile} > $filename") || die "Can't open nsupdate: $!"; print $fh "server localhost\nzone private.invalid.\n$cmd\nshow\nsend\n"; close $fh; open($fh, $filename) || die "Can't re-open tmpfile $filename: $!"; while (<$fh>) { print; } close $fh; unlink $filename; } sub _gethosts { my ($this, $type) = @_; unless (!defined($type)) { _validateTypeOrDie($type); } my $fh; open($fh, "dig -t AXFR \@${misterdns} private.invalid. |") || die "Can't open dig: $!"; my @vh; while (<$fh>) { if ($type) { if (/^(\S+)\.$type\.private\.invalid\.\s+\d+\s+IN\s+TXT\s+\"(.+)\"$/) { my ($z, $m) = ($1, $2); $m =~ s/\"//g; push (@vh, { zone => $z, type => $type, master => $m }); } } else { # Querying everything if (/^(\S+)\.(\S+)\.private\.invalid\.\s+\d+\s+IN\s+TXT\s+\"(.+)\"$/) { my ($z, $t, $m) = ($1, $2, $3); $m =~ s/\"//g; push (@vh, { zone => $z, type => $t, master => $m }); } } } return @vh; } # Find the type of domin that $dom is. If we don't find it, return # undef. (The domain $dom ends in a dot; the DNS info we find won't; # hence the concat of the extra "." after the lc.) # (Skip _dnssec records in this check.) sub type { my ($this, $dom) = @_; _validateOrDie($dom); my @vh = $this->get(); foreach my $i (@vh) { if (lc($i->{zone})."." eq lc($dom)) { return $i->{type} unless ($i->{type} eq '_dnssec'); } } # Didn't find it. return undef; } sub add { my ($this, $dom, $master, $type) = @_; _validateOrDie($dom); my $fqdn = _fqdn($dom, $type); if (my $existingtype = $this->type($dom)) { die "Domain $dom already exists [of type $existingtype]" unless ($existingtype eq '_dnssec' || $type eq '_dnssec'); } $this->__docmd("update add $fqdn 60 TXT $master"); $this->cleanup(); } sub del { my ($this, $dom, $type) = @_; _lookupOrDie($dom, $type); my $fqdn = _fqdn($dom, $type); $this->__docmd("update delete $fqdn TXT"); $this->cleanup(); } sub get { my ($this, $type) = @_; return $this->_gethosts($type); } sub cleanup { my ($this) = @_; # Merge the .jnl file in with the domain file system("rndc sync -clean private.invalid"); # Rebuild and reload the master vhosts files system("/usr/local/bin/sync-master-vhosts"); } sub is_dnssec { my ($this, $dom) = @_; $dom =~ s/^(.+)\.$/$1/; # remove trailing dot my @h = $this->_gethosts('_dnssec'); foreach my $i (@h) { if (lc($i->{zone}) eq $dom) { return 1; } } return 0; } 1;