![]()
This script is a part of the Asbra Firewall Project which is a set of utilities for managing a Linux Netfilter Firewall.
Notice that there is 15,754,925 (Fifteen million, seven hundred and fifty-four thousand and nine hundred and twenty-five) IP-addresses in the blacklist array. They are all loaded into memory for quick access.
Since all netfilter logging is done via a named pipe there is no IO access involved in parsing the traffic, block logging is optional and can handle both logging to syslog and custom output.
Evil ports
Evil ports can be specified for both TCP and UDP, combined with a max hit counter you can block unwanted traffic to illegal ports.
Ulogd2 is a logging daemon written for Netfliter, the script utilizes the JSON plugin in combination with the jq utility for collecting and analyzing data. One reason for this is that it offers more info on the network packets and more accurate field extraction with jq than with Bash Regexp.
Read more about how to setup Ulogd2 for JSON.
The script will also fetch 3 different blacklists and merge them into one. This can be changed in the global config /etc/asbrafw-blacklist.conf
- https://rules.emergingthreats.net/fwrules/emerging-Block-IPs.txt
- https://iplists.firehol.org/files/firehol_level1.netset
- https://myip.ms/files/blacklist/csf/latest_blacklist.txt
# apt update && apt install ulogd2 ulogd2-json jq wget grepcidr
#!/bin/bash
#
# AsbraFW - ulog_blacklist.sh
# This script is a part of the Asbra Firewall Project
#
LOG_PIPE="/afw/ulog/ulogd.json.pipe"
LOG="false"
LOG_FILE="/var/log/asbrafw-blacklist.log"
SYSLOG="true"
SILENT="false"
# Blacklisting on first seen event
BLACKLIST_URLS="\
https://rules.emergingthreats.net/fwrules/emerging-Block-IPs.txt\
https://iplists.firehol.org/files/firehol_level1.netset\
https://myip.ms/files/blacklist/csf/latest_blacklist.txt"
BLACKLIST_FILE="/tmp/blacklist.txt"
BLACKLIST_CMD="nft add element ip NAT BLACK"
BLACKLIST_TIMEOUT="24h"
# Evil Ports setting uses the BLOCKLIST_* values
EVIL_PORTS="true"
EVIL_PORTS_MAX_HIT=5
EVIL_TCP_PORTS="20 21 22 222 2222 23 81 445 514 587 3128 3306 8080 8081 8181 31 1170 1234 1243 1981 2001 2023 2989 3024 3150 3700 4950 6346 6400 6667 6670 12345 12346 16660 20034 20432 27374 27665 30100 31337 33270 33567 33568 40421 60008 65000"
EVIL_UDP_PORTS="2140 18753 20433 27444 31335"
BLOCKLIST_CMD="nft add element ip NAT BLOCK"
BLOCKLIST_TIMEOUT="1h"
VERBOSE="false"
# We can use ip addresses as key if the array is declared with -A
declare -A IP_HITS
bluebg="\e[44;1;37m"
yellow="\e[1;33m"
white="\e[1;37m"
reset="\e[0m"
##
# Check /etc for a config file
#
[[ -f "/etc/asbrafw-blacklist.conf" ]] && source /etc/asbrafw-blacklist.conf
##
# Usage instructions
#
function help() {
cat <<- END
Syntax: $0
-f Fetch & merge blacklists
-q Quiet/Silent, no output
-t <ports> Evil TCP Ports (Blocking timeout is applied)
-u <ports> Evil UDP Ports
-v Verbose output, all traffic
-p <pipe> Name of pipe to read log from
-l <logfile> Name of file to write log messages tog
-s Log to syslog
END
exit 0;
}
##
# Print message and die
#
function die() { echo "$1" ; exit 1; }
##
# Process command line parameters
#
while getopts "hfqvst:u:l:p:" opt
do
case $opt in
h) help ;;
f) UPDATE="true" ;; # Fetch update
q) SILENT="true" ;; # No output to STDOUT or STDERR
v) VERBOSE="true" ;; # Show all traffic
s) SYSLOG="true" ;; # Write messages to syslog
t) EVIL_TCP_PORTS="true" # Check for evil ports
[[ ! -z "$optarg" ]] && EVIL_TCP_PORTS=$optarg
;;
u) EVIL_UDP_PORTS="true"
[[ ! -z "$optarg" ]] && EVIL_UDP_PORTS=$optarg
;;
l) LOG="true"
if [[ -z "$OPTARG" ]] ; then
LOG_FILE="/var/log/asbrafw-blacklist.log"
echo "No logfile given, using: $LOG_FILE"
else
LOG_FILE="$OPTARG"
fi
;;
p) [[ -z "$OPTARG" ]] && die "Missing filename for pipe."
LOG_PIPE="$OPTARG"
;;
esac
done
##
# Fetch the latest blacklists
#
function update_blacklist()
{
mv $BLACKLIST_FILE ${BLACKLIST_FILE}.bak
for url in $BLACKLIST_URLS
do
rm /tmp/$(basename $url)
wget --directory-prefix=/tmp/ $url
cat /tmp/$(basename $url) >> $BLACKLIST_FILE
done
cat $BLACKLIST_FILE | sort | uniq > ${BLACKLIST_FILE}.sorted
mv ${BLACKLIST_FILE}.sorted ${BLACKLIST_FILE}
}
##
# Look for match in blocklist array
#
function ip_is_evil()
{
if grepcidr "$EVIL_IPS" <(echo "$1") >/dev/null
then return 0 # True
else return 1 # False
fi
}
##
# Count total number of ips in blacklist
#
function blacklist_counter()
{
local IP_LIST="$1"
local IP_COUNTER=0
local IP_ROWS=0
while IPS='' read -r line ; do
case $line in
*/30) addr=2 ;;
*/29) addr=6 ;;
*/28) addr=14 ;;
*/27) addr=30 ;;
*/26) addr=62 ;;
*/25) addr=126 ;;
*/24) addr=254 ;;
*/23) addr=510 ;;
*/22) addr=1022 ;;
*/21) addr=2046 ;;
*/20) addr=4094 ;;
*/19) addr=8190 ;;
*/18) addr=16382 ;;
*/17) addr=32766 ;;
*/16) addr=65534 ;;
*) addr=1 ;;
esac
IP_COUNTER=$(($IP_COUNTER+$addr))
IP_ROWS=$(($IP_ROWS+1))
done <<< $IP_LIST
printf "${yellow}Evil hosts: ${reset}%'d IP Addresses in %'d rows...\n" "$IP_COUNTER" "$IP_ROWS"
}
##
# Have we seen ip before?
#
function hit_counter()
{
ip=$1
if [[ ! -z "${IP_HITS[$ip]}" ]]
then IP_HITS[$ip]=$(( ${IP_HITS[$ip]}+1 )) # Increment
else IP_HITS[$ip]=1 # First hit
fi
}
##
# Parse JSON from the UlogD Pipe
#
function parse()
{
[[ -z "$1" ]] && { echo "parse() missing named pipe as argument 1"; exit 1; }
local pipe="$1"
local logfile="$2"
# Read Pipe Loop
while read line <$pipe
do
# Split JSON string from pipe into local variables, protocol has to be quoted because of the period.
json_vals=$(echo $line | jq -r '.src_ip, .src_port, .dest_ip, .dest_port, ."ip.protocol"')
read -r _src_ip _src_port _dest_ip _dest_port _ip_protocol < <( echo $json_vals )
# Give IP protocol a name
case "$_ip_protocol" in
1) _ip_protocol="icmp" ;;
6) _ip_protocol="tcp" ;;
17) _ip_protocol="udp" ;;
esac
# Declare vars
_status="" ;_block="false" ; _black="false" ; _color="\e[0m"
# is IP Address Evil
if ip_is_evil $_src_ip ; then
_status="${_status}Blacklisted host! "
_color="\e[31m"
_black="true"
# Check config if we should look for evil ports
elif [[ $EVIL_PORTS == "true" ]] ; then
# TCP Protocol
if [[ $_ip_protocol == "tcp" ]] ; then
# Loop evil TCP ports
for PORT in $EVIL_TCP_PORTS
do
if [[ $_dest_port == $PORT ]]
then
hit_counter $_src_ip
_color="\e[33m"
_status="${_status}Evil TCP port, hit: ${IP_HITS[$_src_ip]} of $EVIL_PORTS_MAX_HIT"
if [[ ${IP_HITS[$_src_ip]} -ge $EVIL_PORTS_MAX_HIT ]]
then
_color="\e[31m"
_block="true"
_status="Evil TCP max hit count, blocking for: $BLOCKLIST_TIMEOUT"
fi
fi
done
# Loop evil UDP ports
elif [[ $_ip_protocol == "udp" ]] ; then
# Loop evil UDP ports
for PORT in $EVIL_UDP_PORTS
do
if [[ $_dest_port == $PORT ]]
then
hit_counter $_src_ip
_color="\e[33m"
_status="${_status}Evil UDP port, hit: ${IP_HITS[$_src_ip]} of $EVIL_PORTS_MAX_HIT"
if [[ ${IP_HITS[$_src_ip]} -ge $EVIL_PORTS_MAX_HIT ]]
then
_color="\e[31m"
_block="true"
_status="Evil UDP max hit count, blocking for: $BLOCKLIST_TIMEOUT"
fi
fi
done
fi # TCP or UDP
fi # Check for evil ports
if [[ -z $_status && "$VERBOSE" == "true" ]] ; then
_color="\e[0m"
_status="Pass"
fi
#SERVICE=$(getent services $_dest_port | cut -f1 -d" ")
#[[ -n $SERVICE ]] && _status="${_status}(${SERVICE}) "
# Block or blacklist the IP
if [[ $_black == "true" ]] ; then $BLACKLIST_CMD "{ $_src_ip timeout $BLACKLIST_TIMEOUT }"
elif [[ $_block == "true" ]] ; then $BLOCKLIST_CMD "{ $_src_ip timeout $BLOCKLIST_TIMEOUT }"
fi
if [[ ! -z $_status ]] ; then
# Date string
DATE=$(date +"%Y-%m-%d %H:%M")
# Print on screen if not silent
[[ $SILENT != "true" ]] && \
printf "${_color}%-16s | %-15s | %-6s | %-15s | %-6s | %-4s | %-s \e[0m\n" "$DATE" "$_src_ip" "$_src_port" "$_dest_ip" "$_dest_port" "$_ip_protocol" "$_status"
# Write to log
[[ $LOG == "true" && -n $logfile ]] && echo "$_src_ip - $_src_port - $_dest_ip - $_dest_port - $_status" >> $logfile
[[ $SYSLOG == "true" ]] && logger --id=$$ "[AsbraFW] ${_src_ip}:${_src_port} ${_dest_ip}:${_dest_port} - $_status"
fi
_status=''
done
}
# Update blacklist
[[ $UPDATE == "true" ]] && update_blacklist
EVIL_IPS=$(grep -v '^$\|^\s*\#' $BLACKLIST_FILE)
if [[ -z "$EVIL_IPS" ]]
then
die "Blacklist seems empty!"
fi
##
# Be verbose if not silent
#
if [[ "$SILENT" == "false" ]]
then
# Title
echo -e "\n\e[31mAsbraFW\e[0m - Blacklist\n"
echo -e "${yellow}Verbose: ${reset}${VERBOSE}"
echo -e "${yellow}Syslog: ${reset}${SYSLOG}"
# Log file
[[ $LOG == "true" ]] && echo -e "${yellow}Log File: ${reset}$LOG_FILE"
# Pipe to read from
echo -e "${yellow}JSON Pipe: ${reset}$LOG_PIPE"
# Name of blacklist file
echo -e "${yellow}Blacklist: ${reset}$BLACKLIST_FILE"
# Blacklist timeout
echo -e "${yellow}Timeout: ${reset}$BLACKLIST_TIMEOUT for blacklisting, $BLOCKLIST_TIMEOUT for blocking"
# Evil ports to block
[[ $EVIL_PORTS == "true" ]] && echo -e "${yellow}Evil ports: ${reset}$EVIL_TCP_PORTS"
# Show statistics, number och ips in blacklist array
blacklist_counter "$EVIL_IPS"
printf "\n\e[1;37;46m%-16s %-15s %-6s %-15s %-6s %-4s %-s \e[K\e[0m\n" "Timestamp" "Src IP" "SPort" "DstIP" "DPort" "Prot" "Status"
fi
# Parse traffic and block matches
parse $LOG_PIPE $LOG_FILEAnd a simple firewall could look something like this.
#!/usr/sbin/nft
flush ruleset
define NIC_WAN = eno1
define NIC_LAN = eno2
define NIC_VPN = tun1
table arp FW {
# Accept ARP requests and replys
chain IN { type filter hook input priority 0; policy drop;
arp operation { request, reply } counter accept
}
# Accept ARP requests and replys
chain OUT { type filter hook output priority 0; policy drop;
arp operation { request, reply } counter accept
}
# Dont forward ARP
chain FORW { type filter hook forward priority 0; policy drop; }
}
table ip NAT {
# Whitelisted trusted ip-addresses
set WHITE { type ipv4_addr;
elements = { 192.168.1.160, 192.168.10.10 }
}
# Blacklist, timeout 1 day
set BLACK { type ipv4_addr; timeout 1d; }
# Blocked for 4 hour
set BLOCK { type ipv4_addr; timeout 4h; }
chain PREROUTING { type nat hook prerouting priority -150;
ip saddr @WHITE counter accept
ip saddr @BLACK counter drop
ip saddr @BLOCK counter drop
}
}
table inet FW {
chain IN { type filter hook input priority 0; policy drop;
# Configure ulogd for JSON output on group 1
iif $NIC_WAN ip protocol tcp ct state new tcp flags syn log prefix "[AsbraFW] TCP new in." group 1 counter counter
iif $NIC_WAN ip protocol udp log prefix "[AsbraFW] UDP new in." group 1 counter counter
# if the connection is NEW and is not SYN then drop
tcp flags != syn ct state new log prefix "[AsbraFW] First packet is not SYN." group 0 counter drop
# new and sending FIN the connection? DROP!
tcp flags & (fin|syn) == (fin|syn) log prefix "[AsbraFW] Incompatible Flags." group 0 counter drop
# i don't think we've met but you're sending a reset?
tcp flags & (syn|rst) == (syn|rst) log prefix "[AsbraFW] Uninitiated Request." group 0 counter drop
# 0 attack?
tcp flags & (fin|syn|rst|psh|ack|urg) < (fin) log prefix "[AsbraFW] 0 Attack." group 0 counter drop
# xmas attack. lights up everything
tcp flags & (fin|syn|rst|psh|ack|urg) == (fin|psh|urg) log prefix "[AsbraFW] Xmas Attack." group 0 counter drop
# if the ctstate is invalid
#ct state invalid flags all log prefix "[AsbraFW] Invalid conntrack state" group 0 counter drop
# Accept all from LAN, VPN and LO interface
iif $NIC_LAN counter accept
iif $NIC_VPN counter accept
iif lo accept
# accept traffic originated from us
ct state established,related counter accept
# accept local services
tcp dport { 80, 443 } ct state new counter accept
tcp dport 53 ct state new counter accept
udp dport 53 ct state new counter accept
}
}
