274 lines
10 KiB
Perl
Executable File
274 lines
10 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.
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
package DNS::Reverse::Manager;
|
|
use vars '$VERSION'; $VERSION = '1.1.0'; #Version number
|
|
|
|
use 5.008_008;
|
|
|
|
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 File::Copy qw/copy/;
|
|
use Net::DNS::ZoneFile;
|
|
use Net::DNS::ZoneParse qw/writezone/;
|
|
use POSIX qw/strftime/;
|
|
use Regexp::Common;
|
|
|
|
#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 $help = '';
|
|
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 @_;
|
|
print "\n";
|
|
exit 1;
|
|
}
|
|
sub print_help {
|
|
print qq{
|
|
rdns-manager v$VERSION by Matthew Connelly, 2014-15
|
|
Manager script for in-addr.arpa and ip6.arpa zones.
|
|
Source at https://github.com/MaffC/script-collection/blob/master/rdns-manager
|
|
|
|
Usage: rdns-manager [options] [IP address[, hostname]]
|
|
Basic usage:
|
|
- Get current rDNS for IP 1.2.3.4: rdns-manager 1.2.3.4
|
|
- Set rDNS for 1.2.3.4 to example.org: rdns-manager 1.2.3.4 example.org
|
|
|
|
Options:
|
|
-h, --help: This help text.
|
|
-v, --verify-rdns: Verify the set PTR record resolves once the zone has been synchronised.
|
|
-r, --reset: Reset [IP address] to the set default rDNS.
|
|
-p, --populate: Populate the given IPv4 reverse zone with default rDNS records. Does not support IPv6 zones.
|
|
-d, --no-sync: Do not synchronise the DNS zone after making changes. Use this for making bulk changes.
|
|
-s, --force-sync: Force-synchronise the DNS zone for [IP address]. Use after making bulk changes.
|
|
-R, --remove-ptr: Delete the PTR record for [IP address] from its zone.
|
|
|
|
Configuration:
|
|
--reset-hostname=[default rDNS]: Use in combination with -r, --reset.
|
|
--dns-server=[IP address]: Change what DNS server is used for forward and reverse DNS queries.};
|
|
exit;
|
|
}
|
|
sub validate_domain {
|
|
my $domain = shift;
|
|
return 1 if $domain =~ /^$RE{net}{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);
|
|
}
|
|
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;
|
|
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 {
|
|
#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", $zone);
|
|
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 {
|
|
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 {
|
|
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 {
|
|
my $rec = shift;
|
|
$made_modifications = 1;
|
|
write_zone $rec,grep {!($_->name eq $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 {
|
|
my $zone = shift;
|
|
my $syncscript = "/scripts/dnscluster synczone";
|
|
`$syncscript $zone`;
|
|
return $?;
|
|
}
|
|
sub do_sync {
|
|
my $ip = shift;
|
|
my ($rec,$zone) = get_arpa $ip;
|
|
my $res = '';
|
|
print "Syncing zone $zone... ";
|
|
nicedie "Couldn't sync $zone: Don't have a known sync method for network type $net_type." unless $net_type eq "cpanel";
|
|
$res = sync_cpanel $zone if $net_type eq "cpanel";
|
|
print (($res == 0) ? "Synchronised\n" : "Failed\n");
|
|
}
|
|
|
|
#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,
|
|
'prefixlen=i' => \$prefixlen,
|
|
'h|help' => \$help,
|
|
'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;
|
|
|
|
$help and print_help;
|
|
#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
|
|
|
|
#Argument validation
|
|
nicedie "Invalid arguments" if ($nosync and $fsync) or ($force and ($reset or $delptr)) or ($reset and $delptr) or (($verify or $force) and !defined $domain) or ($newzone and (($delptr or $reset or $force or defined $domain) or (defined $domain and ($delptr or $reset))));
|
|
|
|
#Main program flow
|
|
#Simple check that the zone exists. This was a for/when statement, but this script needs perl 5.8.8 compat, so for/given and when are out.
|
|
my ($trec,$tz) = get_arpa $ip;
|
|
my $zone_exists = does_zone_exist $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!" if $zone_exists == -2;
|
|
nicedie "Zonefile $tz (supposedly authoritative for $ip) doesn't appear to be a valid BIND zone. Please check the zonefile and try again." if $zone_exists == -1;
|
|
nicedie "Authoritative zone for IP $ip exists but we can't write to it. Please check the permissions on the zonefile for $tz." if !$zone_exists;
|
|
|
|
if(!defined $domain and $reset) {
|
|
set_ptr $ip,$def_rdns or nicedie "Failed to set rDNS for $ip to '$def_rdns'!";
|
|
print "rDNS set.\n";
|
|
} elsif(!defined $domain and $delptr) {
|
|
del_ptr $ip or nicedie "Failed to delete PTR record for $ip!";
|
|
print "PTR record for IP $ip deleted.\n";exit;
|
|
} elsif(!defined $domain and $newzone) {
|
|
nicedie "Sorry, but the zone population functionality isn't yet written.";
|
|
} elsif(!defined $domain) {
|
|
print "No rDNS record for IP $ip exists.\n" and exit unless does_record_exist $ip;
|
|
print "rDNS for IP $ip: ".get_ptr($ip)."\n";exit;
|
|
}
|
|
if(defined $domain) {
|
|
nicedie "Forward DNS for $domain doesn't match $ip!" unless $force or does_fqdn_match $domain, $ip;
|
|
set_ptr $ip,$domain or nicedie "Failed to set rDNS for $ip to '$domain'!";
|
|
print "rDNS set.\n";
|
|
}
|
|
do_sync $ip if (($made_modifications and !$nosync) or $fsync);
|
|
#very ugly, needs rewritten
|
|
if(defined $domain) {
|
|
print ((confirm_rdns $domain, $ip) ? "rDNS for IP $ip was successfully set to $domain" : "rDNS for IP $ip not yet resolving to $domain (check later with: host $ip)") if $verify;
|
|
} else {
|
|
print ((confirm_rdns $def_rdns, $ip) ? "rDNS for IP $ip was successfully set to $def_rdns" : "rDNS for IP $ip not yet resolving to $def_rdns (check later with: host $ip)") if $verify;
|
|
}
|
|
print "\n";
|