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_FILE
And 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 } }