#!/usr/local/bin/perl -w
##############################################################################
#
# Network Leak Finder Daemon - netleakd  
# 2005 © Jonas Hansen <jonas.v.hansen@gmail.com>
#
# $Id: netleakd,v 1.1.1.1 2005/01/16 21:42:22 cvs Exp $
#
##############################################################################
#    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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
##############################################################################

use Getopt::Long;
use Sys::Syslog;
use Net::PcapUtils;
use IO::Socket::INET;
use IO::Interface qw(:flags);
use NetPacket::Ethernet;
use NetPacket::IP;
use NetPacket::UDP;
use NetPacket::ICMP;
use Mail::Sendmail;

# Autoflush buffers
$|++;

print "[~] netleakd v.0.1a - Network Leak Finder Daemon\n";

# we need to be root
if ($< != 0) {
    die "[-] You need to be root to run netleakd";
    <>;
}


# Set up the signal handlers
BEGIN {
    $SIG{'INT'}  = sub { write_log("netleakd Stopped"); print "[+] Exiting\n"; exit; };
    $SIG{'QUIT'} = sub { write_log("netleakd Stopped"); print "[+] Exiting\n"; exit; };
}

$d_args{'cfile=s'}     = 1; # configuration file
$d_args{'logfile=s'}   = 1; # logfile
$d_args{'syslog'}      = 1; # boolean flag to trigger syslog
$d_args{'signature=s'} = 1; # String inside each packet
$d_args{'interface=s'} = 1; # Network interface
$d_args{'notify=s'}    = 1; # e-mail to notify if a leak was found
$d_args{'verbose'}     = 1;
$d_args{'help'}        = 1;

# Load command line arguments
load_args();


die "[-] No signature! Don't know what to look for then\n" if !$Args{'signature'};
my $pattern = ".*<$Args{'signature'}(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})>.*";
my $last_time = time()-30;

# start listening
print "[+] Listening...\n";
write_log("netleakd Started");

if ($Args{'interface'}) {
    Net::PcapUtils::loop(\&process_packet, DEV => $Args{'interface'}, FILTER => 'ip');
} else {
    Net::PcapUtils::loop(\&process_packet, FILTER => 'ip');
}

sub process_packet {
    my ($user_data,$header,$packet) = @_;
    my $eth = NetPacket::Ethernet->decode($packet);
    if($eth->{type} == 2048){ # ethernet II frame, IP
	my $ip = NetPacket::IP->decode($eth->{data});
	if ($ip->{proto} == 1) {
	    # Parse ICMP packet
	    my $icmp = NetPacket::ICMP->decode($ip->{data})
		or print "[-] Error parsing icmp packet! $!\n";
	    return if !$icmp->{data};
	    if ($icmp->{data} =~ /$pattern/) {
		my $data = $icmp->{data};
		$data =~ s/$pattern/$1/;
		my $info = "Found leak (".$Args{'signature'}.") ".$data." (icmp ".$icmp->{type}.":".$icmp->{code}.") from ".$ip->{src_ip}."\n";
		print "[!] ".$info;
		write_log($info);
		notify($info);
	    }
	}
	if ($ip->{proto} == 17) {
	    # Parse UDP packet
	    my $udp = NetPacket::UDP->decode($ip->{data})
		or print "[-] Error parsing icmp packet! $!\n";
	    return if !$udp->{data};
	    if ($udp->{data} =~ /$pattern/) {
		my $data = $udp->{data};
		$data =~ s/$pattern/$1/;
		my $info = "Found leak (".$Args{'signature'}.") ".$data." (udp) from ".$ip->{src_ip}."\n";
		print "[!] ".$info;
		write_log($info);
		notify($info);		
	    }	    
	}
    }
}



sub write_log {
    return if !$Args{'logfile'} && !$Args{'syslog'};
    my $message = shift(@_);
    chomp $message;
    
    # write to standard log
    if ($Args{'logfile'}) {
	my $date = get_time();
	open F, ">> $Args{'logfile'}" or die "[-] Can't open logfile $Args{'logfile'}! : $!";
	print F "[".$date."]: ".$message."\r\n" if $message or die "[-] Error writing to logfile $Args{'logfile'}! : $!";
	close F;
    }

    #write to syslog
    if ($Args{'syslog'}) {
	openlog my $program, 'cons,pid', 'user';
	syslog('info', "netleakd: ".$message);
	closelog();
    }
}


sub notify {
    return unless $Args{'notify'};
    
    # throttle amount of mails. Only one mail per 30 seconds.
    $current_time = time();
    return if ( $current_time < $last_time + 30 );
    $last_time = $current_time;

    # send the message
    my $message = shift(@_);
    my %mail = ( To => "$Args{'notify'}", Subject => "Leak found",
		 From => 'root@localhost',
		 Message => "netleakd found a leak:\n\n".$message);
    if (sendmail %mail) { 
	print "[+] Notification mail sent to $Args{'notify'}!\n";
	write_log("Notification mail sent to $Args{'notify'}!\n");
    } else {
	print "[*] Warning: Failed to send notification mail to $Args{'notify'}: $Mail::Sendmail::error\n";
    }
}

