aboutsummaryrefslogtreecommitdiff
path: root/dgd/dgd
diff options
context:
space:
mode:
authorSergey Poznyakoff <gray@gnu.org.ua>2015-05-18 14:33:22 +0300
committerSergey Poznyakoff <gray@gnu.org.ua>2015-05-18 15:40:56 +0300
commiteb851ae32ee068dd368a8cb6db9b04e526316643 (patch)
tree656a221a10f8d833b2efca4376795a51dc825933 /dgd/dgd
parentd3b66f2187296695b948ef7764b76ff8f064ce4f (diff)
downloaddnstools-eb851ae32ee068dd368a8cb6db9b04e526316643.tar.gz
dnstools-eb851ae32ee068dd368a8cb6db9b04e526316643.tar.bz2
New sub-package: dgd
Diffstat (limited to 'dgd/dgd')
-rwxr-xr-xdgd/dgd843
1 files changed, 843 insertions, 0 deletions
diff --git a/dgd/dgd b/dgd/dgd
new file mode 100755
index 0000000..0afae81
--- /dev/null
+++ b/dgd/dgd
@@ -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
+
+

Return to:

Send suggestions and report system problems to the System administrator.