#!/usr/bin/perl

# program laeser mail loggen hvert 10. minut og finder ipadresser der laver ulovlig
# forsøg på connect til MTA. Ved tre ulovlige forsøg inden for 20 minutter blokkeres
# ip adressen i 24 timer i firewall.
#
# IPTABLES
# ====================================
# Blok af ip adresse: iptables -A INPUT -s XXX.XXX.XXX.XXX -j DROP
#
# Slet regel: iptables -D INPUT -s XXX.XXX.XXX.XXX -j DROP
#
# FIREWALLD
# ====================================
# Blok af ip adresse:
# firewall-cmd --permanent --add-rich-rule='rule family=ipv4 source address=XXX.XXX.XXX.XXX reject'
#
# Slet regel:
# firewall-cmd --permanent --remove-rich-rule='rule family=ipv4 source address=XXX.XXX.XXX.XXX reject'
# Udfør med:
# firewall-cmd --reload
#
# Se regler med:
# firewall-cmd --list-all

# Data gemmes i en datadase: autofirewall
# Tabel for blokerede adresser:  ipblock - ip(adresse), blocked(tidspunkt)
# Tabel for mulige ulovligheder: iptry   - ip(adresse), firsttry(tid), count(forsøg)

use warnings;
use strict;
use DBI;

my $maillog 		= "/var/log/mail";
my $firewalldb		= "autofirewall";
my $maxtries		= 3;
my $tryinterval		= 20; # minutter
my $testinterval	= 10; # minutter
my $blockinterval	= 24; # timer
my $wanaddr		= "aaa.bbb.ccc.ddd";
my @whitelist		= ("aaa.bbb.ccc" );
my @iptables		= ();
my $dbh;
my $sth;
my @tid;
my $tid_nu;
my $debug		= 0; # 1 = debug; 0 = ingen debug

# ### SUBS ###

#
# Lav datofilter

sub date_filter {
	my $interval 	= shift;

	my @tidbeg 	= localtime(time - $interval * 60);
	my $filter 	= "";
	my $min		= int($tidbeg[1] / 10);

	$filter = sprintf "%4d-%02d-%02dT%02d:%.1s", $tidbeg[5]+1900, $tidbeg[4]+1, $tidbeg[3], $tidbeg[2], $min;

	return $filter;

}

# Hent adresser fra iptables til et array
sub h_iptables_drop {
	my $ipt_ref	= shift;

	my @linier	= ();

	open IPTABLES, "/usr/bin/firewall-cmd --list-all|" or die "Kunne ikke udfoere firewall-cmd\n";

	@linier = grep /^reject/, <IPTABLES>;
	foreach (@linier) {
		if ( /\s+rule.*"([\d\.]{7,15})" reject$/ ) {
			push @$ipt_ref, $1;
			print "$1 \tblokeret i iptables\n" if ($debug >0);
		}
	}

}

## Fix firewall efter reboot/firewall reload
#  Ikke nødvendig med firewalld
sub f_iptables {
	my $ipt_ref	= shift;

	my $blok_ip;
	my @row		= ();
	my $fundet	= 0;
	my @iptableCmd	= ();

	SQL_prep("SELECT ip FROM ipblock");
	SQL_test();
	SQL_exec();

	while ( @row = $sth->fetchrow_array() ) {
		$blok_ip = $row[0];
		print "$blok_ip i databasen\n" if ($debug > 0);
		$fundet = 0;
		foreach ( @$ipt_ref ) {
			if ( /^$blok_ip/ ) {
				$fundet = 1;
				last;
			}
		}
		if ( $fundet == 0 ) {
			@iptableCmd  = ("/usr/sbin/iptables -I INPUT -s $blok_ip -j DROP");
			system(@iptableCmd);# or die "iptables kommando fejlede: @iptableCmd: $?\n";
			print "@iptableCmd\n" if ($debug >  0);
		}
	}

}

