#!/usr/bin/perl

# downloaded from http://dns.measurement-factory.com/tools/nagios-plugins/check_zone_rrsig_expiration.html
# on 2010-02-07 by Peter Palfrader

# $Id: check_zone_rrsig_expiration,v 1.7 2008/11/25 01:36:36 wessels Exp $
#
# check_zone_rrsig_expiration
#
# nagios plugin to check expiration times of RRSIG records.  Reminds
# you if its time to re-sign your zone.

# Copyright (c) 2008, The Measurement Factory, Inc. All rights reserved.
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 
# Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# Neither the name of The Measurement Factory nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
# 
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

# Copyright (c) 2010 Peter Palfrader <peter@palfrader.org>
# - various fixes and cleanups
# - do more than one zone
# Copyright (c) 2012 Peter Palfrader <peter@palfrader.org>
#  - add -s option to configure udp packet size.  default changed from 4k to 1k
# Copyright (c) 2013 Peter Palfrader <peter@palfrader.org>
#  - add -r option to override initial refs.
# Copyright (c) 2014 Peter Palfrader <peter@palfrader.org>
#  - Do not ask for RRSIG directly, instead ask for SOA with dnssec data


# usage
#
# define command {
#   command_name    check-zone-rrsig
#   command_line    /usr/local/libexec/nagios-local/check_zone_rrsig -Z $HOSTADDRESS$
# }
# 
# define service {
#   name		dns-rrsig-service
#   check_command	check-zone-rrsig
#   ...
# }
# 
# define host {
#   use dns-zone
#   host_name zone.example.com
#   alias ZONE example.com
# }
# 
# define service {
#   use dns-rrsig-service
#   host_name zone.example.com
# }

use warnings;
use strict;

use Getopt::Std;
use Net::DNS::Resolver;
use Time::HiRes qw ( gettimeofday tv_interval);
use Time::Local;
use List::Util qw ( shuffle );

sub convert_time {
	my $in = shift;
	my ($ticks, $unit) = ($in =~ /^(\d+)([smhdw]?)$/);

	if ($unit eq 's' || $unit eq '') { }
	elsif ($unit eq 'm') { $ticks *= 60; }
	elsif ($unit eq 'h') { $ticks *= 60*60; }
	elsif ($unit eq 'd') { $ticks *= 60*60*24; }
	elsif ($unit eq 'w') { $ticks *= 60*60*24*7; }
	else { die "Invalid unit '$unit' in '$in'\n" }
	return $ticks;
}

my %opts = (t=>30, s=>1024);
getopts('hdt:c:w:s:r:', \%opts);
usage() unless scalar @ARGV == 1;
usage() if $opts{h};
my $zone = $ARGV[0];

my $data;
my $start;
my $stop;
my $CRIT = 3 * 3600*24;
my $WARN = 7 * 3600*24;

$CRIT = convert_time($opts{c}) if defined $opts{c};
$WARN = convert_time($opts{w}) if defined $opts{w};

my @refs = qw (
a.root-servers.net
b.root-servers.net
c.root-servers.net
d.root-servers.net
e.root-servers.net
f.root-servers.net
g.root-servers.net
h.root-servers.net
i.root-servers.net
j.root-servers.net
k.root-servers.net
l.root-servers.net
m.root-servers.net
);
@refs = split(/\s*,\s*/, $opts{r}) if (defined $opts{r});

$start = [gettimeofday()];
do_recursion();
do_queries();
$stop = [gettimeofday()];
do_analyze();

sub do_recursion {
	my $done = 0;
	my $res = Net::DNS::Resolver->new;
	do {
		print STDERR "\nRECURSE\n" if $opts{d};
		my $pkt;
		my $prettyrefs = (scalar @refs) ? join(", ", @refs) : "empty set(!?)";
		foreach my $ns (shuffle @refs) {
			print STDERR "sending query for $zone NS to $ns\n" if $opts{d};
			$res->nameserver($ns);
			$res->udp_timeout($opts{t});
			$res->udppacketsize($opts{s});
			$pkt = $res->send($zone, 'NS');
			last if $pkt;
		}
		critical("No response to seed query for $zone from $prettyrefs.") unless $pkt;
		critical($pkt->header->rcode . " from " . $pkt->answerfrom)
			unless ($pkt->header->rcode eq 'NOERROR');
		@refs = ();
		foreach my $rr ($pkt->authority, $pkt->answer) {
			print STDERR $rr->string, "\n" if $opts{d};
			push (@refs, $rr->nsdname) if $rr->type eq 'NS';
			next unless lc($rr->name) eq lc($zone);
			add_nslist_to_data($pkt);
			#print STDERR "Adding for $zone: ", $pkt->string, "\n" if $opts{d};
			$done = 1;
		}
		critical("No new references after querying for $zone NS from $prettyrefs.  Packet was ".$pkt->string) unless (scalar @refs);
	} while (! $done);
}


sub do_queries {
	my $n;
	do {
		$n = 0;
		foreach my $ns (keys %$data) {
			next if $data->{$ns}->{done};
			print STDERR "\nQUERY \@$ns SOA $zone\n" if $opts{d};

			my $pkt = send_query($zone, 'SOA', $ns);
			add_nslist_to_data($pkt);
			$data->{$ns}->{queries}->{SOA} = $pkt;

			print STDERR "done with $ns\n" if $opts{d};
			$data->{$ns}->{done} = 1;
			$n++;
		}
	} while ($n);
}

