#!/usr/bin/perl
# $Id: takeoverd,v 1.11 2000/06/19 11:17:38 tlinden Exp tlinden $
#
# This is takeoverd.
#
# Copyleft 2000 Thomas Linden, Consol* GmbH
#
# takeoverd is a small daemon which watches
# all interfaces configured in @interfaces.
# If a certain interface is considered to be
# down, then it hooks up all the interfaces
# using virtual interfaces and arp-spoofing.
#
# It determines if a host (or at least one of
# it's interfaces) is down using arping version
# 0.3 or 0.4. For 0.4 you need to change the arg
# -n to -c.
# An additional check is made against the availa
# bility of the peer, which must be a third host
# on the same segment as the ip we are watching.
# If it is down, takeoverd will *NOT* run fake!
# This is the case if a PEER is down or if our
# own interface is down.
#
# Signals:
# INT     kills the daemon
# TERM    kills the daemon
# HUP     slightly ignored (at the moment)
# USR1    print current fake and interface status
# USR2    turn debug on/off
#
use POSIX ":sys_wait_h";
use Getopt::Long;
use strict;
use Data::Dumper;

my ($Version, $Revision, $NULL);

$Revision = "";
$Version = "$Revision: 1.11 $NULL";
$Version =~ s/: //;
$Version =~ s/\s*$//;


#
# mying vars
#
my (
    $opt_v, $opt_s, $opt_f, $opt_r, $opt_h, $opt_c, $opt_q, $opt_d, $opt_l, $opt_t,
    $configfile, $PidFile, $CHILDPID, $GOT_HUP, $GOT_DOWN,
    $FAKESTATE, $PREVSTATE, $prevpid, $NumTargetArgs,
    $Delay, $NumArpings, $arping, $send_arp, $debug, $SENDSIG, $route, $ifconfig, $command, $notify,
    $NumInterfaces, $forkarg, $FakeOut, @interfaces, @up, @down, @ssh, @command, @notify,
    $waitedpid, $interface, $arping_args, $LOG
    );


$configfile    = "/etc/takeoverd.conf";  # default config file
$arping_args   = "-r -c";                # use "-r -c" for version >= 0.4!
$GOT_HUP       = 0;                      # default sig state
$GOT_DOWN      = 0;                      #      -""-
$debug         = "";                     # default debug state
$NumTargetArgs = 8;                      # how many args req. for one target
$|             = 1;                      # force autoflush

Getopt::Long::Configure( qw(no_ignore_case));
GetOptions (
	    "version|v!"     => \$opt_v,
	    "status|s!"      => \$opt_s,
	    "force|f!"       => \$opt_f,
	    "quit|q!"        => \$opt_q,
	    "debug|d!"       => \$opt_d,
	    "reload|r"       => \$opt_r,
	    "help|h!"        => \$opt_h,
	    "conf|c:s"       => \$opt_c,
	    "log|l:s"        => \$opt_l,
	    "test|t!"        => \$opt_t
	   );


# process commandline options:
$debug = 1 if($opt_d);

if ($opt_f) {
  # force takeover!
  $FAKESTATE = "up";
  $PREVSTATE = "down";
}
else {
  # default behavior, consider peer being up.
  $FAKESTATE = "down";
  $PREVSTATE = "down";
}

$SENDSIG = 'USR1' if($opt_s); # status
$SENDSIG = 'HUP'  if($opt_r); # reload
$SENDSIG = 'TERM' if($opt_q); # quit


&usage if($opt_h);

if($opt_v) {
  print "$0 version $Version\n";
  print "Copyright (c) 2000 ConSol* GmbH.\n";
  exit(0);
}


$configfile = $opt_c if ($opt_c);

# get config from file
&loadconfig;


if ($SENDSIG) {
  # does only take effect, if we actually are running
  &send_signal($SENDSIG);
}

if ($opt_l) {
  open LOGFILE, ">>$opt_l" || die "Could not open $opt_l!\n";
  *LOG = *LOGFILE;
}
else {
  *LOG = *STDOUT; #just a dummie
}

# std debug stream
*DEBUG = *STDOUT;

#
# sighandler for childs ###
#
$SIG{CHLD} = \&wait_child;


#################### FORK main ####################
my $OldPid = $$;
if (fork()) {
  exit(0);
}
###################################################

