#!/opt/local/bin/perl
#
# Script to do bulk PTR lookups on a network of IP's
#
# Updated 4/10 with more features and to make better use of underlying
# CPAN modules:
#
# - Accepts IPv4/IPv6 addresses as singletons or a network in range or
# CIDR format
# - Allows you to configure which DNS server(s) to query
# - Allows you to configure a fixed delay between PTR lookups
# - Output to STDOUT for use in pipelines, or to a file in CSV or JSON
# format
# - Configurable timeout on PTR lookups
# - Persistent UDP connections to help lessen the load on DNS servers
#
# Requires CPAN modules Net::DNS, Net::IP, JSON and Tie::IxHash
#
# perl -MCPAN -e 'CPAN::Shell->install(qw(Net::DNS Net:IP Tie::IxHash JSON))'
#
# should do the trick on any Unix OS. On Debian/Ubuntu, do:
#
# apt-get install libnet-dns-perl libnet-ip-perl libjson-perl libtie-ixhash-perl
#
# Usage: The only required parameter is an IPv4/IPv6 network specified
# as a range or in CIDR format, or a single IP (see the Net::IP docs
# at http://search.cpan.org/~manu/Net-IP-1.25/IP.pm). Output is a
# simple CSV list of the IP addresses and the hostname they each
# resolved to, or NXDOMAIN if no PTR record exists, or error text if
# there is some other error with the DNS query.
#
# Output is always to STDOUT by default, or to a file if '-w' is
# specified. Errors always go to STDERR via croak. Examples:
#
# ./netdns.pl -i 10.0.0/24 > ptr-list.csv
# ./netdns.pl -i 10.0.0.1
# ./netdns.pl -i 10.0.0.5-10.0.0.33 -d 10 -j
# ./netdns.pl -i 10.0.0/25 -d 3 -t 2 -n 8.8.8.8,8.8.4.4 -j > ptr-list.json
# ./netdns.pl -i 10.0.0/25 -j -w ptr-list.json
# ./netdns.pl -i dead:beef::/32
#
# Sample JSON output:
#
# {
#    "10.0.0.0" : "foo1.example.com",
#    "10.0.0.1" : "foo2.example.com",
#    "10.0.0.2" : "foo3.example.com",
#    "10.0.0.3" : "foo4.example.com"
# }
#
# Copyright (c) 2006-2010 Doug Maxwell <doug@unixlore.net>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA
#

use strict;
use warnings;
use Net::DNS;
use Net::IP;
use Getopt::Std;
use JSON;
use Tie::IxHash;
use English;
use Carp;
require 5.006_000; # Needed for $outfile

our ($opt_h,$opt_j,$opt_i,$opt_d,$opt_n,$opt_t,$opt_w);
getopts('hji:d:n:t:w:');

usage() && exit if ( !$opt_i || $opt_h );

sub usage {
    print STDERR "\n$0 synopsis: \n";
    print STDERR "\n";
    print STDERR "Usage: $0 -i <IP or network spec> [-d N] [-t N] [-n <Comma-separated list of IPs or hostnames>] [-w filename] [-j] [-h]\n";
    print STDERR "-i: IP address, range or CIDR (required)\n";
    print STDERR "\t10.0.0.1\n\t10.0.0.3-10.0.0.55\n\t10.0.0/24\n\tdead:beef::/32\n";
    print STDERR "-d: Delay in seconds between lookups\n";
    print STDERR "-t: UDP timeout (defaults to five seconds)\n";
    print STDERR "-n: Comma-separated list of nameserver IPs or hostnames (defaults to system resolver)\n";
    print STDERR "-w: Output to the named file\n";
    print STDERR "-j: Output in JSON (default is CSV)\n";
    print STDERR "-h: This help text\n\n";
}

# Make sure delays are non-negative
my $delay = ( $opt_d && $opt_d > 0 ) ? $opt_d : 0;
my $udp_timeout = ( $opt_t && $opt_t > 0 ) ? $opt_t : 5;

# Initialize the hashref used for JSON output. Tie it so we can print
# it out in insertion order.
my $ptr_records = {};
tie %$ptr_records,"Tie::IxHash";

# $outfile is a filehandle pointing to the output file specified by
# the 'w' option, or to STDOUT.
my $outfile;
if ( $opt_w ) {

    open $outfile, '>', $opt_w or croak "Unable to open '$opt_w': $OS_ERROR\n";

} else {

    open $outfile, '>-', or croak "Unable to open STDOUT: $OS_ERROR\n";
}

my $ip = new Net::IP($opt_i) or croak "Unable to create Net::IP object\n";

my $res = Net::DNS::Resolver->new(
    persistent_udp => 1,
    udp_timeout => $udp_timeout,
) or croak "Unable to create Net::DNS::Resolver object\n";

# Set the nameservers to query as specified by '-n' args
$res->nameservers(split(",",$opt_n)) if ( $opt_n );

do {

  my $ip_address = $ip->ip();

  if ($ip_address) {

    my $query = $res->send("$ip_address",'PTR');
    if (defined $query && $query->answer) {

      foreach my $rr ($query->answer) {

        unless ( $opt_j ) {
            print {$outfile} "$ip_address,",$rr->ptrdname, "\n" or croak "Couldn't write: $OS_ERROR\n";
        }
        $ptr_records->{$ip_address} = $rr->ptrdname;

      }

    } else {

        unless ( $opt_j ) {
            print {$outfile} "$ip_address,",$res->errorstring,"\n"  or croak "Couldn't write: $OS_ERROR\n";
        }
        $ptr_records->{$ip_address} = $res->errorstring;

    }

}

  sleep($delay) if ( $delay );

} while ( ++$ip );

# Pretty-print the results JSON if needed
print {$outfile} JSON->new->pretty(1)->encode($ptr_records) if ( $opt_j );
close $outfile or croak "Unable to close file: $OS_ERROR\n";
