#!/usr/bin/perl

## Toto je specialna vyukova verzia programu IPwatchD s rozsirenym
## komentarom pre ucely clanku na http://www.jariq.sk/item-23.html
## Rozsireny komentar vzdy zacina dvomi mriezkami.
## Oficialna stranka projektu je http://ipwatchd.sourceforge.net

#############################################################################
#                                                                           #
#  IPwatchD - IP conflict detection in Linux systems                        #
#  Copyright (C) 2006  Jaroslav Imrich <jariq@users.sourceforge.net>        #
#                                                                           #
#  This program is free software; you can redistribute it and/or modify     #
#  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  #
#  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.              #
#                                                                           #
#############################################################################

## Moduly PERLU, ktore program pouziva.
## Vsetky sa daju najst na http://search.cpan.org
## V defaultnej instalacii perlu vacsinou nie su Net::Pcap a NetPacket
## Doinstalovat sa daju pomocou MCPAN prikazmi:
## perl -MCPAN -e 'install Net::Pcap'
## perl -MCPAN -e 'install NetPacket'

use strict;
use POSIX;
use Sys::Syslog;
use Net::Pcap;
use NetPacket::ARP;
use NetPacket::Ethernet;

## Deklaracia premennych. V strict mode musi byt kazda
## premenna deklarovana. Ak sa uvadza na jednom riadku
## viac premennych pouzivaju sa zatvorky

# Syslog options
my $syslog_facility = "daemon";
my $syslog_priority = "err";

# Global variables
my ($dev, $mode, $username, $groupname);
my $instance;
my ($test, $dev_ip, $dev_mac);
my $arping;
my ($userid, $groupid, $orig_uid, $orig_gid);
my ($sigset, $sigaction);
my ($error, $pid);
my ($address, $netmask);
my ($object, $filter);
my $message;

## Volanie vlastnej funkcie get_cmd_parameters, ktora je definovana
## dalej v programe a zabezpecuje prebratie parametrov z prikazoveho riadka.

# Get parameters from command line
&get_cmd_parameters();

## $< je specialna premenna je v nej UID pouzivatela  ktory spustil program.
## Viac o specialnych premennych http://perldoc.perl.org/perlvar.html
## alebo http://affy.blogspot.com/p5be/ch12.htm
## Ak program nespustil root ukonci sa s chybovym hlasenim a exit kodom -1.

# Check if program was executed by root
if (not $< == 0) {
        print "You must be root to run this program..\n";
        exit(-1);
}

## Kontrola vlastneho PID suboru. Ak by bol program nahodou nekorektne ukonceny (kill -9 PID)
## tak PID subor moze ostat na disku. Preto je este kontrolovany zoznam procesov.

# Check if program is not running already
if (-e "/var/run/ipwatchd-$dev.pid") {
        $instance = `ps aux | grep ipwatchd | grep -v "grep ipwatchd" | grep -c $dev`;
        chomp($instance);
        if ($instance ne "1") {
                print "There seems to be one instance of IPwatchD already running on $dev\n";
                exit(-1);
        }
}

## Ziskanie IP adresy definovaneho rozhrania. Parsuje sa z textoveho vystupu
## prikazu ifconfig. Parsovanie prebieha s vyuzitim tzv. regularnych vyrazov
## (regular expressions). Skvele su vysvetlene v seriali na linuxsoft.cz -
## http://www.linuxsoft.cz/article.php?id_article=947 a v jeho dalsich dieloch

# Get IP address of selected device
$test = `ifconfig $dev | grep -c "inet addr"`;
chomp($test);
if ($test == 1) {
    $dev_ip = `ifconfig $dev | grep "inet addr"`;
    chomp($dev_ip);
    $dev_ip =~ /^.*inet addr.(\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}) .*$/;
    $dev_ip = $1;
} else {
    print "Device $dev does not exist or does not have IP address assigned.\n";
    exit(-1)
}

## Parsovanie MAC adresy zvoleneho rozhrania