# Find uønskede forespørgsler
sub fallout {
	my $log 	= shift;
	my $filter	= shift;
	my $tid		= shift;

	my $ip		= "";

	open MAILLOG, "<$log" or die "Kunne ikke aabne $log\n";

	LOG: while ( <MAILLOG> ) {
		next if ( /$wanaddr/ );
		next if ( ! /$filter/ );
		
		if ( /reject.* \[(\d+\.\d+.\d+.\d+)\] / ) {
			$ip = $1;
			print "Reject ip $ip\n" if ($debug == 0) ;
			preblock($ip, $tid);
			next;
		}

		if ( /\[(\d+\.\d+.\d+.\d+)\]:.*authentication failure/ ) {
			$ip = $1;
			print "Auth fail $ip\n" if ($debug == 0) ;
			preblock($ip, $tid);
			next;
		}

		if ( /does not resolve to address (\d+\.\d+.\d+.\d+):/ ) {
			$ip = $1;
			print "FQDN mismatch $ip\n" if ($debug == 0) ;
			preblock($ip, $tid);
			next;
		}
		
		if ( /NOQUEUE: reject: RCPT from\s+.*\[(\d+\.\d+.\d+.\d+)\]:/ ) {
			$ip = $1;
			print "Reject no user $ip\n" if ($debug == 0) ;
			preblock($ip, $tid);
			next;
		}

		if ( /lost connection after AUTH from unknown\[(\d+\.\d+.\d+.\d+)\]/ ) {
                        $ip = $1;
                        print "Lost connection after AUTH $ip\n" if ($debug == 0) ;
                        preblock($ip, $tid);
                        next;
		}
		if ( /disconnect from unknown\[(\d+\.\d+.\d+.\d+)\]/ ) {
                        $ip = $1;
                        print "Disconnect from unknown $ip\n" if ($debug == 0) ;
                        preblock($ip, $tid);
                        next;
                }
		if ( /warning: unknown\[(\d+\.\d+.\d+.\d+)\]: SASL LOGIN authentication failed/ ) {
			$ip = $1;
			print "SASL LOGIN authentication failed from $ip\n" if ( $debug == 0) ;
			preblock($ip, $tid);
                        next;
		}

	}

	close MAILLOG;
}

# ## Prepare SQL ##
#

sub SQL_prep {
        my $sql = shift(@_);
        $sth = $dbh->prepare($sql);
}


# ## Test SQL ##
#

sub SQL_test {
        if ( !defined $sth ) {
                die "$DBI::errstr\n";
        }
}


# ## Do SQL command ##
#

sub SQL_exec {
        $sth->execute;
}

## er ip i iptry?
sub i_iptry {
	my $ip 		= shift;

	my @db_svar 	= ();
	
	SQL_prep("SELECT count(ip) FROM iptry WHERE ip=\'$ip\'");
	SQL_test();
	SQL_exec();

	@db_svar = $sth->fetchrow_array;

	return $db_svar[0];
}

## er ip i ipblock
sub i_ipblock {
	my $ip          = shift;

        my @db_svar     = ();

        SQL_prep("SELECT count(ip) FROM ipblock WHERE ip=\'$ip\'");
        SQL_test();
        SQL_exec();
					        
        @db_svar = $sth->fetchrow_array;
        return $db_svar[0];
}
								
## opret i iptry
sub o_iptry {
	my $ip 		= shift;
	my $tid 	= shift;

	SQL_prep("INSERT INTO iptry VALUES (\'$ip\', \'$tid\', 1)");
	SQL_test();
	SQL_exec();
}

## opret i ipblock
sub o_ipblock {
	my $ip 		= shift;
	my $tid 	= shift;

	SQL_prep("INSERT INTO ipblock VALUES (\'$ip\', \'$tid\')");
	SQL_test();
	SQL_exec();
}

## slet ip i ip_try
sub s_iptry {
	my $ip		= shift;

	SQL_prep("DELETE FROM iptry WHERE ip=\'$ip\'");
	SQL_test();
	SQL_exec();
}

##slet ip fra ipblock
sub s_ipblock {
	my $ip		= shift;

	SQL_prep("DELETE FROM ipblock WHERE ip=\'$ip\'");
	SQL_test();
	SQL_exec();
}