sub parse_cf {
    my $file = shift(@_);
    return if !$file;
    open F, "< $file" or die "[-] Can't open configuration-file $file : $!";
    print "[+] Using config file $file\n";
    my @settings = <F>;
    close F;
    
    %CArgs = ();
    foreach my $s ( @settings ) {
	# ignore blank lines
	next if $s =~ /^(\s)*$/;
	# ignore comments
	next if $s =~ /^\#.*/;
	chomp $s;
	my ($key,$value) = split(/\=/,$s,2);
	# ignore trash
	next if !$key || !$value;
	# load everything - even garbage config!! FIXME
	$CArgs{$key} = $value;
	$Args{$key} = $value if !defined($Args{$key});
    }
}    

sub load_args {
    die "Invalid commandline option(s)!\n" unless GetOptions(\%Args, keys %d_args);
    if (defined($Args{'help'})) {
	show_usage();
	exit 0;
    }

    print "[+] Enabling verbose mode\n" if defined($Args{'verbose'});

    if (defined($Args{'cfile'})) {
	parse_cf($Args{'cfile'});
    } else {
	# search for other config files
	(-e '/usr/local/etc/netleakd.conf') && parse_cf('/usr/local/etc/netleakd.conf') &&
	    print "[+] Using config-file /etc/netleak.conf\n";
	(-e '~/.netleakd') && parse_cf('~/.netleakd') &&
	    print "[+] Using config-file /etc/netleak.conf\n";
	(-e '/etc/netleakd.conf') && parse_cf('/etc/netleakd.conf') &&
	    print "[+] Using config-file /etc/netleak.conf\n";
    }

    print "[+] Using logfile $Args{'logfile'}\n" if ($Args{'logfile'} && $Args{'verbose'});   

    $Args{'signature'} = 'IP:' if !$Args{'signature'};
    print "[+] Using signature prefix \"".$Args{'signature'}."\"\n" if $Args{'verbose'};

    print "[+] Writing results to syslog\n" if $Args{'syslog'} && $Args{'verbose'};

    print "[*] Warning: No logging specified!\n" if !$Args{'logfile'} && !$Args{'syslog'};

    $Args{'interface'} = 'eth0' if !$Args{'interface'};
    check_interface($Args{'interface'});
    die "[-] No network interface specified!\n" if !$Args{'interface'};
   
    print "[+] Notifying $Args{'notify'} if leaks are found\n" if $Args{'verbose'} && $Args{'notify'};

}

sub get_time {
    my %months = (
	"1"  => "Jan",
	"2"  => "Feb",
	"3"  => "Mar",
	"4"  => "Apr",
	"5"  => "May",
	"6"  => "Jun",
	"7"  => "Jul",
	"8"  => "Aug",
	"9"  => "Sep",
	"10" => "Oct",
	"11" => "Nov",
	"12" => "Dec",
    );
    my ($Sec, $Min, $Hour, $Day, $Month, $Year, $WeekDay, $DayOfYear, $IsDST) = localtime(time);
    $Month++;
    $Day = "0".$Day if ($Day < 10);    
    $Min = "0".$Min if ($Min < 10);    
    $Sec = "0".$Sec if ($Sec < 10);    
    return "$months{$Month} $Day $Hour:$Min:$Sec";
}

sub check_interface {
    my $s = IO::Socket::INET->new(Proto => 'udp');
    my @interfaces = $s->if_list;
    my $found = 0;
    my $iface = shift(@_);
    for my $if (@interfaces) {
        if($if eq $iface) {
	    $found=1;
	    last;
        }
    }
    die "[-] Interface $iface not found!" if(!$found);
    my $flags = $s->if_flags($iface);
    print "[*] Warning: interface $iface is loopback!\n" if $flags & IFF_LOOPBACK;
    die "[-] Interface $iface not up!" if !($flags & IFF_RUNNING);
    print "[+] Using network interface $iface\n" if $Args{'verbose'};
}


sub show_usage {
    print "Usage: netleakd [OPTION[=VALUE]]\n";
    print " Where pcap-filter is any properly formated pcap-filter. Defaults to \"ip\"\n";
    print "\t--cfile\t\tSpecify alternating configuration file to use\n";
    print "\t--logfile\tWrite results to a logfile\n";
    print "\t--syslog\tWrite results to syslog\n";
    print "\t--signature\tUse the specified signature\n";
    print "\t--interface\tInterface to use. Defaults to eth0.\n";
    print "\t--notify\te-mail to notify is a leak was found\n";
    print "\t--verbose\tPlease yourself with extra output\n";
    print "\n";
}