# Get MAC address of selected device
$test = `ifconfig $dev | grep -c "HWaddr"`;
chomp($test);
if ($test == 1) {
    $dev_mac = `ifconfig $dev | grep "HWaddr"`;
    chomp($dev_mac);
    $dev_mac =~ /^.*HWaddr.(.[^ ]*) .*$/;
    $dev_mac = lc($1);
    $dev_mac =~ s/://g;
} else {
    print "Device $dev does not exist or does not have MAC address.\n";
    exit(-1)
}

## Overenie, ci existuje v systeme utilita arping. Prehladavaju sa standardne
## miesta, kde by sa mohla nachadzat. Tato utilita je potrebna ak program bezi v
## aktivnom mode. Bez nej moze bezat iba v pasivnom mode.

# Check if arping is installed
if ($mode eq 'active') {
    my @dirs = ('/bin', '/sbin', '/usr/bin', '/usr/sbin', '/usr/local/bin', '/usr/local/sbin');
    my $i;

    for ($i=0; $i<=5; $i++) {
        if (-x "$dirs[$i]/arping") {
            $arping = "$dirs[$i]/arping";
            last;
        }
    }

    if (not defined $arping) {
        print "Utility arping (part of iputils package) not found. Passive mode forced.\n";
        $mode = 'passive';
    }
}

## Ziskanie UID a GID neprivilegovaneho usera a skupiny, pod ktorymi
## ma daemon bezat. Viac informacii o pouzitych funkciach na
## http://www.perl.com/doc/manual/html/pod/perlfunc/getpwnam.html
## http://perldoc.perl.org/functions/getgrnam.html

# Check if future effective user and group exists
if ((defined $username) and (defined $groupname)) {

    if (not $userid = getpwnam($username)) {
        print "Unable to find UID for specified user.\n";
        exit(-1);
    }

    if (not $groupid = getgrnam($groupname)) {
        print "Unable to find GID for specified group.\n";
        exit(-1);
    }
}

## Daemonizacia programu velmi podobna daemonizacii v jazyku C
## http://www.linuxprofilm.com/articles/linux-daemon-howto.html
## V pripade uspesneho forku detskeho procesu, sa tento povodny
## ukoncuje. Detstky dedi vsetko po rodicovskom, ci uz sa jedna
## o premenne, file-descriptory...
## Daemon nema pristup k terminalu, takze standardny vstup (STDIN),
## standardny vystup (STDOUT) i standardny chybovy vystup (STDERR)
## su presmerovane do /dev/null. Jedina moznost ako od tohto momentu
## vediet co program robi, je logovat bud do vlastnych suborov, alebo
## pomocou syslogu.

# Daemonize program
# Since now all error messages must be logged with syslog
chdir '/' or die "Can not chdir to /: $!";
umask 0;
if (not open(STDIN,"/dev/null")) {
    &logger($syslog_facility, $syslog_priority, "Can not read /dev/null: $!");
    exit(-1);
}
if (not open(STDOUT,">>/dev/null")) {
    &logger($syslog_facility, $syslog_priority, "Can not write to /dev/null: $!");
    exit(-1);
}
if (not open(STDERR, ">>/dev/null")) {
    &logger($syslog_facility, $syslog_priority, "Can not write to /dev/null: $!");
    exit(-1);
}
$pid = fork;
if (not defined $pid) {
    &logger($syslog_facility, $syslog_priority, "Can not fork: $!");
    exit(-1);
}
exit if $pid;
if (not setsid) {
    &logger($syslog_facility, $syslog_priority, "Can not start a new session: $!");
    exit(-1);
}

## Vytvorenie PID suboru. Pre beh daemona nie je potrebny, ale da sa podla
## jeho existencie kontrolovat ci daemon bezi. Navyse ak obsahuje PID daemona,
## nemusia ine programy, ktore chcu napriklad zaslat daemonovi signal zistovat
## jeho PID zo zoznamu procesov, ale staci, ak si ho precitaju z tohto suboru.

# Create PID file
if (not open(PID,">/var/run/ipwatchd-$dev.pid")) {
    &logger($syslog_facility, $syslog_priority, "Can not open PID file: $!");
    exit(-1);
}
print PID $$;
close(PID);