## find iptry counter
sub f_iptry_count {
	my $ip		= shift;
	
	my @db_svar;

	SQL_prep("SELECT count FROM iptry WHERE ip=\'$ip\'");
	SQL_test();
	SQL_exec();
	
	@db_svar = $sth->fetchrow_array;
	return $db_svar[0];
}

## tæl try count op
sub u_iptry_count {
	my $ip		= shift;
	my $tid		= shift;

	SQL_prep("UPDATE iptry SET firsttry = \'$tid\', count = count + 1 WHERE ip=\'$ip\'");
	SQL_test();
	SQL_exec();
}

## Find try count
sub l_iptry_count {
	my $ip		= shift;

	my @row		= ();

	SQL_prep("SELECT count FROM iptry WHERE ip = \'$ip\'");
	SQL_test();
	SQL_exec();

	@row = $sth->fetchrow_array;

	return $row[0];
}

## find blocked ældre end 24 timer
sub l_old_ipblock {
	my $blocktime	= shift;

	my @blocklist = ();
	my @row;

	SQL_prep("SELECT ip FROM ipblock WHERE timediff( now(), blocked ) > \'$blocktime:00:00\'");
	SQL_test();
	SQL_exec();

	while ( @row = $sth->fetchrow_array) {
		push @blocklist, $row[0];
	}

	return @blocklist;
}

## find iptry nyere end 20 minutter og count >= 3
sub l_new_ipblock_to_block {
	my $testtid	= shift;

	my @list;
	my @row;

	SQL_prep("SELECT ip FROM iptry WHERE timediff( now(), firsttry) < \'00:$testtid:00\' AND count >= 3");
	SQL_test();
	SQL_exec();

	while (@row = $sth->fetchrow_array) {
		push @list, $row[0];
		print "l_new_ipblock_to_block $row[0]\n" if ($debug > 0);
	}

	return @list;
}

## find ipry ældre end 20 minutter (skal slettes)
sub l_old_iptry {
	my $testtid	= shift;

	my @list	= ();
	my @row;

	SQL_prep("SELECT ip FROM iptry WHERE timediff(now(), firsttry) > \'00:$testtid:00\'");
	SQL_test();
	SQL_exec();

	while (@row = $sth->fetchrow_array) {
		push @list, $row[0];
	}

	return @list;
}

## block eller ej
sub preblock {
	my $ip		= shift;
	my $tid		= shift;

	my $i_try	= 0;
	my $i_block	= 0;
	my $white_ip    = "";
	my $white	= 0;

	# do whitelist
	foreach $white_ip (@whitelist) {
		if ( $ip =~ /^$white_ip/ ) {
			print "$tid\t$ip -> $ip whitelisted\n" if ($debug == 0) ;
			$white = 1;
			last;
		}
	}

	$i_block = i_ipblock($ip);
	$i_try = i_iptry($ip);
	print "I iptry: $i_try\t I ipblock: $i_block\n" if ($debug > 0);
	
	if ($i_block == 0 and $white == 0) {
		if ($i_try == 0) {
			# Hvis 0 isæt i iptry
			o_iptry($ip, $tid);
			print "$tid\t$ip -> iptry\n" if ($debug == 0) ;
		} else {
			# tæl count op
			u_iptry_count($ip, $tid);
			print "$ip try count talt op\n" if ($debug == 0) ;
		}
	}
}

