|
|
#!/usr/bin/perl
#
# firewatch - monitor your ipchains logfile with active notification
#
# This code was written by Scott R. Keszler keszler@srkconsulting.com
# and placed into the public domain in 2000 by SRK Consulting.
#
# If you modify|improve this program, please email the above address
# with your changes.
#
# Based on poprelayd by Curt Sampson cjs@cynic.net which was placed into
# the public domain in 1998 by Western Internet Portal Services, Inc.
#
# Best edited with xterm having 155 columns :)
#
# Configuration settings.
#
my $logfile = "/var/log/kernlog"; # logfile to which ipchains syslogs
my $pidfile = "/var/run/firewatch.pid"; # Where we put our PID.
my $notify = "keszler\@srkconsulting.com"; # who gets the email notifications
my $sendmail = "/usr/sbin/sendmail"; # location of sendmail
# in the following explanations of config settings, "ignore" means that firewatch will not send email notifications
# when these protocols, ip addresses, and/or ports appear in the logfile
# # list of protocols to ignore
my @igproto = ( 89 ); # ignore any IGP broadcasts - appear when running tcpdump (i.e. interface in promiscuous mode)
# # list of IP addresses to ignore (all four octets, haven't yet needed to ignore a range)
my @igip = ( "205.198.30.65", "207.70.233.191" ); # my gateway, my isp's DHCP server
# # list of ports to ignore
my @igport = ( 53, 68 ); # DNS (mine is open, but sometimes see broadcasts from my gateway to 255.255.255.255:53), DHCP)
#
# Modules
#
use strict;
use Fcntl;
use POSIX;
use Socket;
#
# Variables
#
my $pid = undef; # Process ID.
my $lffd = undef; # $logfile file descriptor.
my $lfino = undef; # Inode of $logfile when we opened it.
my $lfbuf = undef; # Buffer for data from $lffd.
my $lasttimeout = undef; # Last time we did a timeout.
#
# Subroutines
#
# getlogline()
#
# Return the next line from $logfile, or undef if one isn't currently ready.
#
# XXX Note that there's a bug in this routine that causes it to ignore
# blank lines. I kinda like this behaviour, so I've not fixed it.
#
sub getlogline {
my $ino;
my $foundeof = 0;
my $buf;
my $count;
# The first time we're called; open the logfile, skip to the end,
# and remember the inode we opened.
if (!defined($lffd)) {
$lffd = POSIX::open($logfile, O_RDONLY|O_NONBLOCK, 0);
if (!defined($lffd)) {
die "Can't open $logfile\n";
}
if (POSIX::lseek($lffd, 0, &POSIX::SEEK_END) == -1) {
die "Can't seek to end of $logfile\n";
}
(undef, $lfino, undef) = POSIX::fstat($lffd);
}
# Append new data, if available, to our buffer.
$count = POSIX::read($lffd, $buf, 1024);
if ($count) {
$lfbuf .= $buf;
}
# Return a line, if we have one.
if ($lfbuf =~ m/\n/m) {
($buf, $lfbuf) = split(/\n/m, $lfbuf, 2);
return $buf;
}
# Check the inode number of $logfile; if it's not the saved one,
# the logfile has been rotated and we need to reopen.
(undef, $ino, undef) = POSIX::stat($logfile);
if ($ino != $lfino) {
POSIX::close($lffd);
$lffd = POSIX::open($logfile, O_RDONLY|O_NONBLOCK, 0);
if (!defined($lffd)) {
die "Can't open $logfile\n";
}
(undef, $lfino, undef) = POSIX::fstat($lffd);
}
return undef;
}
# scanaddr($line)
#
# example ipchains kernel log
# Feb 14 09:39:33 hal kernel: Packet log: input DENY eth0 PROTO=17 205.198.30.65:1887 255.255.255.255:53 L=73 S=0x00 I=0 F=0x0000 T=255
# Feb 1 13:56:42 hal kernel: Packet log: input DENY eth0 PROTO=17 63.20.74.17:137 205.198.30.73:137 L=78 S=0x00 I=33890 F=0x0000 T=117
# Feb 2 05:33:27 hal kernel: Packet log: input DENY eth0 PROTO=17 207.70.233.191:67 255.255.255.255:68 L=328 S=0x00 I=2547 F=0x0000 T=62
# Jan 28 08:58:44 hal kernel: device eth0 entered promiscuous mode
# Jan 28 08:59:09 hal kernel: Packet log: input DENY eth0 PROTO=89 205.198.30.65:65535 224.0.0.5:65535 L=64 S=0xC0 I=10 F=0x0000 T=1
# Jan 28 08:59:22 hal kernel: device eth0 left promiscuous mode
# Feb 10 17:27:43 hal kernel: Packet log: input DENY eth0 PROTO=6 212.204.141.76:2326 205.198.30.73:21 L=60 S=0x00 I=40896 F=0x4000 T=47
# Feb 10 17:37:44 hal kernel: Packet log: input DENY eth0 PROTO=6 212.204.141.76:4222 205.198.30.73:12346 L=60 S=0x00 I=46909 F=0x4000 T=47
#
# example Netgear RT314 log
# Feb 18 19:03:53 192.168.1.1 NetgearRT314: IP[Src=206.155.220.159 Dst=64.231.11.245 TCP spo=02051 dpo=27374]}S01>R04mD
# Feb 18 19:57:55 192.168.1.1 NetgearRT314: IP[Src=203.79.195.157 Dst=64.231.11.245 TCP spo=01197 dpo=00111]}S01>R01mD
#
# Scan $line to see if it's from ipchains
# Return the protocol number, origin IP, gethostbyaddr(origin IP), and destination port; or undef if not a packet log
#
# ipchains:
# $1 = numeric protocol
# $2 = origin IP
# $3 = dest port
#
# Netgear RT314:
# $1 = origin IP
# $2 = alpha protocol
# $3 = dest port
sub scanaddr ($) {
my $s = shift;
# Netgear RT314
# if ($s =~ /NetgearRT314.*Src=(\d+\.\d+\.\d+\.\d+).* (.*) spo=.*dpo=([0-9]+)/) {
# my $orig = gethostbyaddr(inet_aton($1), AF_INET);
# return ( $2, $1, $orig, $3 );
# ipchains
if ($s =~ /Packet log.*PROTO=(\d+)\s+(\d+\.\d+\.\d+\.\d+):\d+\s+\d+\.\d+\.\d+\.\d+:(\d+)/) {
my $orig = gethostbyaddr(inet_aton($2), AF_INET);
return ( $1, $2, $orig, $3 );
} else {
return ( undef, undef, undef, undef );
}
}
# cleanup
#
# Clean up and exit; executed on receipt of a sighup.
#
sub cleanup {
unlink $pidfile;
exit 0;
}
#
# Main Program
#
# Check to see we can read/write the files we need.
die "Can't read $logfile: $!\n" if ! -r $logfile;
my %protohash;
{
my ($proto, $pnum, $pdesc);
open PROTO, "/etc/protocols" or die "cannot open /etc/protocols: $!\n";
while () {
next if /^#/;
next if /^$/;
($proto, $pnum, undef, $pdesc) = split /\s+/, $_, 4;
chomp $pdesc;
# Netgear RT314:
# $protohash{uc($proto)} = $pnum . " " . $pdesc;
# ipchains
$protohash{$pnum} = $proto . " " . $pdesc;
}
close PROTO;
}
my %porthash;
{
my ($port, $pnum, $pdesc, $pproto);
open PORT, "/etc/services" or die "cannot open /etc/services: $!\n";
while () {
next if /^#/;
next if /^$/;
($port, $pnum, $pdesc) = split /\s+/, $_, 3;
($pnum, $pproto) = split '/', $pnum;
chomp $pdesc;
$porthash{$pproto}{$pnum} = $port . " " . $pdesc;
}
close PORT;
}
my %igproto;
foreach my $proto (@igproto) {
$igproto{$proto}++;
}
my %igip;
foreach my $ip (@igip) {
$igip{$ip}++;
}
my %igport;
foreach my $port (@igport) {
$igport{$port}++;
}
# Become a daemon: fork, detach, cd /, set creation mode to 0.
if ($pid = fork) {
exit 0; # Parent.
} elsif (defined($pid)) {
$pid = getpid; # Child.
} else {
die "Can't fork: $!\n";
}
# Catch signals.
$SIG{INT} = \&cleanup;
$SIG{TERM} = \&cleanup;
$SIG{HUP} = \&cleanup;
# Write PID file.
open(PIDFILE, ">$pidfile") || die "Can't open PID file: $!\n";
print PIDFILE "$pid\n";
close(PIDFILE);
chmod 0644, $pidfile;
# Detach from terminal, etc.
setpgrp(0, 0);
close(STDIN); close(STDOUT); close(STDERR);
chdir("/");
my @newlines;
my $line;
my $body;
# Main loop.
while (1) {
while ($line = getlogline) {
#print "gotlog: $line\n";
# Netgear RT314:
# my ($prototxt, $orig, $origname, $dport) = scanaddr($line);
# my ($protonum, undef) = split /\s+/, $protohash{$prototxt}, 2;
# ipchains
my ($protonum, $orig, $origname, $dport) = scanaddr($line);
my ($prototxt, undef) = split /\s+/, $protohash{$protonum}, 2;
#print "pn: $protonum pt: $prototxt oi:$orig on:$origname pt:$dport\n";
if ((defined $protonum) && !((defined $igproto{$protonum}) || (defined $igip{$orig}) || (defined $igport{$dport}))) {
# Netgear RT314:
# push @newlines, "$line\nProtocol $protonum ($protohash{$prototxt})\norigin: $orig (host: $origname)\nport = $dport ($porthash{$prototxt}{$dport})\n\n";
# ipchains
push @newlines, "$line\nProtocol $protonum ($protohash{$protonum})\norigin: $orig (host: $origname)\nport = $dport ($porthash{$prototxt}{$dport})\n\n";
#print "pushed\n";
}
}
$body = undef;
while ($line = shift(@newlines)) {
$body .= $line;
}
if (defined $body) {
open MAIL, "|$sendmail $notify" or die "cannot open pipe to sendmail: $!\n";
print MAIL "From: firewatch\nTo: $notify\nSubject: Firewatch notification\n", $body;
close MAIL;
}
sleep 600;
}
|