## Vytvorenie obsluhy signalu SIGTERM, ktory je zaslany napriklad prikazom "kill PID".
## Funkcia program_shutdown, ktora je priradena obsluhe tohto signalu zabezpeci
## napr. zmazanie PID suboru. Popis pouzitych funkcii je na
## http://search.cpan.org/~nwclark/perl-5.8.8/ext/POSIX/POSIX.pod

# SIGTERM handling
$sigset = POSIX::SigSet->new();
$sigaction = POSIX::SigAction->new('program_shutdown', $sigset, &POSIX::SA_NODEFER);
POSIX::sigaction(&POSIX::SIGTERM, $sigaction);

## Ziskanie informacii o sietovom rozhrani pre modul Net::Pcap.
## http://www.perlmonks.org/index.pl?node_id=170648

# Get network address and mask for selected device
if (Net::Pcap::lookupnet($dev, \$address, \$netmask, \$error)) {
    &logger($syslog_facility, $syslog_priority, "Unable to look up net information for $dev - $error.");
    exit(-1);
}

## Vytvorenie Net::Pcap objektu na vybranom sietovom rozhrani.
## http://www.perlmonks.org/index.pl?node_id=170648

# Create packet capture object on selected device
$object = Net::Pcap::open_live($dev, 1500, 0, 0, \$error);
if (not defined $object) {
    &logger($syslog_facility, $syslog_priority, "Unable to create packet capture on device $dev - $error.");
    exit(-1);
}

## Privilegia roota uz nie su potrebne. Objekt na zachytavanie paketov
## je vytvoreny, mozeme sa ich teda zbavit. Neviem ci je to spravny
## postup, nenasiel som vela informacii o uvolnovani privilegii v Perle,
## ale v kazdom pripade funguje. Opat su pouzite specialne premenne.

# Drop privileges
if (($mode eq "passive") and (defined $userid) and (defined $groupid)) {
    $orig_gid = $);
    $orig_uid = $>;
    $) = "$groupid $groupid";
    $> = $userid;
}

## Vytvorenie filtra, ktory zabezpeci zachytavanie iba ARP paketov
## http://www.perlmonks.org/index.pl?node_id=170648

# Create capture filter
if (Net::Pcap::compile($object, \$filter, 'arp', 0, $netmask)) {
    &logger($syslog_facility, $syslog_priority, "Unable to compile packet capture filter.");
    exit(-1);
}

## Spojenie filtra s objektom na zachytavanie paketov
## http://www.perlmonks.org/index.pl?node_id=170648

# Attach filter to packet capture object
if (Net::Pcap::setfilter($object, $filter)) {
    &logger($syslog_facility, $syslog_priority, "Unable to set packet capture filter.");
    exit(-1);
}

## Zapisanie informacie o starte programu do systemovych logov.
## Logger je vlastna funkcia definovana dalej v kode. Pri volani vlastnych
## funkcii ci procedur (v perle jednotne nazyvane podprogramy) sa zvykne
## pred meno procedury davat &. Nie je to nutne, no sprehladnuje to kod.

# Log start of the daemon activity
logger($syslog_facility, "info", "Listening on $dev in $mode mode..");

## Urcenie callback funkcie, ktora spracuva kazdy jeden paket,
## ktory Net::Pcap prijal. V tomto pripade vdaka filtru iba ARP
## pakety. Toto je hlavny cyklus daemona.

# Set callback function to initiate packet capture loop
Net::Pcap::loop($object, -1, \&analyse_packet, '');

## Tu by program nikdy nemal dojst. Pretoze je zacykleny v prijimani a
## spracovavani paketov. Uzatvorenie objektu na zachytavanie paketov a
## ukoncenie programu s exitkodom 0 je teda zbytocne.

# Close packet capture object
Net::Pcap::close($object);

exit(0);

# Hypotetical end of program. Functions follow.. In order of appearance :)



## Funkcia na ziskanie parametrov z prikazoveho riadka.
## Pole @ARGV obsahuje zoznam tychto parametrov. Priradenim pola do
## skalarnej premennej $params ziskame pocet prvkov pola, teda pocet
## parametrov. Funkcia validuje prijate parametre a uklada ich do
## prislusnych globalnych premennych.

