scripts/rdns-manager

252 lines
9.3 KiB
Perl
Executable File

#!/usr/bin/env perl
# License: 3-Clause BSD. Author: Matthew Connelly.
# This is a (formerly Bash, now Perl) script for managing in-addr.arpa and ip6.arpa zones.
# If you have any questions or issues, open an issue at https://bitbucket.org/MaffC/script-collection/issues
use strict;
use warnings;
package DNS::Reverse::Manager;
use feature qw(switch say);
use vars '$VERSION'; $VERSION = '1.0.0'; #Version number
use Data::Validate::IP qw(is_public_ipv4 is_public_ipv6); #for validating v4/v6 addresses
use Getopt::Long qw(:config posix_default bundling pass_through); #for intelligently handling cli arguments
use Net::DNS; #for doing forward and reverse lookups
use Net::IP; #for converting IPs to their reverse zones
use Data::Dumper; #debugging
#conf
my $def_rdns = 'hosted-by.mycompany.com'; #Recomend default is "hosted-by.your-website.tld".
my $def_dns = '8.8.8.8'; #Recommended default is 8.8.8.8 or 4.2.2.1.
my $zone_dir = '/var/named/'; #for cPanel, use /var/named/.
my $zone_ext = ".db"; #Default for most environments is ".db".
my $net_type = "cpanel"; #This was originally written to support cPanel-based DNS environments, and primarily impacts how rdns-manager "syncs".
my $nsd_type = "bind9"; #I might in the future support more than just bind9.
#variables for arguments
my $verify = '';
my $force = '';
my $reset = '';
my $nosync = '';
my $fsync = '';
my $delptr = '';
my $newzone = '';
my $prefixlen = 64;
#other vars
my $made_modifications = '';
#functions
sub nicedie {
print shift."\n";
exit 1;
}
sub validate_domain {
use Data::Validate::Domain qw(is_domain);
my $domain = shift;
return 1 if is_domain $domain;
return 0;
}
sub validate_ip {
my $ip = shift;
return 1 if is_public_ipv4 $ip or is_public_ipv6 $ip;
return 0;
}
sub get_arpa {
my $ip = shift;
if(is_public_ipv4 $ip) {
$ip =~ m/^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$/;
return ($4, "$3.$2.$1.in-addr.arpa");
}
my $len = ($prefixlen/2);
Net::IP->new($ip)->reverse_ip =~ /^(.*)\.(.{$len}ip6\.arpa)\.$/;
return ($1,$2);
}
#TODO make these work for DNS roundrobins. I doubt anyone would be stupid enough to have more than one PTR of the same name
# and i'm not sure if it's even legal, but hey.
sub does_fqdn_match {
my ($fqdn,$ip) = @_;
my $r = Net::DNS::Resolver->new(recurse => 1,tcp_timepit => 5,udp_timeout => 5,nameservers => [$def_dns,]);
my $p = $r->search($fqdn, 'A');
$p = $r->search($fqdn, 'AAAA') unless is_public_ipv4 $ip;
return 0 unless defined $p;
my @res = $p->answer;
#due to IPv6 shortening, we need to use Net::IP here
return 1 unless scalar @res < 1 or Net::IP->new($res[0]->address)->ip ne Net::IP->new($ip)->ip;
return 0;
}
sub confirm_rdns {
my ($fqdn,$ip) = @_;
my ($rec,$zone) = get_arpa $ip;
my $rrec = $rec.".".$zone;
my $r = Net::DNS::Resolver->new(recurse => 1,tcp_timeout => 5,udp_timeout => 5,nameservers => [$def_dns,]);
my $p = $r->search($rrec, 'PTR');
return 0 unless defined $p;
my @res = $p->answer;
return 1 unless scalar @res < 1 or $res[0]->ptrdname."." ne $fqdn;
return 0;
}
sub does_zone_exist {
my $ip = shift;
my ($rec,$zone) = get_arpa $ip;
return -2 if !-e "$zone_dir/$zone$zone_ext";
return -1 if -z "$zone_dir/$zone$zone_ext";
return 0 if !-w "$zone_dir/$zone$zone_ext";
return 1;
}
sub get_zone_array {
use Net::DNS::ZoneFile;
#returns 1 on record exists, 0 on record doesn't exist, -1 on zone exists but isn't writeable, -2 on file exists but isn't a zone, -3 on file doesn't exist
my $ip = shift;
my ($rec,$zone) = get_arpa $ip;
return unless does_zone_exist $ip;
my $zf = new Net::DNS::ZoneFile("$zone_dir/$zone$zone_ext");
my @z = $zf->read;
return @z;
}
sub does_record_exist {
my $ip = shift;
my ($rec,$zone) = get_arpa $ip;
my @z = get_zone_array $ip;
return 0 unless @z;
#I imagine this might be grossly inefficient on large zones (such as fully-populated IPv6 zones).
foreach(@z) {
return 1 if $_->name eq "$rec.$zone";
}
return 0;
}
sub generate_soa_serial {
use POSIX qw(strftime);
my $cur_serial = shift;
my $yyyymmdd = strftime "%Y%m%d", localtime;
return $cur_serial+1 if $cur_serial =~ /^$yyyymmdd[0-9]{2}$/;
return $yyyymmdd."00";
}
sub write_zone {
use File::Copy qw(copy);
use Net::DNS::ZoneParse qw(writezone);
my $zone = shift;
my @z = @_;
foreach(@z) {$_->serial(generate_soa_serial $_->serial) if $_->type eq "SOA";} #update SOA
copy "$zone_dir$zone$zone_ext", "$zone_dir$zone$zone_ext.bak" or print "Warning: Couldn't create a backup of the zone $zone.\n";
open ZONE, ">$zone_dir$zone$zone_ext" or nicedie "Failed to open zonefile for $zone for writing!";
print ZONE writezone @z;
close ZONE or nicedie "Seemingly failed to close $zone$zone_ext, cowardly quitting here.";
}
sub del_ptr {
#I'm so sorry
my $rec = shift;
sub is_match {
my ($rr,$rec) = @_;
return 1 unless $rr->name eq $rec;
return 0;
}
$made_modifications = 1;
write_zone $rec,grep {&is_match(($_,$rec))} @_;
}
sub add_ptr {
my ($ip,$fqdn) = @_;
my ($rec,$zone) = get_arpa $ip;
my @z = get_zone_array $ip;
my $new_rr = Net::DNS::RR->new("$rec.$zone. 3600 IN PTR $fqdn");
push @z,$new_rr;
$made_modifications = 1;
write_zone $zone,@z;
}
sub get_ptr {
my $ip = shift;
return unless does_record_exist $ip;
my ($rec,$zone) = get_arpa $ip;
my @z = get_zone_array $ip;
#More inefficient to repeat the same operation twice even.
foreach(@z) {
return $_->ptrdname if $_->name eq "$rec.$zone";
}
return;
}
sub set_ptr {
my ($ip,$fqdn) = @_;
return add_ptr $ip,$fqdn unless does_record_exist $ip;
my ($record,$zone) = get_arpa $ip;
my @z = get_zone_array $ip;
foreach(@z) {
$_->ptrdname($fqdn) if $_->name eq "$record.$zone";
}
$made_modifications = 1;
write_zone $zone,@z;
return 1;
}
sub sync_cpanel {
return 1;
}
sub do_sync {
my $ip = shift;
my ($rec,$zone) = get_arpa $ip;
my $res = '';
print "Syncing zone $zone... ";
for($net_type) {
$res = sync_cpanel $zone when /cpanel/;
default { nicedie "Couldn't sync $zone: Don't have a known sync method for network type $net_type."; }
}
say (($res) ? "Synchronised" : "Failed");
}
#main
#do argument parsing. all unknown arguments get left in @ARGV so I can `shift`.
GetOptions
'reset-hostname=s' => \$def_rdns,
'dns-server=s' => \$def_dns,
'v|verify-rdns' => \$verify,
'f|force' => \$force,
'r|reset' => \$reset,
'p|populate' => \$newzone,
'd|no-sync' => \$nosync,
's|force-sync' => \$fsync,
'R|remove-ptr' => \$delptr;
#get IP and domain, validate.
my $ip = shift or nicedie "No IP given!";
$prefixlen = $1 if $ip =~ s/\/([0-9]+)//; #split off prefixlen (if given) into variable for later use
nicedie "Invalid IP address '$ip'!" unless validate_ip $ip;
my $domain = shift;
nicedie "Invalid FQDN '$domain'!" if defined $domain and !validate_domain $domain;
$domain =~ s/([a-zA-Z])$/$1./ if defined $domain; #Append final period if it doesn't exist
#Main program flow
#Argument intelligence. Omitting this probably won't impact program flow much but it's important that the user know they're stupid.
nicedie "You seem to have specified both --no-sync and --force-sync. Please make your mind up." if $nosync and $fsync;
nicedie "You seem to have specified some combination of --reset, --remove-ptr and --force. Please make your mind up." if ($reset and $delptr) or (($reset or $delptr) and $force);
nicedie "You seem to have specified arguments that don't make sense together. Please make your mind up." if ($newzone and ($delptr or $reset or $force)) or ($verify and !defined $domain) or (defined $domain and ($newzone or $delptr or $reset));
#Simple check that the zone exists.
for(does_zone_exist $ip) {
my ($trec,$tz) = get_arpa $ip;
nicedie "Authoritative zone for IP $ip doesn't exist! Please create zone $tz or ensure you specified the correct subnet mask if this is an IPv6 address!" when -2;
nicedie "Zonefile $tz (supposedly authoritative for $ip) doesn't appear to be a valid BIND zone. Please check the zonefile and try again." when -1;
nicedie "Authoritative zone for IP $ip exists but we can't write to it. Please check the permissions on the zonefile for $tz." when 0;
}
if(!defined $domain and $reset) {
set_ptr $ip,$def_rdns or nicedie "Failed to set rDNS for $ip to '$def_rdns'!";
print "rDNS set";
print ((confirm_rdns $ip, $def_rdns) ? " and resolving" : " but not yet resolving (check manually with 'host $ip')") if $verify;
print "\n";
} elsif(!defined $domain and $delptr) {
del_ptr $ip or nicedie "Failed to delete PTR record for $ip!";
say "PTR record for IP $ip deleted.";exit;
} elsif(!defined $domain and $newzone) {
nicedie "Sorry, but the zone population functionality isn't yet written.";
} elsif(!defined $domain) {
say "No rDNS record for IP $ip exists." and exit unless does_record_exist $ip;
say "rDNS for IP $ip: ".get_ptr $ip;exit;
}
if(defined $domain) {
nicedie "Forward DNS for $domain doesn't match $ip!" unless does_fqdn_match $ip or $force;
set_ptr $ip,$domain or nicedie "Failed to set rDNS for $ip to '$domain'!";
print "rDNS set";
print ((confirm_rdns $ip, $def_rdns) ? " and resolving" : " but not yet resolving (check manually with 'host $ip')") if $verify;
print ".\n";
}
do_sync $ip if (($made_modifications and !$nosync) or $fsync);