&debug("$OldPid forked, is now: $$\n") if(!-e $PidFile);

# overwrite proc listing
$0 = "takeoverd";



#
# check if we are not running
if (-e $PidFile) {
  open RUN, "<$PidFile" || die $!;
  while (<RUN>) {
    $prevpid = $_;
  }
  close RUN;
  if ($opt_d) {
    &send_signal("USR2");
  }
  print STDERR "takeoverd($prevpid) is already running!\n";
  exit(1);
}
else {
  #
  # store our PID for rc script
  #
  open RUN, ">$PidFile" || die "Error: Could not write PID to $PidFile! $!\n";
  print RUN $$;
  close RUN;
}

#
# sighandler for parent
#
$SIG{INT}  = \&signals;
$SIG{TERM} = \&signals;
$SIG{HUP}  = \&signals;
$SIG{USR1} = \&signals;
$SIG{USR2} = \&signals;

#
# how many interface we are watching
#
$NumInterfaces = $#interfaces + 1;

my $ips = "";
foreach $interface (@interfaces) {
  $ips .= $interface->{IP} . " ";
}

&debug ("watching $NumInterfaces interfaces\n");
&log ("started watching $ips");

#
# main loop
#
while (1) {
  # loop endless!
  # preserve sysload
  if($GOT_HUP) {
    &loadconfig(1);
    $GOT_HUP = 0;
  }
  if ($GOT_DOWN) {
    &change_state("down");
    &debug("unlink $PidFile\n");
    unlink $PidFile || die "Could not remove $PidFile!\n";
    &log("got SIGNAL to finish. Going down.");
    close LOG if($opt_l);
    exit(0);
  }
  $FAKESTATE = "up" if($opt_f);
  &change_state($FAKESTATE) if($FAKESTATE ne $PREVSTATE); # change only if somewhat has changed!
  $PREVSTATE = $FAKESTATE; # remember current state for next loop!

  $0 = "takeoverd wait";

  &debug("sleep $Delay\n");
  sleep $Delay;

  $0 = "takeoverd run";

  foreach $interface (@interfaces) {
    undef $interface->{MAC}; #reset MAC's
    undef $interface->{P_MAC};

    # get MAC's from PEER and partner interface
    &debug("TARGET: $arping $arping_args $NumArpings -i $interface->{INT} $interface->{IP}\n");
    $interface->{MAC} = `$arping $arping_args $NumArpings -i $interface->{INT} $interface->{IP} 2> /dev/null`;

    if ($interface->{PEER} ne "ignore") {
      &debug("PEER:   $arping $arping_args $NumArpings -i $interface->{INT} $interface->{PEER}\n");
      $interface->{P_MAC} = `$arping $arping_args $NumArpings -i $interface->{INT} $interface->{PEER} 2> /dev/null`;
    }
    my(@MACS, @P_MACS);

    @MACS = split /\n/, $interface->{MAC};
    @P_MACS = split /\n/, $interface->{P_MAC};


    foreach (@MACS) {
      chomp;
      if ($_ ne $interface->{MYMAC}) {
	$interface->{MAC} = $_;
	last;
      }
      else {
	$interface->{MAC} = "";
      }
    }

    ($interface->{P_MAC}) = split /\n/, $interface->{P_MAC};


    chomp $interface->{P_MAC};
    chomp $interface->{MAC};

    # assume down, if no MAC known
    $interface->{STATE} = ($interface->{MAC}) ? "up" : "down"; # remember state

    if ($interface->{PEER} eq "ignore") {
      $interface->{P_STATE} = "up";
    }
    else {
      $interface->{P_STATE} = ($interface->{P_MAC}) ? "up" : "down";
    }
    &debug( "TARGET: $interface->{IP} ($interface->{MAC}) $interface->{STATE}\n");
    &debug( "PEER:   $interface->{PEER} ($interface->{P_MAC}) $interface->{P_STATE}\n")
  }
  @up = ();
  @down = ();
  foreach $interface (@interfaces) {
    # check interface state
    push @up,   $interface->{INT} if($interface->{STATE} eq "up");
    push @down, $interface->{INT} if($interface->{STATE} eq "down" && $interface->{P_STATE} eq "up");
  }
  if ($FAKESTATE eq "down" ) {
    # we are currently down, fake is not running
    # check if we need to change state to up
    $FAKESTATE = "up" if($#down != -1); # minimum one peer's interface is down
  }
  else {
    # we are currently up, fake runs
    # we need to check if peer is up again and if we should take fake down
    $FAKESTATE = "down" if(($#up + 1) == $NumInterfaces); # all int's are up, so change to down
  }
  &debug("FAKESTATE ==> $FAKESTATE\n");
  &debug("PREVSTATE ==> $PREVSTATE\n");
  #&change_state($FAKESTATE) if($FAKESTATE ne $PREVSTATE); # change only if somewhat has changed!
}



sub change_state {
  #
  # change fake state
  #
  my($state) = @_;
  &debug("change state to $state\n");
  &log("changed state to $state");
  if ($state eq "up") {
    # simulate fake!
    if (@notify) {
      # notify using whatever command!
      system(@notify);
    }
    &shutdown_target;
    foreach $interface (@interfaces) {
      &debug("$ifconfig $interface->{INT}:0 $interface->{IP} netmask $interface->{MASK} up\n");
      if(!$opt_t) {
	system (
		$ifconfig,
		$interface->{INT} . ":0",
		$interface->{IP},
		"netmask",
		$interface->{MASK},
		"up"
	       ) && warn "Could not bring up interface $interface->{INT}:0!\n";
	# THIS IS NOT AN OBFUSCATED PERL-CODE!
	# the system call does just not return 0 if successful ;-)
      }
      &debug("$route add -host $interface->{IP} $interface->{INT}:0\n");
      if(!$opt_t) {
	system (
		$route,
		"add",
		"-host",
		$interface->{IP},
		$interface->{INT} . ":0"
	       ) && warn "Could not add host route for $interface->{IP} at $interface->{INT}:0!\n";
	# THIS IS NOT AN OBFUSCATED PERL-CODE!
	# the system call does just not return 0 if successful ;-)
      }
    }
    if (!($CHILDPID = fork)) {
      $SIG{INT}  = sub {print "kid killed!\n"; exit 0;};
      $SIG{TERM} = sub {print "kid killed!\n"; exit 0;};
      my $ips = "";
      foreach $interface (@interfaces) {
	$ips .= $interface->{IP} . " ";
      }
      $0 = "takeoverd up ( $ips)";
      &debug("enter keepalive child ($CHILDPID) (send_arp...\n");
      while (1) {
	sleep $Delay;
	foreach $interface (@interfaces) {
	  &debug("$send_arp $interface->{IP} $interface->{MYMAC} $interface->{IP} ffffffffffff $interface->{INT}\n");
	  if(!$opt_t) {
	    system(
		   $send_arp,
		   $interface->{IP},
		   $interface->{MYMAC},
		   $interface->{IP},
		   "ffffffffffff",
		   $interface->{INT}
		  );
	  }
	}
      }
      exit(0);
    }
  }
  else {
    # shutdown!
    kill 'INT', $CHILDPID if($CHILDPID != 0); # do not kill ourself!
    foreach $interface (@interfaces) {
      &debug("$ifconfig $interface->{INT}:0 down\n");
      if(!$opt_t) {
	system (
		$ifconfig,
		$interface->{INT} . ":0",
		"down"
	       ) && warn "Could not shut down $interface->{INT}:0!\n";
	# THIS IS NOT AN OBFUSCATED PERL-CODE!
	# the system call does just not return 0 if successful ;-)
      }
      # reset MAC to peer's one!
      if (!fork) {
	&debug("$send_arp $interface->{IP} $interface->{MAC} $interface->{IP} ffffffffffff $interface->{INT}\n");
	if(!$opt_t) {
	  system(
		 $send_arp,
		 $interface->{IP},
		 $interface->{MAC},
		 $interface->{IP},
		 "ffffffffffff",
		 $interface->{INT},
		);
	}
	exit(0);
      }
    }
  }
}


END { &debug("$0 finished\n"); }

sub debug {
  #
  # print debugging statements to STDOUT, if $debug is set(from commandline)
  #
  if ($debug ne "") {
    print DEBUG "[$$] ";
    print DEBUG @_;
  }
}


sub signals {
  #
  # called by signal handler, clean up everything
  #
  print DEBUG "received SIG$_[0]!\n";
  if ($_[0] eq "INT" || $_[0] eq "TERM") {
    #&change_state("down"); # shut down fake
    $GOT_DOWN = 1;
  }
  elsif ($_[0] eq "HUP") {
    $GOT_HUP = 1;
  }
  elsif ($_[0] eq "USR1" || $_[0] eq "USR2") {
    if (-e "/tmp/takeoverd.terminal") {
      open TERM, "/tmp/takeoverd.terminal" || die $!;
      my $dev = <TERM>;
      chomp $dev;
      close DEBUG;
      open DEBUG, ">$dev" || die $!;
      close TERM;
      #print "DEBUG: $dev\n";
      unlink "/tmp/takeoverd.terminal";
    }
    else {
      close DEBUG;
      *DEBUG = *STDOUT; #reset to default
    }
    if($_[0] eq "USR1") {
      &status;
    }
    elsif ( $_[0] eq "USR2") {
      &sigdebug;
    }
  }
}


sub wait_child {
  #
  # wait or child
  #
  my $waitedpid = wait;
  $SIG{CHLD} = \&wait_child;
  $waitedpid = 0;
}


sub status {
  #
  # got SIGUSR1, print out some stats
  #
  print DEBUG "\n================\ntakeoverd status: \n";
  print DEBUG "fake is currently $FAKESTATE.\n\nTarget interfaces:\n";
  foreach $interface (@interfaces) {
    print DEBUG "$interface->{IP} ($interface->{MAC}) $interface->{STATE}.\n";
  }
  print DEBUG "takeoverd pid: $$\n================\n";
}

sub sigdebug {
  #
  # turn debug on or off
  #
  $debug = ($debug eq "") ? "debug" : undef;
}


sub send_signal {
  #
  # send signal to actual running takeoverd
  # if not running and signal = debug, do nothing!
  #
  my($signal) = @_;
  my($PID, $e, $device, @rest, $user);
  if ($signal ne "HUP") {
    my $dev = `/usr/bin/who -m`;
    chomp $dev;
    ($user, $device, @rest) = split /\s\s*/, $dev;
    open TERM, ">/tmp/takeoverd.terminal" || die $!;
    print TERM "/dev/" . $device;
    close TERM;
  }
  if (-e $PidFile) {
    open RUN, "<$PidFile" || die $!;
    while (<RUN>) {
      $PID = $_;
    }
    close RUN;
    unless ($e = kill $signal, $PID) {
      warn "error sending signal to $0: $e!\n";
    }
    exit(0);
  }
  else {
      print "takeoverd is not running!\n";
      exit(1);
  }
}


sub loadconfig {
  #
  # load config from file
  #
  my($mode) = @_;
  my($ip, %InterFace, $SSH);
  if ( ! -e $configfile) {
    warn "Error: file \"$configfile\" does not exist!\n";
    # do only exit if called by sig handler
    exit(1) if(!$mode);
  }
  else {
    open CONFIG, "<$configfile"  || die $!;
    while (<CONFIG>) {
      chomp;
      next if(/^\s*$/ || /^\s*#/); # ignore whitespace(s) and lines beginning with #
      $_ =~ s/^\s*//;
      my ($option,$value) = split /\s\s*=?\s*/, $_, 2;
      $value              =~ s/\s*$//;
      $Delay              = $value if /^Delay/;
      $NumArpings         = $value if /^NumArpings/;
      $arping             = $value if /^arping/;
      $ifconfig           = $value if /^ifconfig/;
      $route              = $value if /^route/;
      $command            = $value if /^command/;
      $notify             = $value if /^notify/;
      $send_arp           = $value if /^send_arp/;
      $PidFile            = $value if /^PidFile/;
      $SSH                = $value if /^ssh/;
      if (/^<([\d+\.+]+?)>$/) {
	# start IP statement
	$ip = $1;
	$InterFace{IP} = $ip;
      }
      if (/^<\/\Q$ip\E>$/) {
	&debug("loaded config for target: $ip\n");
	&debug("targets $ip peer will be ignored!\n") if($InterFace{PEER} eq "ignore");
	# end ip statement
	my @values = values %InterFace;
	if ($#values != $NumTargetArgs) {
	  warn "Target config $ip incomplete! Here is the dump:\n";
	  print Dumper(\%InterFace);
	  print "Required fields: PeerIp, InterFace, Mask, Mac, State, PeerMac, PeerState, MyMac!\n";
	  exit(1) if(!$mode);
	}
	else {
	  push @interfaces, { %InterFace };
	}
	undef %InterFace;
      }
      $InterFace{PEER}    = $value if /^\s*PeerIp/;
      $InterFace{INT}     = $value if /^\s*InterFace/;
      $InterFace{MASK}    = $value if /^\s*Mask/;
      $InterFace{MAC}     = $value if /^\s*Mac/;
      $InterFace{STATE}   = $value if /^\s*State/;
      $InterFace{P_MAC}   = $value if /^\s*PeerMac/;
      $InterFace{P_STATE} = $value if /^\s*PeerState/;
      $InterFace{MYMAC}   = $value if /^\s*MyMac/;
    }
    close CONFIG;
    @ssh     = split /\s*,\s*/, $SSH;
    @command = split /\s+\s*/, $command;
    @notify  = split /\s+\s*/, $notify; # optional!
  }
  if (!@ssh || !@interfaces || !$Delay || !$NumArpings || !$command || !$arping || !$send_arp || !$PidFile || !$ifconfig || !$route) {
    print "Error: config $configfile not complete!\n";
    exit(1) if(!$mode);
  }
}




sub usage {
print qq(
usage: $0 [option]
Options:
-s | --status       print status of current running takeoverd (= SIGUSR1).
-r | --reload       reload config (= SIGHUP).
-d | --debug        turn debug on|off. (= SIGUSR2).
-f | --force        force to take over interfaces.
-c | --conf <file>  use alternate config file (default: /etc/takeoverd.conf).
-l | --log <file>   use logging.
-t | --test         test mode, do only report, do not change state!
-q | --quit         quit running takeoverd (= SIGINT or SIGTERM).
-h | --help         print usage message.
-v | --version      print version number.

);
exit(1);
}





sub log {
  my($msg) = @_;
  if(!$opt_l) {
    return;
  }
  my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
  $year += 1900;
  $mon += 1;
  $mon =~ s/^(\d)$/0$1/;
  $hour =~ s/^(\d)$/0$1/;
  $min =~ s/^(\d)$/0$1/;
  $sec =~ s/^(\d)$/0$1/;
  $mday =~ s/^(\d)$/0$1/;
  print LOG "$mday.$mon.$year $hour:$min:$sec takeoverd[$$]: $msg\n";
}


sub shutdown_target {
  #
  # executes a remote script for taking
  # our target down!
  # !!! ATTENTION !!!
  # it uses open() to get the PID of the ssh call issued
  # which will be used to kill the ssh-client if the session
  # times out. Timeout is the same value as in $Delay (see config).
  # return of the remote command is stored in $errcode.
  # in our case, the remote command prints the errorcode of the
  # ifconfig command(s), which is 0 in case of success.
  #
  my($try,@errors,$errcode, @exec, $pid);
  $pid = undef;
  foreach $try(@ssh) {
    &debug("try to execute $try $command...\n");
    @exec = split /\s\s*/, $try;
    push @exec, @command;
    eval {
      local $SIG{ALRM} = sub { kill INT => $pid if($pid); die "alarm\n"; };
      local $SIG{PIPE} = sub { die "alarm\n" };
      alarm $Delay;
      $pid = open PIPE, "@exec|";
      $errcode = <PIPE>; # should be a 0 in case of success!
      chomp $errcode;
      close PIPE;
      alarm 0;
    } if(!$opt_t); # only if not in test mode!
    if($@) {
      if($@ eq "alarm\n") {
	print "timeout...\n";
      }
    }
    &debug("=====> ERRORCODE: ($errcode) <======\n");
    @exec = ();
    last if($errcode == 0 && $errcode ne "");
    push @errors, $errcode;
  }
  if ($#ssh == $#errors) {
    # all tries were insuccessful!
    &log("Could not execute remote command \"$command\"! all tries were insuccessful!");
    &debug("Could not execute remote command \"$command\"! all tries were insuccessful\n");
  }
  else {
    &log("Executed \"$try $command\" successful.");
    &debug("Executed \"$try $command\" successful.\n");
  }
}





