#!/usr/bin/perl

=head1 NAME

pfdhcplistener - listen to DHCP requests

=head1 SYNOPSIS

pfdhcplistener -i <interface> [options]

 Options:
   -d     Daemonize
   -h     Help

=cut

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

BEGIN {
    # log4perl init
    use constant INSTALL_DIR => '/usr/local/pf';
    use lib INSTALL_DIR . "/lib";
    use pf::log(service => 'pfdhcplistener');
}

use pf::constants;
use pf::clustermgmt;
use pf::config;
use pf::config::cached;
use pf::util;
use pf::config::util;
use pf::services::util;
use pf::util::dhcp;
use List::MoreUtils qw(any);
use NetAddr::IP;
use pf::SwitchFactory;
use MIME::Base64();
use NetPacket::Ethernet qw(ETH_TYPE_IP);
use NetPacket::IP;
use NetPacket::IPv6;
use NetPacket::UDP;


pf::SwitchFactory::preLoadModules();

# initialization
# --------------
# assign process name (see #1464)
our $PROGRAM_NAME = "pfdhcplistener";

my $logger = get_logger( $PROGRAM_NAME );

# init signal handlers
POSIX::sigaction(
    &POSIX::SIGHUP,
    POSIX::SigAction->new(
        'reload_config', POSIX::SigSet->new(), &POSIX::SA_NODEFER
    )
) or $logger->logdie("pfdhcplistener: could not set SIGHUP handler: $!");

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

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


my %args;
getopts( 'dhi:', \%args );

my $daemonize = $args{d};
my $interface = $args{i};

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

my $is_inline_vlan;
my $interface_ip;
my $interface_vlan;
my $pcap;
my $net_type;
my $process;

sub reload_config {
    $process = pf::cluster::is_vip_running($interface);
    $logger->info("Reload configuration on $interface with status $process");
}

reload_config;


$PROGRAM_NAME = $0 = "${PROGRAM_NAME}_${interface}";

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

my $net_addr = NetAddr::IP->new($Config{"interface $interface"}{'ip'},$Config{"interface $interface"}{'mask'});
my $running_w_dhcpd = $FALSE;

# start dhcp monitor
if ( isenabled( $Config{'network'}{'dhcpdetector'} ) ) {
    if (any { $_ eq $interface } @listen_ints, @dhcplistener_ints ) {
        $net_type = $Config{"interface $interface"}{'type'};
        $interface_ip = $Config{"interface $interface"}{'ip'};
        $interface_vlan = get_vlan_from_int($interface) || $NO_VLAN;

        foreach my $network (keys %ConfigNetworks) {
            my %net = %{$ConfigNetworks{$network}};
            my $network_obj = NetAddr::IP->new($network,$ConfigNetworks{$network}{netmask});
            if(isenabled($net{dhcpd}) && $network_obj->contains($net_addr)){
                $running_w_dhcpd = $TRUE;
                $logger->info("The listener process is on the same server as the DHCP server.");
            }

            # are we listening on an inline interface ?
            next if (!pf::config::is_network_type_inline($network));
            my $ip = new NetAddr::IP::Lite clean_ip($net{'next_hop'}) if defined($net{'next_hop'});
            if (grep( { $_->tag("int") eq $interface} @inline_enforcement_nets) != 0 || (defined($net{'next_hop'}) && $net_addr->contains($ip))) {
                $logger->warn("DHCP detector on an inline interface");
                $is_inline_vlan = $TRUE;
            }
        }
        $logger->info("DHCP detector on $interface enabled");
        dhcp_detector();
    }
    $logger->warn(
        "pfdhcplistener for $interface finished - this is bad. " .
        "Are you sure the interface you are trying to run the listener on is configured in packetfence to do so?"
    );
}

END {
    if ( defined($interface) ) {
        deletepid($PROGRAM_NAME);
        $logger->info("stopping pfdhcplistener for interface $interface");
    }
}

