#!/usr/bin/perl 

=head1 NAME

pfmon - ARP listener and maintenance threads

=head1 SYNOPSIS

pfmon [options]

 Options:
   -d      Daemonize
   -h      Help
   -v      Verbose

=cut

use warnings;
use strict;
use Getopt::Std;
use Net::Pcap 0.16;
use File::Basename qw(basename);
use threads;
use threads::shared;
use POSIX qw(:signal_h);
use Pod::Usage;

use constant INSTALL_DIR => '/usr/local/pf';

use lib INSTALL_DIR . "/lib";


#$thread=1;

use pf::config;
use pf::iplog;
use pf::locationlog;
use pf::node;
use pf::services;
use pf::traplog;
use pf::util;

Log::Log4perl->init_and_watch( INSTALL_DIR . "/conf/log.conf", $LOG4PERL_RELOAD_TIMER );
my $logger = Log::Log4perl->get_logger( basename($0) );
Log::Log4perl::MDC->put( 'proc', basename($0) );
Log::Log4perl::MDC->put( 'tid',  threads->self->tid() );

POSIX::sigaction(
    &POSIX::SIGHUP,
    POSIX::SigAction->new(
        'normal_sighandler', POSIX::SigSet->new(), &POSIX::SA_NODEFER
    )
) or $logger->logdie("pfmon: could not set SIGHUP handler: $!");

POSIX::sigaction(
    &POSIX::SIGTERM,
    POSIX::SigAction->new(
        'normal_sighandler', POSIX::SigSet->new(), &POSIX::SA_NODEFER
    )
) or $logger->logdie("pfmon: could not set SIGTERM handler: $!");

POSIX::sigaction(
    &POSIX::SIGINT,
    POSIX::SigAction->new(
        'normal_sighandler', POSIX::SigSet->new(), &POSIX::SA_NODEFER
    )
) or $logger->logdie("pfmon: could not set SIGINT handler: $!");


my %args;
getopts( 'dhvr', \%args );

pod2usage( -verbose => 1 ) if ( $args{h} );

pfmon_preload();

my $daemonize = $args{d};
my $verbose   = $args{v};
my $restart   = $args{r};

my @kids;

# standard signals and daemonize
daemonize() if ($daemonize);

# thread off the cleanup function
push @kids, threads->create( \&cleanup );

