diff options
author | Sergey Poznyakoff <gray@gnu.org.ua> | 2015-05-18 14:33:22 +0300 |
---|---|---|
committer | Sergey Poznyakoff <gray@gnu.org.ua> | 2015-05-18 15:40:56 +0300 |
commit | eb851ae32ee068dd368a8cb6db9b04e526316643 (patch) | |
tree | 656a221a10f8d833b2efca4376795a51dc825933 /dgd/dgd | |
parent | d3b66f2187296695b948ef7764b76ff8f064ce4f (diff) | |
download | dnstools-eb851ae32ee068dd368a8cb6db9b04e526316643.tar.gz dnstools-eb851ae32ee068dd368a8cb6db9b04e526316643.tar.bz2 |
New sub-package: dgd
Diffstat (limited to 'dgd/dgd')
-rwxr-xr-x | dgd/dgd | 843 |
1 files changed, 843 insertions, 0 deletions
@@ -0,0 +1,843 @@ +#!/usr/bin/perl +# Copyright (C) 2015 Sergey Poznyakoff <gray@gnu.org> +# +# 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 3, 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, see <http://www.gnu.org/licenses/>. + +use strict; +use Getopt::Long qw(:config gnu_getopt no_ignore_case); +use File::Basename; +use Net::Ping; +use Pod::Usage; +use Pod::Man; +use Proc::Daemon; +use Sys::Syslog; +use Data::Dumper; + +my $progname = basename($0); +my $progdescr = "Dead gateway detector"; +my $confdir = "/etc/dgd"; +my $conffile = "$confdir/dgd.conf"; + +my $debug; +my $use_syslog; + +my @links; +my $active_link; + +my %config = ( + core => { + interval => 60, + timeout => 3, + probes => 3, + tolerance => 2 + } +); + +my $rxip = '\d{1,3}\.\d{1,3}.\d{1,3}.\d{1,3}'; + +sub check_prog { + my @cmd = split(/\s+/, shift); + my $loc = shift; + unless (-f $cmd[0]) { + error("$loc: file $cmd[0] does not exist"); + return 0; + } + unless (-x $cmd[0]) { + error("$loc: file $cmd[0] is not executable"); + return 0; + } + return 1; +} + +my %kw = ( + core => { + section => { + interval => { re => '^\d+$' }, + timeout => { re => '^\d+$' }, + probes => { re => '^\d+$' }, + tolerance => { re => '^\d+$' }, + active => 1, + up => { check => \&check_prog }, + down => { check => \&check_prog }, + logpriority => { re => '^0|1$' } + } + }, + syslog => { + section => { + facility => { re => '^((auth(priv)?)|cron|daemon|ftp|(local[0-7])|user)$' }, + tag => 1 + } + }, + link => { + section => { + name => { mandatory => 1 }, + if => 1, + ip => { re => "^$rxip\$" }, + gw => { re => "^$rxip\$", mandatory => 1 }, + ns => 1, + net => 1, + up => { check => \&check_prog }, + down => { check => \&check_prog }, + priority => { re => '^\d+$' } + }, + mandatory => 1 + } +); + +use constant EX_OK => 0; +use constant EX_USAGE => 64; # command line usage error +use constant EX_DATAERR => 65; # data format error +use constant EX_NOINPUT => 66; # cannot open input file +use constant EX_UNAVAILABLE => 69; # service unavailable +use constant EX_SOFTWARE => 70; # internal software error (not used yet) +use constant EX_OSFILE => 72; # critical OS file missing +use constant EX_CANTCREAT => 73; # can't create (user) output file +use constant EX_CONFIG => 78; # configuration error + +sub diag { + my $prio = shift; + my $msg = shift; + local %_ = @_; + + $msg = "$prio: $msg" if $config{core}{logpriority}; + $msg = "$_{prefix}: $msg" if defined($_{prefix}); + + if ($use_syslog) { + syslog($prio, $msg); + } else { + print STDERR "$progname: " if defined($progname); + print STDERR "$msg\n" + } +} + +sub error { + diag('err', @_); +} + +sub debug { + my $l = shift; + diag('debug', join(' ',@_)) if $debug >= $l; +} + +sub abend { + my $code = shift; + diag('crit', @_); + exit $code; +} + +sub parse_section { + my ($conf, $input) = @_; + my $ref = $conf; + my $quote; + my $rootname; + while ($input ne '') { + my $name; + if (!defined($quote)) { + if ($input =~ /^"(.*)/) { + $quote = ''; + $input = $1; + } elsif ($input =~ /^(.+?)(?:\s+|")(.*)/) { + $name = $1; + $input = $2; + } else { + $name = $input; + $input = ''; + } + } else { + if ($input =~ /^([^\\"]*)\\(.)(.*)/) { + $quote .= $1 . $2; + $input = $3; + } elsif ($input =~ /^([^\\"]*)"\s*(.*)/) { + $name = $quote . $1; + $input = $2; + $quote = undef; + } else { + die "unparsable input $input"; + } + } + + if (defined($name)) { + $rootname = $name unless defined $rootname; + $ref->{$name} = {} unless ref($ref->{$name}) eq 'HASH'; + $ref = $ref->{$name}; + $name = undef; + } + } + return ($ref, $rootname); +} + +sub check_mandatory { + my ($section, $kw, $loc, $s) = @_; + my $err = 0; + while (my ($k, $d) = each %{$kw}) { + if (ref($d) eq 'HASH' + and $d->{mandatory} + and !exists($section->{$k})) { + if (exists($d->{section})) { + if ($s) { + error("$loc: mandatory section [$k] not present"); + ++$err; + } + } else { + error("$loc: mandatory variable \"$k\" not set"); + ++$err; + } + } + } + return $err; +} + +sub readconfig { + my $file = shift; + my $conf = shift; + my %param = @_; + + open(my $fd, "<", $file) + or do { + return 1 if $param{include}; + abend(EX_NOINPUT, "can't open configuration file $file: $!"); + }; + + my $line; + my $err; + my $section = $conf; + my $kw = $param{kw}; + my $include = 0; + my $rootname; + + while (<$fd>) { + ++$line; + chomp; + if (/\\$/) { + chop; + $_ .= <$fd>; + redo; + } + + s/^\s+//; + s/\s+$//; + s/#.*//; + next if ($_ eq ""); + + if (/^\[(.+?)\]$/) { + $err += check_mandatory($section, $kw, "$file:$line") + if defined $kw; + + $include = 0; + my $arg = $1; + $arg =~ s/^\s+//; + $arg =~ s/\s+$//; + if ($arg eq 'include') { + $include = 1; + } else { + ($section, $rootname) = parse_section($conf, $1); + if (ref($param{kw}) eq 'HASH') { + if (defined($rootname)) { + if (ref($param{kw}{$rootname}) eq 'HASH' + and exists($param{kw}{$rootname}{section})) { + $kw = $param{kw}{$rootname}{section}; + } else { + error("$file:$line: unknown section"); + $kw = undef; + } + } else { + $kw = $param{kw}; + } + } + } + } elsif (/([\w_-]+)\s*=\s*(.*)/) { + my ($k, $v) = ($1, $2); + $k = lc($k) if $param{ci}; + + if ($include) { + if ($k eq 'path') { + $err += readconfig($v, $conf, include => 1, @_); + } elsif ($k eq 'glob') { + foreach my $file (glob $v) { + $err += readconfig($file, $conf, @_); + } + } else { + error("$file:$line: unknown keyword"); + ++$err; + } + next; + } + + if (defined($kw)) { + my $x = $kw->{$k}; + if (!defined($x)) { + error("$file:$line: unknown keyword $k"); + ++$err; + next; + } elsif (ref($x) eq 'HASH') { + if (exists($x->{re})) { + if ($v !~ /$x->{re}/) { + error("$file:$line: invalid value for $k"); + ++$err; + next; + } + if (exists($x->{check}) + and !&{$x->{check}}($v, "$file:$line")) { + ++$err; + next; + } + } elsif (exists($x->{check})) { + if (!&{$x->{check}}($v, "$file:$line")) { + ++$err; + next; + } + } elsif (!exists($x->{var}) and !exists($x->{mandatory})) { + error("$file:$line: unknown keyword $k"); + ++$err; + next; + } + } + } + + $section->{$k} = $v; + } else { + error("$file:$line: malformed line"); + ++$err; + next; + } + } + close $fd; + if (defined($param{kw}) and !$param{include}) { + $err += check_mandatory($conf, $param{kw}, "$file:$line", 1) + } + return $err; +} + +sub get_default_gw { + my $gw; + open(my $fd, '-|', "netstat -rn") + or abend(EX_OSFILE, "can't start netstat: $!"); + while (<$fd>) { + if (/^0\.0\.0\.0\s+(\S+)/) { + $gw = $1; + last; + } + } + close $fd; + return $gw; +} + +sub scan_links { + @links = sort { $a->{priority} <=> $b->{priority} } + map { $_->{priority} = 100 unless exists $_->{priority}; + debug(1, "registered link $_->{name} via $_->{if}"); + $_ + } values %{$config{link}}; + + abend(EX_CONFIG, "no links configured") + if $#links == -1; + + if (defined($config{core}{active})) { + for ($active_link = 0; $active_link <= $#links; ++$active_link) { + last if $links[$active_link]->{name} eq $config{core}{active}; + } + + abend(EX_CONFIG, + "no link corresponding to the active one") + if ($active_link > $#links); + } else { + my $gw = get_default_gw(); + debug(1, "default gw $gw"); + if (defined($gw)) { + for ($active_link = 0; $active_link <= $#links; ++$active_link) { + last if $links[$active_link]->{gw} eq $gw; + } + + abend(EX_CONFIG, "no active link configured") + if ($active_link > $#links); + + debug(1, "active link $links[$active_link]->{name}"); + } else { + abend(EX_CONFIG, "no active link configured") + } + } + +} + +sub runcmd { + my $cmdline = shift; + debug(2, "running $cmdline"); + if (open(my $fd, '-|', "$cmdline 2>&1")) { + while (<$fd>) { + diag('notice', "\"$cmdline\": $_"); + } + close($fd); + } elsif ($? == -1) { + error("failed to execute \"$cmdline\": $!"); + } elsif ($? & 127) { + error("\"$cmdline\" died with signal " . ($? & 127)); + } elsif ($? >> 8) { + error("\"$cmdline\" exited with code " . ($? >> 8)); + } else { + error("failed to execute \"$cmdline\": $!"); + } +} + +sub updown { + my ($what, $name) = @_; + + my $cmd = defined($config{link}{$name}{$what}) + ? $config{link}{$name}{$what} + : $config{core}{$what}; + if (defined($cmd)) { + runcmd("$cmd $name"); + } elsif ($what eq 'up') { + runcmd("route add default gw $config{link}{gw}"); + } elsif ($what eq 'down') { + runcmd("route del default gw"); + } else { + die "unrecognized action $what"; + } +} + +sub link_is_alive { + my $arg = shift; + $arg = $links[$arg] if ref($arg) ne 'HASH'; + return $arg->{alive} >= $config{core}{tolerance}; +} + +sub check_links { + my $link; + foreach $link (@links) { + $link->{alive} = 0; + } + debug(1, "checking links"); + my $p = Net::Ping->new("icmp", $config{core}{timeout}); + for (my $i = 0; $i < $config{core}{probes}; $i++) { + foreach $link (@links) { + debug(3, "ping $link->{name}"); + next if link_is_alive($link); + ++$link->{alive} if $p->ping($link->{gw}); + } + } + $p->close(); + if (!defined($active_link)) { + return 1; + } elsif (!link_is_alive($active_link)) { + my $name = $links[$active_link]->{name}; + error("link $name went down"); + updown('down', $name); + $active_link = undef; + return 1; + } + return 0; +} + +sub newlink { + debug(1, "looking for available link"); + for (my $i = 0; $i <= $#links; $i++) { + #next if $i == $active_link; + debug(2, "$links[$i]->{name}: $links[$i]->{alive}/$config{core}{tolerance}"); + if (link_is_alive($i)) { + my $name = $links[$i]->{name}; + diag('info', "switching to link $name"); + updown('up', $name); + $active_link = $i; + return; + } + } + error("no fallback link"); +} + +# ######## +my $foreground; + +GetOptions("h" => sub { + pod2usage(-message => "$progname: $progdescr", + -exitstatus => 0); + }, + "help" => sub { + pod2usage(-exitstatus => EX_OK, -verbose => 2); + }, + "usage" => sub { + pod2usage(-exitstatus => EX_OK, -verbose => 0); + }, + "debug|d+" => \$debug, + "foreground|f" => \$foreground, + "config|c=s" => \$conffile) or exit(1); + +if (readconfig($conffile, \%config, kw => \%kw)) { + exit(EX_CONFIG); +} + +abend(EX_USAGE, "too many arguments") if $#ARGV >= 0; + +print Data::Dumper->Dump([\%config], [qw(config)]) if $debug >= 4; + +scan_links; + +unless ($foreground) { + $config{core}{syslog}{facility} = 'daemon' + unless exists $config{core}{syslog}{facility}; +} + +if (defined($config{core}{syslog}{facility})) { + my $tag = $config{core}{syslog}{tag} || $progname; + openlog($tag, "pid", $config{core}{syslog}{facility}); +} + +my $pidfile = $config{core}{pidfile}; +$SIG{TERM} = $SIG{INT} = $SIG{QUIT} = sub { + unlink($pidfile) if defined($pidfile); + exit(EX_OK); +}; + +unless ($foreground) { + my $daemon = Proc::Daemon->new(work_dir => '/'); + my $pid = $daemon->Init; + if ($pid) { + if (defined($pidfile)) { + if (open(my $fd, '>', $pidfile)) { + print $fd "$pid\n"; + close $fd; + } else { + error("can't open $pidfile: $!"); + } + } + exit(EX_OK); + } +} + +$SIG{HUP} = sub { + my %t; + diag('info', "re-reading configuration file"); + if (readconfig($conffile, \%t)) { + %config = %t; + @links = (); + scan_links; + } +}; + +$use_syslog = defined $config{syslog}{facility}; + +while (1) { + if (check_links()) { + newlink(); + } elsif ($active_link > 0) { + for (my $i = 0; $i < $active_link; $i++) { + if (link_is_alive($i) + and $links[$i]->{priority} < $links[$active_link]->{priority}) { + diag('info', "switching to higher priority link $links[$i]->{name}"); + updown('down', $links[$active_link]->{name}); + + $active_link = $i; + updown('up', $links[$active_link]->{name}); + } + } + } + sleep($config{core}{interval}); +} + +=head1 NAME + +dgd - dead gateway detector + +=head1 SYNOPSIS + +B<dgd> +[B<-df>] +[B<-c> I<FILE>] +[B<--config=>I<FILE>] +[B<--debug>] +[B<--foreground>] + +B<dgd> B<-h> | B<--help> | B<--usage> + +=head1 DESCRIPTION + +Monitors a set of network links, by sending ICMP echo requests to +remote gateways. One of the links is assumed to be I<active>, i.e., +to be used as a default gateway, others are used as fallback. + +When active link goes down, B<dgd> executes a preconfigured B<down> +action for that link, then it selects next available link and executes +an B<up> action for it. Default B<up> action sets up default gateway +via the selected link. Default B<down> action removes default +gateway from the routing table. + +Each link can be assigned a I<priority>, an integer value between 0 +and 100 (default). When selecting fallback link to replace the dead one, +the link with lowest priority is given preference. + +When a link goes up, its priority is compared to that of the active one. +If it is numerically less, the new link is made active, using the same +procedure as described above. + +=head1 CONFIGURATION FILE + +The configuration is read from file F</etc/dgd/dgd.conf>. The syntax is +somewhat traditional. Whitespace is mostly ignored. The B<#> character +begins a comment to the end of line. Blank lines are ignored. + +The file consists of sections and variabes. A section begins with +the name of the section in square brackets and continues until the +next section begins. Each variable must belong to some section. +Names of sections and variables are case-sensitive. + +Sections can be further divided into subsections, by listing subsection +name after the section name and a white space, e.g.: + + [section subsection] + +Subsection name must be enclosed in double quotes if it contains +whitespace or double-quote characters. Otherwise, quoting is optional. +Within a quoted subsection name, double-quote and backslash have to be +escaped as B<\"> and B<\\>, respectively. + +Variables are assigned using the following syntax + +=over 8 + +=item I<NAME> B<=> I<VALUE> + +=back + +Whitespace is allowed on both sides of the equals sign. I<VALUE> is read +verbatim, it can contain any characters, including whitespace. Very long +values can be split over several physical lines, by ending each line +excepting the last with a backslash immediately followed by a newline character. + +=head2 Section B<[include]> + +Special section B<[include]> can be used to include one or more files into +another file. It can contain following variables: + +=over 4 + +=item B<path => F<FILE> + +Include contents of F<FILE>. The file must exist. + +=item B<glob => I<PATTERN> + +Include all files matching B<glob>(7) I<PATTERN>. It is OK if no +file matches the pattern. + +=back + +=head2 Section B<[core]> + +Controls core functionality of the program. + +=over 4 + +=item B<interval => I<NUM> + +Check links each I<NUM> seconds (default 60). + +=item B<timeout => I<NUM> + +Ping timeout, in seconds (default 3). + +=item B<probes => I<NUM> + +Number of ping probes to run for each gateway (default 3). + +=item B<tolerance => I<NUM> + +Number of probes that must succeed in order for the link to be +marked as I<alive>. Default is 2 + +=item B<active => I<STRING> + +Name of the active link. If not defined, the link whose B<gw> variable +matches the default gateway address is assumed active. + +=item B<logpriority => B<0>|B<1> + +If B<1>, log the priority with each diagnostic message. + +=item B<up => I<COMMAND> + +Name of the external command to run when a link goes up. Name of +the link in question is passed as argument to I<COMMAND>. + +=item B<down => I<COMMAND> + +Name of the external command to run when a link goes up. Name of +the link in question is passed as argument to I<COMMAND>. + +=back + +=head2 Section B<[syslog]> + +=over 4 + +=item B<facility => I<STRING> + +Syslog facility to use. Allowed values for I<STRING> are: B<auth>, +B<authpriv>, B<cron>, B<daemon>, B<ftp>, B<user>, and B<local0> +through B<local7>. Default is B<daemon>. + +=item B<tag => I<STRING> + +Tag messages with I<STRING> (default -- name of the program). + +=back + +=head2 Section B<[link]> + +Defines a link. At least one link must be defined. + +=over 4 + +=item B<name => I<STRING> + +Name of this link. This variable is mandatory, + +=item B<gw => I<IPADDR> + +IP address of the remote gateway. This variable is mandatory, + +=item B<if => I<STRING> + +Name of the network interface. + +=item B<up => I<COMMAND> + +Name of the external command to run when this link goes up. Overrides +B<core.up>. + +=item B<down => I<COMMAND> + +Name of the external command to run when this link goes up. Overrides +B<core.down>. + +=item B<priority => I<NUMBER> + +Priority of the link, an integer number between 0 (highest priority) and +100 (lowest priority). Default is 100. + +=back + +The variables below are not used directly by B<dgd>. They are intended +to pass additional information to B<up> and B<down> scripts: + +=over 4 + +=item B<ip => I<IPADDR> + +IP address assigned to the interface. + +=item B<ns => I<IPADDR> [I<IPADDR>...] + +Whitespace-separated list of ip addresses of NS servers. + +=item B<net => I<STRING> + +Network that is routed through this link. + +=back + +=head1 OPTIONS + +=over 4 + +=item B<-c>, B<--config=>I<FILE> + +Read configuration parameters from I<FILE>, instead of F</etc/dgd.conf>. + +=item B<-d>, B<--debug> + +Increase debug level. + +=item B<-f>, B<--foreground> + +Remain in the foreground. Print diagnostics on standard error, unless +B<[syslog]> configuration section is present. + +=back + +The following options cause B<dgd> to print informational message on +the standard error and exit: + +=over 4 + +=item B<-h> + +Print short usage summary. + +=item B<--help> + +Display this manual. + +=item B<--usage> + +Print invocation syntax summary. + +=back + +=head1 FILES + +=over 4 + +=item F</etc/dgd/dgd.conf> + +Default configuration file. + +=back + +=head1 SIGNALS + +=over 4 + +=item B<SIGHUP> + +Instructs B<dgd> to re-read its configuration file. The new configuration +takes effect only if there were no errors in it. + +=item B<SIGTERM>, B<SIGINT>, B<SIGQUIT> + +Program terminates. + +=back + +=head1 EXIT CODES + +=over 4 + +=item B<0> + +Successful termination. + +=item B<64> + +Command line usage error. + +=item B<66> + +Couldn't open input file. + +=item B<72> + +Failed to run external command. + +=item B<78> + +Error in configuration file. + +=back + +=head1 AUTHOR + +Sergey Poznyakoff <gray@gnu.org> + +=cut + + |