exit(0);

=head1 SUBROUTINES

=over

=cut

sub dhcp_detector {
    my $filter = make_pcap_filter(@{$Config{network}{dhcp_filter_by_message_types}});
    my $filter_t;
    my $net;
    my $mask;
    my $opt = 1;
    my $err;

    # updating process name so we know what interface we are listening on
    # WARNING: the format is expected by watchdog in pf::services. Don't change lightly.
    $PROGRAM_NAME = basename($PROGRAM_NAME) . ": listening on $interface";
    $pcap = Net::Pcap::pcap_open_live( $interface, 576, 1, 0, \$err );

    if (!defined($pcap)) {
        $logger->logdie("Unable to initiate packet capture. Is $interface an actual network interface?");
    }
    $logger->trace("Using filter '$filter'");

    if ((Net::Pcap::compile( $pcap, \$filter_t, $filter, $opt, 0 )) == -1) {
        $logger->logdie("Unable to compile filter string '$filter'");
    }

    Net::Pcap::setfilter( $pcap, $filter_t );
    my $result = Net::Pcap::loop( $pcap, -1, \&process_pkt, [ $interface , $pcap ] );
    $logger->logdie(Net::Pcap::pcap_geterr($pcap)) if ($result == -1);
}

sub process_pkt {
    my ( $user_data, $hdr, $pkt ) = @_;
    if ($process || !$pf::cluster::cluster_enabled){
        eval {
            my $l2 = NetPacket::Ethernet->decode($pkt);
            my $l3 = $l2->{type} eq ETH_TYPE_IP ? NetPacket::IP->decode($l2->{'data'}) : NetPacket::IPv6->decode($l2->{'data'});
            my $l4 = NetPacket::UDP->decode($l3->{'data'});
            my $udp_payload_b64 = MIME::Base64::encode($l4->{data});
            my %args = (
                src_mac => clean_mac($l2->{'src_mac'}),
                dest_mac => clean_mac($l2->{'dest_mac'}),
                src_ip => $l3->{'src_ip'},
                dest_ip => $l3->{'dest_ip'},
                running_w_dhcpd => $running_w_dhcpd,
                is_inline_vlan => $is_inline_vlan,
                interface => $interface,
                interface_ip => $interface_ip,
                interface_vlan => $interface_vlan,
                net_type => $net_type,
            );
            # we send all IPv4 DHCPv4 codepath
            if($l2->{type} eq ETH_TYPE_IP) {
                my $apiclient = pf::api::queue->new(queue => 'pfdhcplistener');
                $apiclient->notify('process_dhcp', %args, udp_payload_b64 => $udp_payload_b64);
            }
            else{
                my $apiclient = pf::api::queue->new;
                $apiclient->notify('process_dhcpv6', $udp_payload_b64);
            }
        };
        if($@) {
            $logger->error("Error processing packet: $@");
        }
    }
    #reload all cached configs after each iteration
    pf::config::cached::ReloadConfigs();
    #Only perform stats when in debug mode
    $logger->debug( sub {
        my $pcap = $user_data->[1];
        my %stats;
        Net::Pcap::pcap_stats($pcap,\%stats);
        return join(' ','pcap_stats',map { "$_ = $stats{$_}"  } keys %stats);
    });
}

sub normal_sighandler {
    Net::Pcap::pcap_breakloop($pcap);
    $logger->trace( "pfdhcplistener: caught SIG" . $_[0] . " - terminating" );
}

=back

=head1 BUGS AND LIMITATIONS

Probably

=head1 AUTHOR

Inverse inc. <info@inverse.ca>

Minor parts of this file may have been contributed. See CREDITS.

=head1 COPYRIGHT

Copyright (C) 2005-2015 Inverse inc.

Copyright (C) 2005 Kevin Amorin

Copyright (C) 2005 David LaPorte

=head1 LICENSE

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