# Get parameters from command line
sub get_cmd_parameters {

    my $params = @ARGV;

    # Example: ipwatchd --help
    if ($params == 1) {
        
        if (($ARGV[0] eq '-h') or ($ARGV[0] eq '--help')) { &print_help() }
        elsif (($ARGV[0] eq '-v') or ($ARGV[0] eq '--version')) { &print_version() }
        else { &print_usage() }

    # Example: ipwatchd -i eth0
    } elsif ($params == 2) {

        if (($ARGV[0] eq '-i') and ($ARGV[1] ne '')) {
            $dev = $ARGV[1];
            $mode = "active";
        } else { &print_usage() }

    # Example: ipwatchd -i eth0 -p
    } elsif ($params == 3) {

        if (($ARGV[0] eq '-i') and ($ARGV[1] ne '') and ($ARGV[2] eq '-p')) {
            $dev = $ARGV[1];
            $mode = "passive";
        } else { &print_usage() }

    # Example: ipwatchd -i eth0 -p -u jariq -g users
    } elsif ($params == 7) {

        if (($ARGV[0] eq '-i') and ($ARGV[1] ne '') and ($ARGV[2] eq '-p') and ($ARGV[3] eq '-u') and ($ARGV[4] ne '') and ($ARGV[5] eq '-g') and ($ARGV[6] ne '')) {
            $dev = $ARGV[1];
            $mode = "passive";
            $username = $ARGV[4];
            $groupname = $ARGV[6];
        } else { &print_usage() }

    } else { &print_usage() }

}

## Vypise help na STDOUT

# Hmm.. What is this doing?
sub print_help {
    print "IPwatchD - IP conflict detection in Linux systems\n";
    print "\n";
    print "Usage: ipwatchd -i device [-p [-u username -g groupname]]\n";
    print "\n";
    print "-i device         Device that should be monitored/protected\n";
    print "-p                Passive mode (conflicts are only logged)\n";
    print "-u username       Run daemon as unprivileged user\n";
    print "-g groupname      and unprivileged group\n";
    print "\n";
    print "or     ipwatchd -v|-h\n";
    print "\n";
    print "-v, --version     Prints program version\n";
    print "-h, --help        Displays this help message\n";
    print "\n";
    print "If IPwatchD running in active mode (default) detects gratuitous\n";
    print "ARP request with IP address of monitored interface (IP conflict)\n";
    print "it immediately sends ARP reply to the conflicting host and also\n";
    print "gratuitous ARP request to update cache of neighbouring hosts\n";
    print "on local network.\n";
    print "\n";
    print "This version of IPwatchD uses arping from iputils package to send\n";
    print "ARP packets in active mode.\n";
    print "\n";
    print "Please send any bug reports to jariq\@users.sourceforge.net\n";
    print "For more information please visit http://ipwatchd.sourceforge.net\n";
    exit(0);
}

## Vypise verziu programu na STDOUT

# Print version of program
sub print_version {
    print "IPwatchD v0.1 (beta)\n";
    exit(0)
}

## Vypise skrateny help na STDOUT

# Print short message about usage
sub print_usage {
    print "Usage: ipwatchd -i device [-p [-u username -g groupname]]\n";
    print "Try 'ipwatchd -h' or 'ipwatchd --help' for more information.\n";
    exit(-1);
}

## Funkcia na logovanie udalosti cez syslog. Pri syslogu existuje clenenie sprav
## podla facility a priority (man syslog.conf). Na zaciatku programu su definovane
## premenne $syslog_facility (daemon) a $syslog_priority (err), ktore su touto funkciou
## ocakavane ako vstupne parametre.
## http://search.cpan.org/~saper/Sys-Syslog-0.18/Syslog.pm

# Log event with syslog
sub logger {

        ## Vstupne parametre funkcie sa preberaju z pola @_
        ## Vid specialne premenne

        my ($fac, $prio, $msg) = @_;
        #setlogsock('unix');
        openlog('ipwatchd', 'pid,cons', $fac);
        syslog($prio, $msg);
        closelog();

}

## Obsluzna funkcia pre signal SIGTERM