## block og rens iptry
sub blocking {
	my $tid 	= shift;
	my $trytid	= shift;

	my @toBeBlocked = ();
	my @iptableCmd	= ();
	my $count;

	# Find dem der skal blokkes
	@toBeBlocked = l_new_ipblock_to_block($trytid);
	
	foreach (@toBeBlocked) {
		@iptableCmd  = ();
		$count = l_iptry_count($_);
		print "Blokeres: $_ count $count\n" if ($debug == 0);
		## Do blocking
		# @iptableCmd  = ("/usr/sbin/iptables -I INPUT -s $_ -j DROP");
		#
		@iptableCmd  = ("/usr/bin/firewall-cmd --permanent --add-rich-rule=\'rule family=ipv4 source address=$_ reject\'");
		# system(@iptableCmd);# or die "iptables kommando fejlede: @iptableCmd: $?\n";
		system(@iptableCmd);# or die "firewall-cmd kommando fejlede: @iptableCmd: $?\n";
		print "@iptableCmd\n" if ($debug == 0);

		# fjern dem fra iptry
		s_iptry($_);
		print "Slettede $_ fra iptry\n" if ($debug == 0);

		# opret i ipblock
		o_ipblock($_, $tid);

		# Update stat
		updateStat($_);
	}
	if ( scalar @toBeBlocked > 0 ) {
		@iptableCmd  = ("firewall-cmd --reload");
		system(@iptableCmd);# or die "firewall-cmd kommando fejlede: @iptableCmd: $?\n";
		print "@iptableCmd\n" if ($debug == 0);
	}
	
}

sub updateStat {
	my $ip 		= shift;

	my $count	= 0;
	my $c		= 0;
	my @row		= ();

	# Findes adresse allerede i stats?
	SQL_prep("SELECT count FROM ipblock_stats WHERE ipaddresse=\'$ip\'");
	SQL_test();
	SQL_exec();

	while ( @row = $sth->fetchrow_array ) {
		$c++;
		($count) = @row;
	}

	if ( $c == 0 ) {
		SQL_prep("INSERT INTO ipblock_stats VALUES (\'$ip\', 1)");
		SQL_test();
		SQL_exec();
	} else {
		$count++;
		SQL_prep("UPDATE ipblock_stats SET count=$count WHERE ipaddresse=\'$ip\'");
		SQL_test();
		SQL_exec();
	}


}	

## Rens op i gamle blok og gamle iptry
sub cleanup {
	my $testtid	= shift;
	my $blocktid	= shift;
	
	my @oldIptry	= ();
	my @oldIpblock	= ();
	my @iptableCmd  = ();
	my $ip;

	@oldIptry = l_old_iptry($testtid);
	@oldIpblock = l_old_ipblock($blocktid);

	# slet udløbne i iptry
	foreach $ip (@oldIptry) {
		s_iptry($ip);
		print "Slettede $ip fra iptry\n" if ($debug == 0);
	}

	# ryd gamle blokkeringer
	foreach $ip (@oldIpblock) {
		@iptableCmd  = ();
		print "Fjernes fra firewall: $ip\n" if ($debug == 0);

		## unblocking
		# @iptableCmd  = ("/usr/sbin/iptables -D INPUT -s $ip -j DROP");
		@iptableCmd  = ("/usr/bin/firewall-cmd --permanent --remove-rich-rule=\'rule family=ipv4 source address=$ip reject\'");
		system(@iptableCmd); # or die "iptables kommando fejlede: @iptableCmd: $?\n";
		print "@iptableCmd\n" if ($debug == 0);

		# fjerm fra ipblock
		s_ipblock($ip);
		print "$ip fjernet fra ipblock\n" if ($debug > 0);
	}
	if ( scalar @oldIpblock ) {
		@iptableCmd  = ("firewall-cmd --reload");
		system(@iptableCmd); # or die "iptables kommando fejlede: @iptableCmd: $?\n";
	}
}



# ### MAIN LOOP ###


# Forbind til database
$dbh = DBI->connect("dbi:mysql:$firewalldb", "USER", "PASSWD") 
	or die "Kunne ikke forbinde til databasen: $firewalldb\n";

@tid = localtime(time);
$tid_nu = sprintf "%4d-%02d-%02d %02d:%02d:%02d", $tid[5]+1900, $tid[4]+1, $tid[3], $tid[2], $tid[1], $tid[0];

print "\n===== " . scalar localtime . " =====\n";

# Find blokerede adresser i iptables
h_iptables_drop(\@iptables);

# Fix iptables
# Udeladt med firewalld
#
#f_iptables(\@iptables);

# Find "bad guyes"
fallout( $maillog, date_filter($testinterval), $tid_nu );

# udfør blokering
blocking($tid_nu, $tryinterval);

# oprensning
cleanup($tryinterval, $blockinterval);