sub do_analyze {
	my $nscount = 0;
	my $NOW = time;
	my %MAX_EXP_BY_TYPE;
	foreach my $ns (keys %$data) {
		print STDERR "\nANALYZE $ns\n" if $opts{d};
		my $pkt = $data->{$ns}->{queries}->{SOA};
		critical("No response from $ns") unless $pkt;
		print STDERR $pkt->string if $opts{d};
		critical($pkt->header->rcode . " from $ns")
			unless ($pkt->header->rcode eq 'NOERROR');
		critical("$ns is lame") unless $pkt->header->ancount;
		foreach my $rr ($pkt->answer) {
			next unless $rr->type eq 'RRSIG';
			my $exp = sigrr_exp_epoch($rr);
			my $T = $rr->typecovered;
			if (!defined($MAX_EXP_BY_TYPE{$T}->{exp}) || $exp > $MAX_EXP_BY_TYPE{$T}->{exp}) {
				$MAX_EXP_BY_TYPE{$T}->{exp} = $exp;
				$MAX_EXP_BY_TYPE{$T}->{ns} = $ns;
			}
		}
		$nscount++;
	}
	warning("No nameservers found.  Is '$zone' a zone?") if ($nscount < 1);
	warning("No RRSIGs found") unless %MAX_EXP_BY_TYPE;
	my $min_exp = undef;
	my $min_ns = undef;
	my $min_type = undef;
	foreach my $T (keys %MAX_EXP_BY_TYPE) {
		printf STDERR ("%s RRSIG expires in %.1f days\n", $T, ($MAX_EXP_BY_TYPE{$T}->{exp}-$NOW)/86400) if $opts{d};
		if (!defined($min_exp) || $MAX_EXP_BY_TYPE{$T}->{exp} < $min_exp) {
			$min_exp = $MAX_EXP_BY_TYPE{$T}->{exp};
			$min_ns = $MAX_EXP_BY_TYPE{$T}->{ns};
			$min_type = $T;
		}
	}
	critical("$min_ns has expired RRSIGs") if ($min_exp < $NOW);
	if ($min_exp - $NOW < ($CRIT)) {
		my $ND = sprintf "%3.1f days", ($min_exp-$NOW)/86400;
		critical("$min_type RRSIG expires in $ND at $min_ns")
	}
	if ($min_exp - $NOW < ($WARN)) {
		my $ND = sprintf "%3.1f days", ($min_exp-$NOW)/86400;
		warning("$min_type RRSIG expires in $ND at $min_ns")
	}
	success(sprintf("No RRSIGs at zone apex expiring in the next %3.1f days", $WARN/86400));
}

sub sigrr_exp_epoch {
	my $rr = shift;
	die unless $rr->type eq 'RRSIG';
	my $exp = $rr->sigexpiration;
	die "bad exp time '$exp'"
		unless $exp =~ /^(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/;
	my $exp_epoch = timegm($6,$5,$4,$3,$2-1,$1);
	return $exp_epoch;
}

sub add_nslist_to_data {
	my $pkt = shift;
	foreach my $ns (get_nslist($pkt)) {
		next if defined $data->{$ns}->{done};
		print STDERR "adding NS $ns\n" if $opts{d};
		$data->{$ns}->{done} |= 0;
	}
}

sub success {
	output('OK', shift);
	exit(0);
}

sub warning {
	output('WARNING', shift);
	exit(1);
}

sub critical {
	output('CRITICAL', shift);
	exit(2);
}

sub output {
	my $state = shift;
	my $msg = shift;
	$stop = [gettimeofday()] unless $stop;
	my $latency = tv_interval($start, $stop);
	printf "ZONE %s: %s; (%.2fs) |time=%.6fs;;;0.000000\n",
		$state,
		$msg,
		$latency,
		$latency;
}

sub usage {
	print STDERR "usage: $0 [-d] [-w=<warn>] [-c=<crit>] [-t=<timeout>] [-r=<initialns1>[,<initialns2>[,..]]] [-s=<packet-size>] <zone>\n";
	exit 3;
}

sub send_query {
	my $qname = shift;
	my $qtype = shift;
	my $server = shift;
	my $res = Net::DNS::Resolver->new;
	$res->nameserver($server) if $server;
	$res->udp_timeout($opts{t});
	$res->dnssec(1);
	$res->retry(2);
	$res->udppacketsize($opts{s});
	my $pkt = $res->send($qname, $qtype);
	unless ($pkt) {
		$res->usevc(1);
		$res->tcp_timeout($opts{t});
		$pkt = $res->send($qname, $qtype);
	}
	return $pkt;
}

sub get_nslist {
	my $pkt = shift;
	return () unless $pkt;
	return () if (!$pkt->authority && !$pkt->answer);
	my @nslist;
	foreach my $rr ($pkt->authority, $pkt->answer) {
		next unless ($rr->type eq 'NS');
		next unless lc($rr->name) eq lc($zone);
		push(@nslist, lc($rr->nsdname));
	}
	return @nslist;
}
