From 0506ad84c9773b17a79454857a365222cb33377b Mon Sep 17 00:00:00 2001
From: =?utf8?q?Rapha=C3=ABl=20Gertz?= <git@rapsys.eu>
Date: Sun, 1 Dec 2019 00:03:33 +0100
Subject: [PATCH] Add new_ipv4 constructor that build NetAddr::IP object
 Prevent read of /etc/resolv.conf by gethostbyname in each NetAddr::IP->new
 call Only ban the ip on specified port and protocol Ban full ip on protocol
 with more than 5 port tried Compare length as numeric to have intended proper
 output Cleanup

---
 blacklist | 153 +++++++++++++++++++++++++++++++++++++++++++-----------
 1 file changed, 123 insertions(+), 30 deletions(-)

diff --git a/blacklist b/blacklist
index 6e3f5c6..e950ba0 100755
--- a/blacklist
+++ b/blacklist
@@ -5,13 +5,21 @@ use warnings;
 
 use IPC::System::Simple qw(capturex);
 use Data::Validate::IP qw(is_ipv4 is_ipv6);
-use NetAddr::IP;
+use NetAddr::IP::Util qw(shiftleft inet_4map6 ipv4to6);
+use NetAddr::IP qw(:nofqdn Ones);
 
+#IP v4 hash
 my %ip4s = ();
+#IP v6 hash
 my %ip6s = ();
+#Blacklist v4 array
 my @blrule4s = ();
+#Blacklist v6 array
 my @blrule6s = ();