$kids[$#kids]->join();
$logger->error("cleanup thread finished - this is bad");

END {
    if ( !$args{h} ) {
        deletepid();
        $logger->info("stopping pfmon");

        # wake up the arpgun... then kill him
        foreach my $kid (@kids) {
            $kid->detach;
        }

        # killing kids ain't fun, but somebody's got to do it...
        kill 6, -$$;
    }
}

exit(0);

=head1 SUBROUTINES

=over

=cut
sub arp_detector {
    my ($eth) = @_;
    my $logger = Log::Log4perl::get_logger('pfmon::arp_detector');
    Log::Log4perl::MDC->put( 'tid', threads->self->tid() );

    my ( $filter_t, $net, $mask, $opt, $err );
    $opt = 1;

    my $pcap_t = Net::Pcap::pcap_open_live( $eth, 1500, 1, 0, \$err );
    my $filter = arp_filter();
    if ( ( Net::Pcap::lookupnet( $eth, \$net, \$mask, \$err ) ) == -1 ) {
        $logger->logdie("Net::Pcap::lookupnet failed. Error was $err");
    }
    if ( ( Net::Pcap::compile( $pcap_t, \$filter_t, $filter, $opt, $net ) )
        == -1 )
    {
        $logger->logdie("Unable to compile filter string '$filter'");
    }
    Net::Pcap::setfilter( $pcap_t, $filter_t );
    Net::Pcap::loop( $pcap_t, -1, \&process_packet, $eth );
}

sub process_packet {
    my ( $user_data, $header, $packet ) = @_;
    listen_arp($packet) if ($packet);
}

=item listen_arp

Listens to ARP traffic and logs.

In PacketFence 3.0 we dropped support for ARP mode.
This method has been severely crippled as part of that work.
However it has been kept around since we might introduce ARP surveillance in the future.

=cut
sub listen_arp {
    my ( $type, $srcmac, $srcip, $destmac, $destip ) = &decode(@_);
    return if ( !isinternal($srcip) );

    if ( $type == 1 ) {
        $logger->debug(
            "ARP who-has $destip tell $srcip  $srcmac $srcip $destmac $destip"
        );
        my $gip = ip2gateway($srcip);
        if ( valid_ip($srcip) && valid_ip($gip) && ( $srcip eq $gip ) ) {
            if (   $destmac =~ /ff:ff:ff:ff:ff:ff/i
                || $destmac =~ /00:00:00:00:00:00/i )
            {
                $logger->info(
                    "broadcast arp request from router for $destip - re-trapping all nodes"
                );
            } elsif ( !grep( { $_ eq $monitor_int } @listen_ints ) ) {
                $logger->info(
                    "flooded arp request from router for $destmac ($destip) - re-trapping all nodes"
                );
            } else {
                $logger->debug(
                    "arp request from router for $destmac ($destip)");
                return;
            }
        }

    } elsif ( $type == 2 ) {
        $logger->debug("ARP $srcip is-at $srcmac $srcmac $srcip $destmac $destip");
    }
}

sub cleanup {
    my $logger = Log::Log4perl::get_logger('pfmon::cleanup');
    Log::Log4perl::MDC->put( 'tid', threads->self->tid() );
    $logger->info("Starting cleanup thread");

    if ( !$restart ) {
        $logger->info("closing open iplogs (just in case)");
        iplog_shutdown();
    } else {
        $logger->info("restarted - leaving iplogs open");
    }

    my $counter = 0;
    while (1) {
        $counter = ( $counter + 1 ) % 10;

        # run these functions every $maintenance_interval * 10
        if ( $counter == 0 ) {
            $logger->info("running expire check");
            iplog_cleanup( $Config{'expire'}{'iplog'} )
                if ( $Config{'expire'}{'iplog'} );
            locationlog_cleanup( $Config{'expire'}{'locationlog'} )
                if ( $Config{'expire'}{'locationlog'} );
            node_cleanup( $Config{'expire'}{'node'} )
                if ( $Config{'expire'}{'node'} );
            traplog_cleanup( $Config{'expire'}{'traplog'} )
                if ( $Config{'expire'}{'traplog'} );
            $logger->info("checking registered nodes for expiration");
            nodes_maintenance();
        }

        sleep $Config{'general'}{'maintenance_interval'};
    }
}

=item decode

Encapsulate the ARP packet decoding.

=cut
sub decode {
    my $pkt = shift;

    my ($m1, $m2, $proto, $hwas, $pas, $hwal, $pal, $opcode, $sha, $spa, $tha, $tpa) 
        = unpack( 'H12H12nnnCCnH12NH12N', $pkt );

    return ($opcode, clean_mac($sha), int2ip($spa), clean_mac($tha), int2ip($tpa));
}

sub daemonize {
    chdir '/' or $logger->logdie("Can't chdir to /: $!");
    open STDIN, '<', '/dev/null'
        or $logger->logdie("Can't read /dev/null: $!");
    my $log_file = "$install_dir/logs/pfmon";
    open STDOUT, '>>', "$log_file"
        or $logger->logdie("Can't write to $log_file: $!");

    defined( my $pid = fork ) or $logger->logdie("pfmon: could not fork: $!");
    POSIX::_exit(0) if ($pid);
    if ( !POSIX::setsid() ) {
        $logger->error("could not start a new session: $!");

        #    die("pfmon: could not start a new session: $!\n");
    }
    open STDERR, '>&STDOUT' or $logger->logdie("Can't dup stdout: $!");
    createpid();
}

sub normal_sighandler {
    deletepid();
    if ( threads->self->tid() == 0 ) {
        $logger->info( "caught SIG" . $_[0] . " - closing open iplogs" );

        iplog_shutdown();
        $logger->logdie( "pfmon: caught SIG" . $_[0] . " - terminating" );
    }
}

=back

=head1 AUTHOR

Dave Laporte <dave@laportestyle.org>

Kevin Amorin <kev@amorin.org>

Dominik Gehl <dgehl@inverse.ca>

Olivier Bilodeau <obilodeau@inverse.ca>

=head1 COPYRIGHT

Copyright (C) 2005 Dave Laporte

Copyright (C) 2005 Kevin Amorin

Copyright (C) 2009-2011 Inverse inc.

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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
USA.

=cut