# Safely shutdown daemon if SIGTERM received
sub program_shutdown {

        ## Effective user sa prepne spat na roota aby mal dostatocne opravnenia
        ## na uzavretie Net::Pcap objektu a vymazanie PID suboru.
        ## Nie som si isty, ci je takyto postup bezpecny. K tejto problematike som
        ## nenasiel vhodnu dokumentaciu - navrhy na zlepsenie su vitane!

        # Regain privileges
        if (($mode eq "passive") and (defined $userid) and (defined $groupid)) {
               $> = $orig_uid;
               $) = $orig_gid;
        }

        Net::Pcap::close($object);
        logger($syslog_facility, "info", "Shutting down (SIGTERM received)");
        unlink("/var/run/ipwatchd-$dev.pid");
        exit(0);

}

## Funkcia spracovavajuca kazdy prijaty ARP paket

# Analyse every captured ARP packet
sub analyse_packet {

        my ($user_data, $header, $packet) = @_;
        my ($rec_smac, $rec_sip, $rec_dmac, $rec_dip);

        ## Dekodovanie informacii z paketov
        ## http://www.perlmonks.org/index.pl?node_id=170648

        my $eth_obj = NetPacket::Ethernet->decode($packet);
        my $arp_obj = NetPacket::ARP->decode($eth_obj->{data}, $eth_obj);

        ## Ziskanie zdrojovych a cielovych MAC a IP adries
       
        $rec_smac = lc($arp_obj->{sha});
        $rec_sip = &hex2dec($arp_obj->{spa});
        $rec_dmac = lc($arp_obj->{tha});
        $rec_dip = &hex2dec($arp_obj->{tpa});

        ## Ak je zdrojova IP adresa z paketu rovnaka ako adresa na sietovom rozhrani
        ## a zdrojova MAC adresa z paketu ina nez na nasom sietovom rozhrani jedna sa
        ## o IP konflikt.

        if (($rec_sip eq $dev_ip) and ($rec_smac ne $dev_mac)) {

                ## V aktivnom mode je udalost zalogovana a je spustena utilita arping,
                ## ktora vysle gratuitous ARP reply. Aby bol zabezpeceny update
                ## udajov na okolitych pocitacoch, switchoch a dalsich sietovych zariadeniach,
                ## vysle aj gratuitous ARP request.

                if ($mode eq "active") {

                # Log event with syslog
                $message = "MAC address ".&printable_mac($rec_smac)." is trying to be our address $dev_ip and we are trying to protect";
                &logger($syslog_facility, $syslog_priority, $message);

                        # Send gratuitous ARP reply to the network
                        `$arping -A -c 1 -I $dev $dev_ip`;
       
                        # Defend our IP by sending gratuitous ARP request
                        # that updates cache of every host on the network
                        `$arping -U -c 1 -I $dev $dev_ip`;

                ## V pasivnom mode je udalost iba zalogovana

                } else {

                        # Log event with syslog and take no action
                        $message = "MAC address ".&printable_mac($rec_smac)." is trying to be our address $dev_ip";
                        &logger($syslog_facility, $syslog_priority, $message);

                }
        }
}

## Z dekodovaneho paketu je ziskana IP adresa v hexa tvare.
## Tato funkcia sluzi na jej prevod na klasicky dekadicky tvar.

# Convert IP address into decimal form
sub hex2dec {

        my ($hexip) = @_;
        my ($a, $b, $c, $d, $ip);
        $a = hex substr($hexip, 0, 2);
        $b = hex substr($hexip, 2, 2);
        $c = hex substr($hexip, 4, 2);
        $d = hex substr($hexip, 6, 2);
        $ip = "$a.$b.$c.$d";
        return ($ip);

}

## Funkcia na konverziu MAC adresy ziskanej z dekodovaneho paketu
## na format, ktory je porovnatelny s vystupom z programu ifconfig.

# Convert MAC address into printable form
sub printable_mac {

        my ($mac) = @_;
        my ($a, $b, $c, $d, $e, $f);
        $a = substr($mac, 0, 2);
        $b = substr($mac, 2, 2);
        $c = substr($mac, 4, 2);
        $d = substr($mac, 6, 2);
        $e = substr($mac, 8, 2);
        $f = substr($mac, 10, 2);
        $mac = "$a:$b:$c:$d:$e:$f";
        return($mac);

}