-my %whitelist = (
+
+#IP whitelist
+#my $iplist = qr/^(?:127\.|::1|2a01:4f8:190:22a6:|5\.9\.143\.173|85\.68\.|81\.67\.|89\.157\.|82\.241\.255\.46)/;
+my %iplist = (
 	ipv4 => [
 		# Localhost
 		'127.0.0.0/8',
@@ -29,78 +37,163 @@ my %whitelist = (
 		'2a01:4f8:191:1405::/64'
 	]
 );
+
+#Create a new NetAddr::IP object without calling slow gethostbyname (load /etc/resolv.conf)
+sub new_ipv4($) {
+	#Extract ip and mask
+	my ($ip, $mask) = split('/', shift);
+	#Build base struct
+	my $self = {
+		#Set mask
+		mask	=> !defined($mask)||$mask==32?Ones:shiftleft(Ones, 32 - $mask),
+		#Mark as ipv4
+		isv6	=> 0,
+		#Generate fake address
+		#XXX: NetAddr::IP expect a faked Socket6 gethostbyname struct
+		#XXX: see /usr/lib64/perl5/vendor_perl/NetAddr/IP/Util.pm +235
+		#addr	=> scalar ($ip, '', AF_INET, 16, NetAddr::IP::Util::inet_4map6(NetAddr::IP::Util::ipv4to6(pack('C4', split('\.', $ip)))))
+		addr	=> inet_4map6(ipv4to6(pack('C4', split('\.', $ip))))
+	};
+	#Return fake NetAddr::IP object
+	return bless $self, 'NetAddr::IP';
+}
+
+#User whitelist
 my @userlist = ('rapsys');
 
-# Extract sshd.service scan
+#Extract sshd.service scan
 #map {
 #	# Extract user and ip
 #	if (/Failed password for (?:invalid user )?(.+) from (.+) port [0-9]+ ssh2/ && grep($_ ne $1, @userlist)) {
 #		# Save ip
 #		my $ip = $2;
 #		# Check if v4 ip and not in whitelist
-#		if (is_ipv4($ip) && not scalar map { my $network = NetAddr::IP->new($_); my $netip = NetAddr::IP->new($ip); unless ($network->contains($netip)) { (); } } @{$whitelist{ipv4}}) {
+#		if (is_ipv4($ip) && not scalar map { my $network = NetAddr::IP->new($_); my $netip = NetAddr::IP->new($ip); unless ($network->contains($netip)) { (); } } @{$iplist{ipv4}}) {
 #			# Add ip in v4 blacklist
 #			$ip4s{$ip}=1;
 #		# Check if v6 ip
-#		} elsif (is_ipv6($ip) && not scalar map { my $network = NetAddr::IP->new($_); my $netip = NetAddr::IP->new($ip); unless ($network->contains($netip)) { (); } } @{$whitelist{ipv6}}) {
+#		} elsif (is_ipv6($ip) && not scalar map { my $network = NetAddr::IP->new($_); my $netip = NetAddr::IP->new($ip); unless ($network->contains($netip)) { (); } } @{$iplist{ipv6}}) {
 #			$ip6s{$ip}=1;
 #		}
 #	}
 #} capturex('journalctl', '-u', 'sshd.service');
-#
-# Extract kernel port scan
+
+#Extract kernel port scan
 map {
-	if (/kernel: net-fw DROP .* SRC=([^\s]+) DST=.*/) {
+	#oct. 04 19:10:30 aurae.aoihime.eu kernel: net-fw DROP IN=enp3s0 OUT= MAC=50:46:5d:a1:a1:85:0c:86:10:f5:c6:4b:08:00 SRC=61.227.52.153 DST=144.76.27.210 LEN=52 TOS=0x00 PREC=0x00 TTL=116 ID=29123 DF PROTO=TCP SPT=64349 DPT=445 WINDOW=8192 RES=0x00 SYN URGP=0
+	if (/kernel: net-fw DROP .* SRC=([^\s]+) .* PROTO=([^\s]+) .* DPT=([^\s]+)/) {
 		# Save ip
 		my $ip = $1;
+		# Save proto
+		my $proto = lc($2);
+		# Save dpt
+		my $dpt = $3;
 		# Check if v4 ip and not in whitelist
-		if (is_ipv4($ip) && not scalar map { my $network = NetAddr::IP->new($_); my $netip = NetAddr::IP->new($ip); unless ($network->contains($netip)) { (); } } @{$whitelist{ipv4}}) {
-			$ip4s{$ip}=1;
-		} elsif (is_ipv6($ip) && not scalar map { my $network = NetAddr::IP->new($_); my $netip = NetAddr::IP->new($ip); unless ($network->contains($netip)) { (); } } @{$whitelist{ipv6}}) {
-			$ip6s{$ip}=1;
+		if (is_ipv4($ip) && not scalar map { my $network = new_ipv4($_); my $netip = new_ipv4($ip); unless ($network->contains($netip)) { (); } } @{$iplist{ipv4}}) {
+			if (!defined $ip4s{$ip}) {
+				%{$ip4s{$ip}} = ('tcp' => {}, 'udp' => {});
+			}
+			$ip4s{$ip}{$proto}{$dpt}=1;
+		} elsif (is_ipv6($ip) && not scalar map { my $network = NetAddr::IP->new($_); my $netip = NetAddr::IP->new($ip); unless ($network->contains($netip)) { (); } } @{$iplist{ipv6}}) {
+			if (!defined $ip6s{$ip}) {
+				%{$ip6s{$ip}} = ('tcp' => {}, 'udp' => {});
+			}
+			$ip6s{$ip}{$proto}{$dpt}=1;
 		}
-	} elsif (/op=PAM:authentication grantors=\? acct="(.+)" exe="\/usr\/(?:libexec\/dovecot\/auth|sbin\/sshd)" hostname=.+ addr=(.+) terminal=(?:dovecot|ssh) res=failed/ && grep($_ ne $1, @userlist)) {
+		print $ip."\n";
+	#oct. 04 19:17:10 aurae.aoihime.eu kernel: audit: type=1100 audit(1570209430.543:17321294): pid=5890 uid=0 auid=4294967295 ses=4294967295 msg='op=PAM:authentication grantors=? acct="root" exe="/usr/sbin/sshd" hostname=195.154.112.70 addr=195.154.112.70 terminal=ssh res=failed'
+	} elsif (/op=PAM:authentication grantors=\? acct="(.+)" exe="\/usr\/(?:libexec\/dovecot\/auth|sbin\/sshd)" hostname=.+ addr=(.+) terminal=(dovecot|ssh) res=failed/ && grep($_ ne $1, @userlist)) {
 		# Save ip
 		my $ip = $2;
+		# Save proto
+		my $proto = 'tcp';
+		# Save dpt
+		my $dpt = $3 eq 'ssh' ? 22 : 445;
 		# Check if v4 ip and not in whitelist
-		if (is_ipv4($ip) && not scalar map { my $network = NetAddr::IP->new($_); my $netip = NetAddr::IP->new($ip); unless ($network->contains($netip)) { (); } } @{$whitelist{ipv4}}) {
+		if (is_ipv4($ip) && not scalar map { my $network = new_ipv4($_); my $netip = new_ipv4($ip); unless ($network->contains($netip)) { (); } } @{$iplist{ipv4}}) {
+			if (!defined $ip4s{$ip}) {
+				%{$ip4s{$ip}} = ('tcp' => {}, 'udp' => {});
+			}
 			# Add ip in v4 blacklist
-			$ip4s{$ip}=1;
+			$ip4s{$ip}{$proto}{$dpt}=1;
 		# Check if v6 ip
-		} elsif (is_ipv6($ip) && not scalar map { my $network = NetAddr::IP->new($_); my $netip = NetAddr::IP->new($ip); unless ($network->contains($netip)) { (); } } @{$whitelist{ipv6}}) {
-			$ip6s{$ip}=1;
+		} elsif (is_ipv6($ip) && not scalar map { my $network = NetAddr::IP->new($_); my $netip = NetAddr::IP->new($ip); unless ($network->contains($netip)) { (); } } @{$iplist{ipv6}}) {
+			if (!defined $ip6s{$ip}) {
+				%{$ip6s{$ip}} = ('tcp' => {}, 'udp' => {});
+			}
+			$ip6s{$ip}{$proto}{$dpt}=1;
+		}
+	#nov. 30 15:30:07 aurae.aoihime.eu kernel: audit: type=1100 audit(1575124207.371:38129): pid=685 uid=985 auid=4294967295 ses=4294967295 msg='op=PAM:authentication grantors=? acct="toto" exe="/usr/bin/pwauth" hostname=? addr=? terminal=? res=failed'
+	#XXX: Until mod_authnz pass to pwauth the (SERVER_NAME|SERVER_ADDR) + REMOTE_ADDR+REMOTE_PORT in env it's impossible to know who did a failed auth
+	#XXX: see https://github.com/phokz/mod-auth-external/blob/master/mod_authnz_external/TODO
+	#} elsif (/op=PAM:authentication grantors=\? acct="(.+)" exe="\/usr\/bin\/pwauth" hostname=.+ addr=(.+) terminal=\? res=failed/ && grep($_ ne $1, @userlist)) {
+	#	...
+	}
+} capturex('journalctl', '-m', '-t', 'kernel', '-o', 'cat', '--no-hostname');
+
+#Process each ipv4s keys
+map {
+	#Set proto as either tcp or udp
+	for my $proto (('tcp', 'udp')) {
+		#Check if branch is empty
+		if (!scalar keys %{$ip4s{$_}{$proto}}) {
+			#Prune it
+			delete $ip4s{$_}{$proto};
 		}
 	}
-} capturex('journalctl', '-m', '-t', 'kernel');
+} keys %ip4s;
 
-# Open blrule4s file for reading
+#Process each ipv6s keys
+map {
+	#Set proto as either tcp or udp
+	for my $proto (('tcp', 'udp')) {
+		#Check if branch is empty
+		if (!scalar keys %{$ip6s{$_}{$proto}}) {
+			#Prune it
+			delete $ip6s{$_}{$proto};
+		}
+	}
+} keys %ip6s;
+
+#Open blrule4s file for reading
 open (my $fh, '<', '/etc/shorewall/blrules') or die "Can't open < /etc/shorewall/blrules: $!";
 
-# Populate with comments
+#Populate with comments
 @blrule4s = map { chomp($_); if (/^#/) { $_; } else { (); } } <$fh>;
 
-# Prepend each specific ip from whitelist
-map { push @blrule4s, "WHITELIST\tnet:$1\tall" if (/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/32$/); } @{$whitelist{ipv4}};
+#Prepend each specific ip from whitelist
+map { push @blrule4s, "WHITELIST\tnet:$1\tall" if (/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/32$/); } @{$iplist{ipv4}};
 
-# Build blacklist
-map { push @blrule4s, "DROP\t\tnet:".$_.(length lt 12?"\t":'')."\tfw"; } sort keys %ip4s;
 
-# Close blrule4s file
+#Build blacklist
+map {
+	#Set proto from hash
+	for my $proto (keys %{$ip4s{$_}}) {
+		#Push rule
+		push @blrule4s, "DROP\t\tnet:".$_.(length($_)<12?"\t":'')."\tfw\t$proto\t".(scalar keys %{$ip4s{$_}{$proto}}>5||defined $ip4s{$_}{$proto}{0}?'#':'').join(",", keys %{$ip4s{$_}{$proto}});
+	}
+} sort keys %ip4s;
+
+#Close blrule4s file
 close $fh or die "Can't close fh: $!";
 
-# Open blrule4s file for writing
+#Open blrule4s file for writing
 open ($fh, '>', '/etc/shorewall/blrules') or die "Can't open > /etc/shorewall/blrules: $!";
 
-# Inject content of blacklist
+#Inject content of blacklist
 map { print $fh $_."\n"; } @blrule4s;
 
-# Close blrule4s file
+#Close blrule4s file
 close $fh or die "Can't close fh: $!";
 
-# Print ipv6 to update hash
+#Print ipv6 to update hash
 #XXX; right now it don't seems scanned at all...
 for (sort keys %ip6s) {
-	print $_."\n";
+	#Set proto from hash
+	for my $proto (keys %{$ip6s{$_}}) {
+		#Print the ipv6 scanner
+		print $_."\t$proto\t".join(",", keys %{$ip6s{$_}{$proto}})."\n";
+	}
 }
 
 # Restart shorewall service
-- 
2.41.3