pfbadhost-fork/pf-badhost.sh
Jez Caudle 5522c8b300 Removed all references to Hail Mary.
Started removing compatability with everything not OpenBSD.
2025-07-01 13:38:58 +00:00

1567 lines
55 KiB
Bash

#!/usr/bin/env ksh
#
# Copyright (c) 2018-2021 Jordan Geoghegan <jordan@geoghegan.ca>
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
# In loving memory of Ron Sather
# Copyright (c) Jez Caudle 2024 onwards
# The original version worked across different BSDs. This is only tested on OpenBSD. All the comparability code has been removed as I don't have the resources to test anywhere else.
# This script downloads some of the most popular IP Blocklists, but you can add
# any lists you like.
# IPv4 or IPv6 lists containing individual addresses or CIDR blocks are supported.
# The address parser also supports mixed lists.
# Scroll down to to the "User Configuration Area" - there you can configure:
# IPv6, Authlog Analysis, GeoIP/Country Blacklisting, Bogon Filtering,
# Tor filtering as well as configure custom rules and blocklists
# IPv6 Notes: THIS IS NOT TESTED IN THIS FORK AS I DON'T HAVE ACCESS TO IPv6
# pf-badhost requires IPv6 lists to be preformatted to be RFC-5952 compliant.
# Example preprocessors have been written for the default list providers included in this script.
# If adding your own IPv6 lists, the addresses must be RFC 5952 compliant and
# have one address per line with no leading or trailing whitespace.
version='0.6'
release_date='2024-02-01'
release_name='Alice'
set -ef #-o pipefail
# ###########################################################################
# ------------------------------------------------------------------------------
# User Configuration Area -- BEGIN
# ------------------------------------------------------------------------------
# Configure additional lists as you see fit
# Custom IPv4 lists should "Just Work"
# Custom IPv6 lists may require preprocessing due to
# strict/difficult regex matching and validation logic
# Set to '1' to enable
# Set to '0' to disable
# This is only tested with OpenBSD and 'ksh'
# HTTP user agent override (Pretend to be Firefox on Win10 by default)
_AGENT="Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0"
# Enable Logging to /var/log/pf-badhost/
_LOG=1
# Enable Strict Mode
# (This option tells pf-badhost to abort if it exceeds maximum retrys)
_STRICT=1
# Max Download Attempts (How many times we'll attempt to download a file before giving up)
_RETRY=3
# Enable IPv4
_IPV4=1
# Enable IPv6
_IPV6=0
# Enable Subnet Aggregation
_AGGREGATE=0
# Enable Geoblocking / Country Blacklisting
_GEOBLOCK=0
# Enable IPv4 Bogon Filter (Blocks unassigned/reserved/martian addresses)
_BOGON_4=0
# Enable IPv6 Bogon Filter (Blocks unassigned/reserved/martian addresses)
_BOGON_6=0
###################################################################
# Cloud Bruteforcer Mitigation (SSH authlog analysis)
# Searches SSH authlog for bruteforcers
#
# Set to '1' to enable
_CLOUD_BRUTEFORCE_MITIGATION=0
#
# Set failed log-in limit for bans
_LOGIN_LIMIT=25
###################################################################
###################################################################
# Country GeoIP Blacklist
# Enter any ISO-3166 Country Codes you want to block (1 per line)
# Lines below starting with '#' or ';' will be ignored
_COUNTRY_CODES=$(cat <<'__EOT'
# CN
# IR
# KP
__EOT
)
###################################################################
###################################################################
# ASN Filtering
# Enter any network ASN you want to block (1 per line)
# Lines below starting with '#' or ';' will be ignored
_ASN_LIST=$(cat <<'__EOT'
# AS64496
__EOT
)
###################################################################
###################################################################
# Block Lists
# Enter URL to any IP blocklist
# IPv4 Supports arbitrary list formating including: JSON, XML, CSV, HTML
# IPv6 Requires preformatted lists (1 address per line)
# Lines below starting with '#' or ';' will be ignored
# Lists may optionally be gzip compressed
#---
# NOTE: DO NOT put quotes in here, as there is a bug in most pdksh
# (including default shells of NetBSD and OpenBSD) that makes the
# shell puke when quotes are used within a HEREDOC statement as below
# See: https://marc.info/?l=openbsd-misc&m=160560808529209&w=2
_BLOCKLISTS=$(cat <<'__EOT'
### Local File Example
# file:/path/to/local/file
### Download popular IPv4 blocklists
https://www.binarydefense.com/banlist.txt
https://rules.emergingthreats.net/blockrules/compromised-ips.txt
https://rules.emergingthreats.net/fwrules/emerging-Block-IPs.txt
https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_level1.netset
https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_level2.netset
### Firehol level 3 can be a little aggressive.
### Ill leave it up to users to choose to enable.
# https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_level3.netset
### Spamhause DROP lists (Dont Route Or Peer)
https://www.spamhaus.org/drop/drop.txt
https://www.spamhaus.org/drop/edrop.txt
https://www.spamhaus.org/drop/dropv6.txt
### Block Shodan
https://isc.sans.edu/api/threatlist/shodan/?text
### Block botnets + command and control servers
https://feodotracker.abuse.ch/downloads/ipblocklist.txt
https://sslbl.abuse.ch/blacklist/sslipblacklist.txt
### Optional lists -- uncomment to enable
### Block IPv4 Martians
# https://www.team-cymru.org/Services/Bogons/bogon-bn-agg.txt
### Ozgur Kazancci Community Block List
### This is a list of IPs that Ozgur has found
### to have been missed by the pf-badhost default lists
# https://ozgur.kazancci.com/ban-me.txt
### StopForumSpam.com Toxic IP Ranges
### Download Rate limited to 24/day, so disabled by default
# https://www.stopforumspam.com/downloads/toxic_ip_cidr.txt
# https://www.stopforumspam.com/downloads/listed_ip_1_ipv46.gz
### Blocklist.de - uncomment to enable
### Combined list of all blocklist.de lists
# https://lists.blocklist.de/lists/all.txt
### SSH attackers
# https://lists.blocklist.de/lists/22.txt
# https://lists.blocklist.de/lists/ssh.txt
# https://lists.blocklist.de/lists/bruteforcelogin.txt
### FTP attackers
# https://lists.blocklist.de/lists/21.txt
# https://lists.blocklist.de/lists/ftp.txt
# https://lists.blocklist.de/lists/proftpd.txt
### HTTP/Apache attackers
# https://lists.blocklist.de/lists/80.txt
# https://lists.blocklist.de/lists/443.txt
# https://lists.blocklist.de/lists/apache.txt
### SMTP/E-Mail Attackers
# https://lists.blocklist.de/lists/25.txt
# https://lists.blocklist.de/lists/110.txt
# https://lists.blocklist.de/lists/143.txt
# https://lists.blocklist.de/lists/993.txt
# https://lists.blocklist.de/lists/email.txt
# https://lists.blocklist.de/lists/mail.txt
# https://lists.blocklist.de/lists/imap.txt
# https://lists.blocklist.de/lists/courierimap.txt
# https://lists.blocklist.de/lists/courierpop3.txt
# https://lists.blocklist.de/lists/pop3.txt
# https://lists.blocklist.de/lists/postfix.txt
### VOIP/SIP Attackers
# https://lists.blocklist.de/lists/asterisk.txt
# https://lists.blocklist.de/lists/sip.txt
### IRC / Bots
# https://lists.blocklist.de/lists/ircbot.txt
# https://lists.blocklist.de/lists/bots.txt
__EOT
)
###################################################################
###################################################################
# User Defined Rules: (add or negate addresses and ranges from block list)
# You can add as many rules as you like here
# Lines below starting with '#' or ';' will be ignored
_USER_RULES=$(cat <<'__EOT'
### Examples: (uncomment to enable)
# !169.254.169.254
# !2001:19f0:ffff::1
# !255.255.255.255
# Multicast
# 224.0.0.0/3
### NAT64/DNS64 Discovery
# !192.0.0.170
# !192.0.0.171
### Carrier Grade NAT (RFC 6598) Address Space
# !100.64.0.0/10
### Unique Local IPv6
# !fc00::/7
__EOT
)
###################################################################
###################################################################
# Tor Filtering
#
# Please be aware that Tor whitelisting/blacklisting options
# are mutually exclusive - ie enabling multiple Tor options is considered
# an error condition and the script will abort
#
# This will ensure traffic to and from Tor is permitted to pass freely
_TOR_WHITELIST=0
#
# Block Tor
# Think VERY carefully before enabling this, as you will
# inevitably piss off a lot of people
_TOR_BLOCK_ALL=0 # Block ALL tor nodes (exit, relay etc)
_TOR_BLOCK_EXIT=0 # Block Tor exit nodes
###################################################################
###################################################################
# Global Whitelist and List Filtering
# Supports IPv4 and IPv6 addresses with optional CIDR notation
#
# By default pf-badhost does not permit the address ranges specified
# in RFC3330 & RFC5156 unless manually specified as a custom rule.
# To disable this behavior, set the 2 options below to '0'
_RFC3330=1
_RFC5156=1
#
# Manual filtering and whitelisting:
# It usually makes more sense to negate an address using a custom
# rule (specified above) rather than using the whitelist.
#
# The whitelist function can be used to perform arbitrary filtering
# Use at your own risk.
#
_WHITELIST=0 # Set to '1' to enable
WHITELIST() {
# Add as many entries to the whitelist as you like
mygrep -v -e '192\.0\.2\.5' -e '2001:db8::/69'
}
#
###################################################################
###################################################################
# Preprocessor Functions
# Add any IP list formating/preprocessing logic you like here
#
# SpamHause IPv6 Preprocessor
EXAMPLE_PROC() {
awk '{print $1}'
}
###################################################################
###################################################################
# Custom Lists
#
# Add any lists here that require preformatting or special treatment
#
CUSTOM_LISTS() {
# Custom List Example:
# URL_FETCH https://www.example.com/example.txt - | EXAMPLE_PROC > "$(TMP_FILE)"
true
}
# ------------------------------------------------------------------------------
# User Configuration Area -- END
# ------------------------------------------------------------------------------
###################################################################
# (Do not edit below this line unless you know what you're doing)
# ------------------------------------------------------------------------------
# Abort Sequences and Housekeeping
# ------------------------------------------------------------------------------
ABORT() {
WARN_ERR "ERROR: '/etc/pf-badhost.txt' contains invalid data! Reverting changes and bailing out..."
OLD_CONF_RESET
TRAPPER
}
CLEANUP() {
rm -rf -- "${listdir}" "${geodir}" "${scratchdir}" "${workdir}" || WARN_ERR 'ERROR: Failed to delete temporary files!'
}
ERR() {
echo '' 1>&2 ; printf 'ERROR: %s\nBailing out without making changes...\n' "$*" | logger -t 'pf-badhost' -s
TRAPPER
}
HELP_MESSAGE() {
printf '\n###################################################################\n'
printf '# pf-badhost %s (%s) Released on: %s\n' "${version}" "${release_name}" "${release_date}"
printf '# Copyright 2018-2021 Jordan Geoghegan <jordan@geoghegan.ca>\n#\n'
printf '# pf-badhost blocks malicious IP addresses using the power of the PF firewall\n#\n'
printf '# The man page can be found at:\n'
printf '# https://geoghegan.ca/pub/pf-badhost/0.5/man/man.txt\n'
printf '###################################################################\n\n'
}
OLD_CONF_RESET() {
cp -- "${oldconf}" /etc/pf-badhost.txt || WARN_ERR 'ERROR: Failed to to restore previous blocklist!'
if ! "${getroot}" -- "${pfctl}" -nf /etc/pf.conf ; then
WARN_ERR 'ERROR: old pf-badhost.txt also has bad data!'
WARN_ERR 'Clearing /etc/pf-badhost.txt and bailing out...'
cp /dev/null /etc/pf-badhost.txt || WARN_ERR "ERROR: Failed to clear '/etc/pf-badhost.txt'"
fi
}
TMP_FILE_ABORT() {
ERR 'Failed to create and/or write to a temporary file! Please ensure that "/tmp" has free space!'
}
TRAP_ABORT() {
ERR "Interupt or uncaught error detected.."
}
TRAPPER() {
CLEANUP ; exit 1
}
WARNING() {
if [ "${_VERBOSE}" -eq 0 ] && [ "${_LOG}" -eq 1 ]; then
WARN_ERR "$*" >/dev/null 2>&1
elif [ "${_VERBOSE}" -eq 1 ] && [ "${_LOG}" -eq 0 ]; then
printf '\n%s\n\n' "$*" 1>&2
elif [ "${_VERBOSE}" -eq 0 ] && [ "${_LOG}" -eq 0 ]; then
true
else
WARN_ERR "$*"
fi
}
WARN_ERR() {
# Force printing and logging of error messages
echo '' 1>&2
logger -t 'pf-badhost' -s -- "$*"
echo '' 1>&2
}
# ------------------------------------------------------------------------------
# Alias functions
# ------------------------------------------------------------------------------
# Opportunistically use mawk or GNU awk if they're available
myawk() {
if command -v mawk >/dev/null 2>&1 ; then
nice mawk "$@" -
elif command -v gawk >/dev/null 2>&1 ; then
nice gawk "$@" -
else
nice awk "$@" -
fi
}
# Users must expicitely set the "netget" var to overide platform default fetch util
# Use '-F' to set fetch util preference from commandline
myfetch() {
typeset _cmd="$(CHECK_CMD "${netget}")"
case "${netget}" in
curl) nice "${_cmd}" -o - -s -A "${_AGENT}" -m 900 -- "$@" ;;
fetch) nice "${_cmd}" -o - -q -- "$@" ;;
ftp) nice "${_cmd}" -o - -V -U "${_AGENT}" -w 30 -- "$@" ;;
wget) nice "${_cmd}" -O - -q -U "${_AGENT}" --timeout=900 -- "$@" ;;
*) ERR "${_cmd} not found!"
esac
}
# Opportunistically use RipGrep or GNU grep if they're available
mygrep() {
if command -v rg >/dev/null 2>&1 ; then
nice rg "$@" - || true
elif command -v ggrep >/dev/null 2>&1 ; then
nice ggrep -E "$@" - || true
else
nice grep -E "$@" - || true
fi
}
# Opportunistically use GNU sort if available (needed for NetBSD support)
mysort() {
if command -v gsort >/dev/null 2>&1 ; then
nice gsort "$@" -
else
nice sort "$@" -
fi
}
# ------------------------------------------------------------------------------
# Authlog Analysis Functions
# ------------------------------------------------------------------------------
# CLOUD_BRUTEFORCE_MITIGATION preproccessor
AUTHLOG_PROC() {
myawk -- '{if ($6 !~ "Disconnected|Accepted" && $7 !~ "disconnect") printf("%s\n%s\n%s\n%s\n%s\n%s\n", $9, $10, $11, $12, $13, $14)}'
}
CLOUD_BRUTEFORCE_MITIGATION_MITIGATE() {
# Check OSTYPE
if [ "${_OS_TYPE}" != 'macos' ]; then
# IPv4 Authlog List Gen
if [ "${_IPV4}" -eq 1 ]; then
"${getroot}" -- "${authlog_unzip}" -f "${authlog_path1}" "${authlog_path2}" | AUTHLOG_PROC | PARSE_V4 | WHITELIST_FILTER | myawk -- '{ a[$0]++ }END{ for(i in a) print a[i],i }' | myawk -v LOGIN_LIMIT="${_LOGIN_LIMIT}" -- '$1>LOGIN_LIMIT {print $2}' | mysort -uV
fi
# IPv6 Authlog List Gen
if [ "${_IPV6}" -eq 1 ]; then
"${getroot}" -- "${authlog_unzip}" -f "${authlog_path1}" "${authlog_path2}" | AUTHLOG_PROC | PARSE_V6 | WHITELIST_FILTER | myawk '{ a[$0]++ }END{ for(i in a) print a[i],i }' | myawk -v LOGIN_LIMIT="${_LOGIN_LIMIT}" -- '$1>LOGIN_LIMIT {print $2}' | mysort -uV
fi
else
echo 'MacOS does not support authlog analysis :(' 1>&2
fi > "${authlog}"
}
# ------------------------------------------------------------------------------
# Geoblock Functions
# ------------------------------------------------------------------------------
GEO_ASN() {
# Parse GeoIP data registered as ASN rather than IP range (prints ASN to feed to _asn_array[])
typeset -u _cc
for _cc in "${_country_code[@]}" ; do
find "${geodir}" -type f -exec cat -- {} + | myawk -v country="${_cc}" -F '|' -- '{if ($2 == country && $3 == "asn") printf("AS%s\n", $4)}'
done | mysort -u
}
GEOBLOCKER() {
typeset -u _cc
# Test if awk includes patch from June 12 2020: [ https://github.com/onetrueawk/awk/pull/80 ]
if [ "${awk_patch}" -eq 1 ]; then
# Generate Country IP CIDR Blocks
for _cc in "${_country_code[@]}" ; do
find "${geodir}" -type f -exec cat -- {} + | myawk -v country="${_cc}" -v IPV4="${_IPV4}" -v IPV6="${_IPV6}" -F '|' -- '{if (IPV4 == 1 && $2 == country && $3 == "ipv4") printf("%s/%d\n", $4, 32-log($5)/log(2))} {if (IPV6 == 1 && $2 == country && $3 == "ipv6") printf("%s/%d\n", $4, $5)}'
done > "$(TMP_FILE)"
else
# [Workaround] Generate Country IP CIDR Blocks
WARNING 'awk does not appear to have June 12 2020 patches installed.'
WARNING 'Using workaround geoblock function...'
for _cc in "${_country_code[@]}" ; do
if [ "${_IPV4}" -eq 1 ]; then
find "${geodir}" -type f -exec cat -- {} + | myawk -v country="${_cc}" -F '|' -- '{if ($2 == country && $3 == "ipv4") print($4, $5)}' | myawk -- '{printf("%s/%d\n", $1, 32-log($2)/log(2))}'
fi
if [ "${_IPV6}" -eq 1 ]; then
find "${geodir}" -type f -exec cat -- {} + | myawk -v country="${_cc}" -F '|' -- '{if ($2 == country && $3 == "ipv6") printf("%s/%d\n", $4, $5)}'
fi
done > "$(TMP_FILE)"
fi
}
# ------------------------------------------------------------------------------
# IP Validator Functions and Input Sanitization
# ------------------------------------------------------------------------------
# Validates IPv4 addresses (can pull addresses from arbitrarily formatted text, thanks to "grep -o"
PARSE_V4() {
# Replace use of grep with ripgrep or GNU grep for a large performance increase
mygrep -o -- '((25[0-5]|(2[0-4]|1{0,1}[[:digit:]]){0,1}[[:digit:]])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[[:digit:]]){0,1}[[:digit:]])(/(3[0-2]|[1-2][[:digit:]]|[1-9]))?'
}
# Validates IPv6 addresses (Addresses must be RFC-5952 Compliant and have one address per line)
# Many IPv6 address lists require a preprocessor to format the lists/addresses correctly for validation
PARSE_V6() {
# Replace use of grep with ripgrep or GNU grep for a large performance increase
SANITIZE_ARRAY_NO_SORT | mygrep -x -- '(([[:xdigit:]]{1,4}:){7,7}[[:xdigit:]]{1,4}|([[:xdigit:]]{1,4}:){1,7}:|([[:xdigit:]]{1,4}:){1,6}:[[:xdigit:]]{1,4}|([[:xdigit:]]{1,4}:){1,5}(:[[:xdigit:]]{1,4}){1,2}|([[:xdigit:]]{1,4}:){1,4}(:[[:xdigit:]]{1,4}){1,3}|([[:xdigit:]]{1,4}:){1,3}(:[[:xdigit:]]{1,4}){1,4}|([[:xdigit:]]{1,4}:){1,2}(:[[:xdigit:]]{1,4}){1,5}|[[:xdigit:]]{1,4}:((:[[:xdigit:]]{1,4}){1,6})|:((:[[:xdigit:]]{1,4}){1,7}|:)|[fF][eE]80:(:[[:xdigit:]]{0,4}){0,4}%[[:alnum:]]{1,}|::([fF]{4}(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[[:digit:]]){0,1}[[:digit:]])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[[:digit:]]){0,1}[[:digit:]])|([[:xdigit:]]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[[:digit:]]){0,1}[[:digit:]])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[[:digit:]]){0,1}[[:digit:]]))(/(12[0-8]|1[0-1][[:digit:]]|[1-9][[:digit:]]{0,1}))?'
}
PRIVATE_ADDRESS_FILTER() {
# Allow all private address ranges in blocklist by default (may break stuff)
if [ "${_RFC3330}" -ne 1 ] && [ "${_RFC5156}" -ne 1 ]; then
# Pipe through cat to avoid wasting cpu cycles on grep
cat
# Allow RFC5156 addresses in blocklist, filter out RFC3330 addresses
elif [ "${_RFC3330}" -eq 1 ] && [ "${_RFC5156}" -ne 1 ]; then
RFC3330_FILTER
# Allow RFC3330 addresses in blocklist, filter out RFC5156 addresses
elif [ "${_RFC3330}" -ne 1 ] && [ "${_RFC5156}" -eq 1 ]; then
RFC5156_FILTER
# Make sure RFC 3330 & 5156 addresses are not in blocklist
else
RFC3330_FILTER | RFC5156_FILTER
fi
}
RFC3330_FILTER() {
mygrep -v '^0\.0\.0\.0/8|^10\.0\.0\.0/8|^14\.0\.0\.0/8|^24\.0\.0\.0/8|^39\.0\.0\.0/8|^127\.0\.0\.0/8|^128\.0\.0\.0/16|^169\.254\.0\.0/16|^172\.16\.0\.0/12|^191\.255\.0\.0/16|^192\.0\.0\.0/24|^192\.0\.2\.0/24|^192\.88\.99\.0/24|^192\.168\.0\.0/16|^198\.18\.0\.0/15|^223\.255\.255\.0/24|^224\.0\.0\.0/4|^224\.0\.0\.0/3|^240\.0\.0\.0/4'
}
RFC5156_FILTER() {
mygrep -vi '^::FFFF:0:0/96|^fe80::/10|^fc00::/7|^2001:db8::/32|^2002::/16|^2001::/32|^5f00::/8|^3ffe::/16|^2001:10::/28|^ff00::/8'
}
SANITIZE_ARRAY() {
mygrep -v -- '^#|^;|^[[:space:]]*#|^[[:space:]]*;|^[[:space:]]*$' | myawk -- '{print $1}' | mysort -u
}
SANITIZE_ARRAY_NO_SORT() {
mygrep -v -- '^#|^;|^[[:space:]]*#|^[[:space:]]*;|^[[:space:]]*$' | myawk -- '{print $1}'
}
SANITIZE_COUNTRY_CODES() {
# Normalize user-provided country codes
printf '%s\n' "${_COUNTRY_CODES}" | tr '[:lower:]' '[:upper:]' | SANITIZE_ARRAY_NO_SORT | mygrep -- '^[[:upper:]]{2}$' | mysort -u
}
# ------------------------------------------------------------------------------
# List Generation Functions
# ------------------------------------------------------------------------------
LIST_GEN() {
# Make sure there are no empty files in listdir
find "${listdir}" -type f -size 0 -delete || WARN_ERR 'ERROR: Failed to delete temporary files!'
# Filter and Generate IP Address List
if [ "${_AGGREGATE}" -eq 1 ]; then
### IPv6 Stuff
if [ "${_IPV6}" -eq 1 ]; then
if [ "${agg6}" -eq 1 ]; then
find "${listdir}" -type f -exec gunzip -dcf -- {} + | PARSE_V6 | SUB_AGG_6 > "${v6list}" || TMP_FILE_ABORT
else
WARNING 'aggregate6 utility not found, unable to aggregate IPv6 addresses...'
readonly IPV6_PROC=1 ; find "${listdir}" -type f -exec gunzip -dcf -- {} + | PARSE_V6 | tr '[:upper:]' '[:lower:]' | mysort -uV > "${v6list}" || TMP_FILE_ABORT
fi
fi
### IPv4 Stuff
if [ "${_IPV4}" -eq 1 ]; then
# Prefer aggy as it's 100 to 1000 times faster than the alternatives
if [ "${go_agg}" -eq 1 ]; then
WARNING 'Using experimental "aggy" aggregator...'
find "${listdir}" -type f -exec gunzip -dcf -- {} + | PARSE_V4 | SUB_AGG_GO > "${v4list}" || TMP_FILE_ABORT
# Prefer the C based 'aggregate' util over the Python based 'aggregate6' util
elif [ "${agg4}" -eq 1 ] && [ "${agg6}" -eq 1 ]; then
find "${listdir}" -type f -exec gunzip -dcf -- {} + | PARSE_V4 | SUB_AGG_C > "${v4list}" || TMP_FILE_ABORT
# aggregate C utility
elif [ "${agg4}" -eq 1 ] && [ "${agg6}" -eq 0 ]; then
find "${listdir}" -type f -exec gunzip -dcf -- {} + | PARSE_V4 | SUB_AGG_C > "${v4list}" || TMP_FILE_ABORT
# Use aggregate6 IPv4 support if C based aggregate util not found
elif [ "${agg4}" -eq 0 ] && [ "${agg6}" -eq 1 ]; then
find "${listdir}" -type f -exec gunzip -dcf -- {} + | PARSE_V4 | SUB_AGG_PY > "${v4list}" || TMP_FILE_ABORT
# If neither aggregate utility is found, use Perl function
elif [ "${agg4}" -eq 0 ] && [ "${agg6}" -eq 0 ] && [ "${perl_exist}" -eq 1 ]; then
WARNING 'Falling back to pure Perl IPv4 aggregator...'
find "${listdir}" -type f -exec gunzip -dcf -- {} + | PARSE_V4 | SUB_AGG_PERL > "${v4list}" || TMP_FILE_ABORT
# If aggy, aggregate{6} or Perl not installed, we can't do subnet aggregation
else
WARNING 'Unable to aggregate subnets! Perl, aggy, aggregate and/or aggregate6 not found!'
NO_AGG
fi
fi
else
NO_AGG
fi
{ PRINT_LIST | PRIVATE_ADDRESS_FILTER | WHITELIST_FILTER ; } > "${finout}" || TMP_FILE_ABORT
}
LIST_INSTALL() {
# Calculate byte offsets for cmp (strip date header)
typeset old_offset="$(head -2 -- /etc/pf-badhost.txt | wc -c)"
typeset new_offset="$(head -2 -- "${finout}" | wc -c)"
# Reload pf-badhost table only if there are blocklist chages
# 'cmp -s' on most platforms has a bug where it ignores byte offsets :(
if cmp -- /etc/pf-badhost.txt "${finout}" "${old_offset}" "${new_offset}" >/dev/null 2>&1; then
printf '\nNo blocklist changes...\n' 1>&2
if [ "${_LOG}" -eq 1 ]; then
{ printf '# Last Run (no changes): %s\n' "$(date)" ; cat -- < '/etc/pf-badhost.txt' ; } > "${oldconf}" || ERR 'Failed to update log file!'
cp -- "${oldconf}" /var/log/pf-badhost/pf-badhost.log || ERR 'Failed to update log file!'
chmod 640 /var/log/pf-badhost/pf-badhost.log >/dev/null 2>&1
fi
else
# Backup old blocklist
cp -- /etc/pf-badhost.txt "${oldconf}" || TMP_FILE_ABORT
# Move newly generated blocklist into place
cp -- "${finout}" /etc/pf-badhost.txt || ERR 'Failed to update /etc/pf-badhost.txt! Please ensure the file has correct permissions and that the partition has free space!'
# Ensure proposed changes are valid before finally reloading pfbadhost table
if "${getroot}" -- "${pfctl}" -nf /etc/pf.conf ; then
"${getroot}" -- "${pfctl}" -t pfbadhost -T replace -f /etc/pf-badhost.txt || ABORT
if [ "${_LOG}" -eq 1 ]; then
LOGGER
fi
else
ABORT
fi
fi
}
PRINT_LIST() {
# Generate pf-badhost.txt from newly processed blocklist data
printf '# Date Created: %s\n' "$(date)"
PRINT_STATS | sed 's/^/# /g'
# User defined rules and address negation
if [ -s "${user_rules}" ]; then
printf '\n# User Defined Rules:\n\n'
cat -- < "${user_rules}"
fi
# Authlog Analysis
if [ "${_CLOUD_BRUTEFORCE_MITIGATION}" -eq 1 ]; then
printf '\n# Rules Generated from %s:\n\n' "$authlog_path1"
cat -- < "${authlog}"
fi
# Tor Filtering
if [ -s "${tor_whitelist}" ]; then
printf '\n# Tor Whitelist:\n\n'
cat -- < "${tor_whitelist}"
elif [ -s "${tor_blacklist}" ]; then
printf '\n# Tor Blacklist:\n\n'
cat -- < "${tor_blacklist}"
fi
# System rules (RFC3330/5156 negation)
printf '\n# System Rules:\n\n'
if [ "${_RFC3330}" -ne 1 ] && [ "${_RFC5156}" -ne 1 ]; then
# Allow all private address ranges in blocklist (may break stuff)
true
elif [ "${_RFC3330}" -eq 1 ] && [ "${_RFC5156}" -ne 1 ]; then
# Allow RFC5156 addresses in blocklist, filter out RFC3330 addresses
printf '%s\n' "${_rfc3330[@]}"
elif [ "${_RFC3330}" -ne 1 ] && [ "${_RFC5156}" -eq 1 ]; then
# Allow RFC3330 addresses in blocklist, filter out RFC5156 addresses
printf '%s\n' "${_rfc5156[@]}"
else
# Make sure RFC 3330 & 5156 address ranges are not in blocklist
printf '%s\n' "${_rfc3330[@]}"
printf '%s\n' "${_rfc5156[@]}"
fi
# Main ruleset
if [ "${_IPV4}" -eq 1 ]; then
printf '\n# IPv4 List Generated Rules:\n\n'
cat -- < "${v4list}"
fi
if [ "${_IPV6}" -eq 1 ]; then
printf '\n# IPv6 List Generated Rules:\n\n'
cat -- < "${v6list}"
fi
}
TOR_FILTER() {
# Grab correct list
if [ "${_TOR_WHITELIST}" -eq 1 ] || [ "${_TOR_BLOCK_ALL}" -eq 1 ]; then
URL_FETCH 'https://github.com/SecOps-Institute/Tor-IP-Addresses/raw/master/tor-nodes.lst' "${tor_rawlist}" || ERR 'Failed to fetch Tor IP list!'
elif [ "${_TOR_BLOCK_EXIT}" -eq 1 ]; then
URL_FETCH 'https://github.com/SecOps-Institute/Tor-IP-Addresses/raw/master/tor-exit-nodes.lst' "${tor_rawlist}" || ERR 'Failed to fetch Tor IP list!'
else
return 1
fi
# Parse Tor list right here to avoid further if/or logic later on
if [ "${_TOR_WHITELIST}" -eq 1 ]; then
# Create whitelist
if [ "${_IPV4}" -eq 1 ]; then
PARSE_V4 < "${tor_rawlist}" | mysort -uV | sed 's/^/!/g'
fi
if [ "${_IPV6}" -eq 1 ]; then
PARSE_V6 < "${tor_rawlist}" | mysort -uV | sed 's/^/!/g'
fi
fi > "${tor_whitelist}"
if [ "${_TOR_BLOCK_ALL}" -eq 1 ] || [ "${_TOR_BLOCK_EXIT}" -eq 1 ]; then
# Create blacklist
if [ "${_IPV4}" -eq 1 ]; then
PARSE_V4 < "${tor_rawlist}" | mysort -uV
fi
if [ "${_IPV6}" -eq 1 ]; then
PARSE_V6 < "${tor_rawlist}" | mysort -uV
fi
fi > "${tor_blacklist}"
}
WHITELIST_FILTER() {
if [ "${_WHITELIST}" -eq 1 ]; then
WHITELIST
else
# Pipe through cat to avoid wasting cpu cycles on grep if whitelisting is disabled
cat
fi
}
# ------------------------------------------------------------------------------
# List Statistics and Totals
# ------------------------------------------------------------------------------
V4_TOTAL() {
# The awk bug I discovered back in June 2020 strikes again!
if [ "${awk_patch}" -eq 1 ]; then
myawk -v v4num="${v4_num}" -F '/' -- '/\/[[:digit:]][[:digit:]]?$/ {sum+= 2^(32 - $2)} END {printf "%0.0f", sum + v4num}' < "${v4list}"
else
mygrep -- '/[[:digit:]]{1,2}$' < "${v4list}" | myawk -F '/' -- '{print 2^(32 - $2)}' | myawk -v v4num="${v4_num}" -- '{sum+=$1} END {printf "%0.0f", sum + v4num}'
fi
}
V6_TOTAL() {
# The awk bug I discovered back in June 2020 strikes again!
if [ "${awk_patch}" -eq 1 ]; then
myawk -v v6num="${v6_num}" -F '/' -- '/\/[[:digit:]][[:digit:]]?[[:digit:]]?$/ {sum+= 2^(128 - $2)} END {printf "%0.0f", sum + v6num}' < "${v6list}"
else
mygrep -- "/[[:digit:]]{1,3}$" < "${v6list}" | myawk -F '/' -- '{print 2^(128 - $2)}' | myawk -v v6num="${v6_num}" -- '{sum+=$1} END {printf "%0.0f", sum + v6num}'
fi
}
# ------------------------------------------------------------------------------
# Logging Functions
# ------------------------------------------------------------------------------
LOGGER() {
# Gzip old log file
gzip -9 -c < /var/log/pf-badhost/pf-badhost.log > "${gztemp}" || ERR 'Failed to rotate log file!'
cp -- "${gztemp}" /var/log/pf-badhost/pf-badhost.log.0.gz || ERR 'Failed to create log file!'
# Move new log into place
cp -- "${finout}" /var/log/pf-badhost/pf-badhost.log || ERR 'Failed to create log file!'
chmod 640 /var/log/pf-badhost/pf-badhost.log /var/log/pf-badhost/pf-badhost.log.0.gz >/dev/null 2>&1
}
PRINT_STATS() {
# Print number of addresses in table (expand CIDR ranges)
typeset authlog_num v4_num v4_total v6_num v6_total
authlog_num="$(wc -l -- < "${authlog}" | tr -cd '[:digit:]')"
if [ "${_CLOUD_BRUTEFORCE_MITIGATION}" -eq 1 ]; then
printf '\nBruteforcers found in "%s": %s\n' "${authlog_path1}" "${authlog_num}"
else
printf '\n'
fi
if [ "${_IPV4}" -eq 1 ]; then
v4_num="$(mygrep -cv -- "/[[:digit:]]{1,2}$" < "${v4list}")"
v4_total="$(V4_TOTAL)"
printf 'IPv4 addresses in table: %s\n' "${v4_total}"
fi
if [ "${_IPV6}" -eq 1 ]; then
v6_num="$(mygrep -cv -- "/[[:digit:]]{1,3}$" < "${v6list}")"
v6_total="$(V6_TOTAL)"
printf 'IPv6 addresses in table: %s\n\n' "${v6_total}"
else
printf '\n\n'
fi
}
# ------------------------------------------------------------------------------
# Subnet Aggregation Functions
# ------------------------------------------------------------------------------
# If no aggregator enabled or found, normalize and sort the address list
NO_AGG() {
# IPv4
if [ "${_IPV4}" -eq 1 ]; then
find "${listdir}" -type f -exec gunzip -dcf -- {} + | PARSE_V4 | mysort -uV > "${v4list}" || TMP_FILE_ABORT
fi
# IPv6
if [ "${_IPV6}" -eq 1 ] && [ -z "${IPV6_PROC}" ]; then
find "${listdir}" -type f -exec gunzip -dcf -- {} + | PARSE_V6 | tr '[:upper:]' '[:lower:]' | mysort -uV > "${v6list}" || TMP_FILE_ABORT
fi
}
# Call "aggregate6" utility to aggregate IPv6 address list.
SUB_AGG_6() {
nice aggregate6 -t -6 | sed 's/\/128$//g'
}
# Call "aggregate" utility to aggregate IPv4 address list.
# Run "pkg_add aggregate" on OpenBSD to install
# Very mature, stable code written in C
SUB_AGG_C() {
myawk -- '!/\/3[0-2]$|\/[1-2][[:digit:]]$|\/[1-9]$/ {$0=$0"/32"}1' | nice aggregate -qt | sed 's/\/32$//g'
}
# Call experimental "aggy" aggregate utility.
# Preliminary testing has shown it to be 100 to 1000 times faster than the alternatives
# See install instructions for info on how to install aggy
SUB_AGG_GO() {
myawk -- '!/\/3[0-2]$|\/[1-2][[:digit:]]$|\/[1-9]$/ {$0=$0"/32"}1' | aggy | sed 's/\/32$//g'
}
# Call "aggregate6" utility to aggregate IPv4 address list.
# Run "pkg_add aggregate6" on OpenBSD to install or "pip3 install aggregate6" on other platforms
SUB_AGG_PY() {
nice aggregate6 -t -4 | sed 's/\/32$//g'
}
# IPv4 CIDR/Address Aggregator [pure Perl version of "aggregate" utility]
_SUBNET_MERGE_PERL=$(cat <<'__EOT'
#!/usr/bin/perl -lp0a
$_=join$\,sort map{1x(s/\d*./unpack B8,chr$&/ge>4?$&:32)&$_}@F;1while s/^(.*)
\1.*/$1/m||s/^(.*)0
\1.$/$1/m;s!^.*!(join'.',map{ord}split'',pack B32,$&).'/'.length$&!gme
__EOT
)
# Pure Perl version of ISC "aggregate" utitity [code stored in above variable "_SUBNET_MERGE_PERL"]
# WARNING: ~10x slower than C based utilty and ~1000x slower than aggy
SUB_AGG_PERL() {
nice perl -e "${_SUBNET_MERGE_PERL}" | sed 's/\/32$//g'
}
# ------------------------------------------------------------------------------
# Temp File Functions
# ------------------------------------------------------------------------------
TMP_FILE() {
mktemp -- "${listdir}/tmp.XXXXXXXX" || TMP_FILE_ABORT
}
TMP_FILE_GEOBLOCK() {
mktemp -- "${geodir}/geo.XXXXXXXX" || TMP_FILE_ABORT
}
TMP_FILE_SCRATCH() {
mktemp -- "${scratchdir}/scratch.XXXXXXXX" || TMP_FILE_ABORT
}
# ------------------------------------------------------------------------------
# Tests and Sanity Checks
# ------------------------------------------------------------------------------
AWK_TEST() {
echo '5e58386636aa775c2106140445' | myawk -- 'END {print log(2)}' 2>&1 | mygrep -c 'log result out of range'
}
CHECK_DRIVE() {
# Make sure /etc/pf-badhost.txt exists
if [ -f /etc/pf-badhost.txt ] && [ -w /etc/pf-badhost.txt ]; then
true
else
ERR '/etc/pf-badhost.txt either not found or has incorrect permissions!'
fi
# If logging is enabled, make sure permissions are correct
if [ "${_LOG}" -eq 1 ]; then
# Make sure log dir exists and has correct permissions
if [ -d /var/log/pf-badhost ] && [ -r /var/log/pf-badhost ]; then
true
else
ERR "Directory '/var/log/pf-badhost' either not found, or has incorrect permissions!"
fi
# Make sure log file is writeable
if [ -f /var/log/pf-badhost/pf-badhost.log ] && [ -w /var/log/pf-badhost/pf-badhost.log ]; then
true
else
ERR "Log file '/var/log/pf-badhost/pf-badhost.log' has incorrect permissions!"
fi
# Make sure gzip file is writeable
if [ -f /var/log/pf-badhost/pf-badhost.log.0.gz ] && [ -w /var/log/pf-badhost/pf-badhost.log.0.gz ]; then
true
else
ERR "Log file '/var/log/pf-badhost/pf-badhost.log.0.gz' has incorrect permissions!"
fi
fi
}
CHECK_CMD() {
typeset _cmd="${1}"
command -v -- "${_cmd}" || ERR "'${_cmd}' not found! Please ensure that '${_cmd}' is installed!"
}
CHECK_PRIVILEGE() {
# Make sure we're running as "_pfbadhost" user
if [ "$(whoami)" != '_pfbadhost' ]; then
printf '\nScript must be run as user "_pfbadhost" - Exiting...\n' 1>&2
exit 1
fi
}
IS_ASN() {
typeset -u _asn
typeset _num _prefix
_asn="${1}"
_num="${_asn#AS}"
# Strip first two characters from '_asn' var
_prefix="${_asn%"${_asn#??}"}"
if [ "${_prefix}" = 'AS' ]; then
IS_INT "${_num}" || return 1
if [ "${_num}" -ge 0 ] && [ "${_num}" -le 4294967295 ]; then
return 0
else
return 1
fi
else
return 1
fi
}
IS_INT() {
case "$1" in
''|*[!0-9]*) return 1 ;;
*) return 0 ;;
esac
}
PRE_EXEC_TESTS() {
typeset _cmd
# Confirm if awk has June 2020 patches
if [ "$(AWK_TEST)" -eq 0 ]; then
awk_patch=1
else
awk_patch=0
fi
# Confirm if aggregate is installed
if command -v aggregate >/dev/null 2>&1; then
agg4=1
else
agg4=0
fi
# Confirm if aggregate6 is installed
if command -v aggregate6 >/dev/null 2>&1; then
agg6=1
else
agg6=0
fi
# Confirm if experimental aggy utility is installed
if command -v aggy >/dev/null 2>&1; then
go_agg=1
else
go_agg=0
fi
# Confirm if Perl is installed
if command -v perl >/dev/null 2>&1; then
perl_exist=1
else
perl_exist=0
fi
# Make sure requisite utilities are installed
for _cmd in 'cmp' 'find' 'gunzip' 'nc' "${netget}" ; do
CHECK_CMD "${_cmd}"
done > /dev/null
if [ "${_NO_UID_CHECK}" -ne 1 ]; then
CHECK_PRIVILEGE
fi
if [ "${_CLOUD_BRUTEFORCE_MITIGATION}" -eq 1 ]; then
CHECK_CMD "${authlog_unzip}" > /dev/null
fi
if [ "${_PRINT_ONLY}" -ne 1 ]; then
pfctl="$(CHECK_CMD pfctl)"
CHECK_DRIVE
fi
# Check for network connectivity to GitHub, bail out if fail
URL_FETCH https://github.com /dev/null || ERR 'No network connectivity!'
}
# Make sure user-provided values are sane
VAR_SANITY_CHECK() {
typeset _cmd
IS_INT "${_AGGREGATE}" || ERR 'User defined variable "$_AGGREGATE" contains a non-integer value - Unable to proceed!'
IS_INT "${_BOGON_4}" || ERR 'User defined variable "$_BOGON_4" contains a non-integer value - Unable to proceed!'
IS_INT "${_BOGON_6}" || ERR 'User defined variable "$_BOGON_6" contains a non-integer value - Unable to proceed!'
IS_INT "${_CHECK_ONLY}" || ERR 'User defined variable "$_CHECK_ONLY" contains a non-integer value - Unable to proceed!'
IS_INT "${_GEOBLOCK}" || ERR 'User defined variable "$_GEOBLOCK" contains a non-integer value - Unable to proceed!'
IS_INT "${_CLOUD_BRUTEFORCE_MITIGATION}" || 'User defined variable "$_CLOUD_BRUTEFORCE_MITIGATION" contains a non-integer value - Unable to proceed!'
IS_INT "${_IPV4}" || 'User defined variable "$_IPV4" contains a non-integer value - Unable to proceed!'
IS_INT "${_IPV6}" || ERR 'User defined variable "$_IPV6" contains a non-integer value - Unable to proceed!'
IS_INT "${_LOG}" || ERR 'User defined variable "$_LOG" contains a non-integer value - Unable to proceed!'
IS_INT "${_LOGIN_LIMIT}" || ERR 'User defined variable "$_LOGIN_LIMIT" contains a non-integer value - Unable to proceed!'
IS_INT "${_NO_UID_CHECK}" || ERR 'User defined variable "$_NO_UID_CHECK" contains a non-integer value - Unable to proceed!'
IS_INT "${_PRINT_ONLY}" || ERR 'User defined variable "$_PRINT_ONLY" contains a non-integer value - Unable to proceed!'
IS_INT "${_RETRY}" || ERR 'User defined variable "$_RETRY" contains a non-integer value - Unable to proceed!'
IS_INT "${_STRICT}" || ERR 'User defined variable "$_STRICT" contains a non-integer value - Unable to proceed!'
IS_INT "${_TOR_BLOCK_ALL}" || ERR 'User defined variable "$_TOR_BLOCK_ALL" contains a non-integer value - Unable to proceed!'
IS_INT "${_TOR_BLOCK_EXIT}" || ERR 'User defined variable "$_TOR_BLOCK_EXIT" contains a non-integer value - Unable to proceed!'
IS_INT "${_TOR_WHITELIST}" || ERR 'User defined variable "$_TOR_WHITELIST" contains a non-integer value - Unable to proceed!'
IS_INT "${_VERBOSE}" || ERR 'User defined variable "$_VERBOSE" contains a non-integer value - Unable to proceed!'
IS_INT "${_WHITELIST}" || ERR 'User defined variable "$_WHITELIST" contains a non-integer value - Unable to proceed!'
# Make sure that at least one address family is enabled
if [ "${_IPV4}" -ne 1 ] && [ "${_IPV6}" -ne 1 ]; then
ERR 'No address family enabled! Please enable IPv4 and/or IPv6 in your pf-badhost config!'
fi
# Make sure $_LOGIN_LIMIT is greater than 0
if [ "${_LOGIN_LIMIT}" -lt 1 ]; then
_LOGIN_LIMIT=1
fi
# Make sure $RETRY is greater than 0
if [ "${_RETRY}" -lt 1 ]; then
_RETRY=1
fi
# Tor var sanity check
if [ "${_TOR_WHITELIST}" -eq 1 ] && [ "${_TOR_BLOCK_ALL}" -eq 1 ]; then
ERR 'Tor Whitelisting/Blacklisting options are mutually exclusive!'
elif [ "${_TOR_WHITELIST}" -eq 1 ] && [ "${_TOR_BLOCK_EXIT}" -eq 1 ]; then
ERR 'Tor Whitelisting/Blacklisting options are mutually exclusive!'
elif [ "${_TOR_BLOCK_ALL}" -eq 1 ] && [ "${_TOR_BLOCK_EXIT}" -eq 1 ]; then
ERR 'Tor Whitelisting/Blacklisting options are mutually exclusive!'
fi
}
# ------------------------------------------------------------------------------
# URL Fetch Functions
# ------------------------------------------------------------------------------
# This function accepts 2 arguments, the first one being the URL to fetch,
# and the second argument being the intended output destination.
# If the second argument is '-' then we output to stdout
#
# Output to filesystem location - Example:
# URL_FETCH https://example.com/file.txt /local/file/path
#
# Output to stdout - Example:
# URL_FETCH https://example.com/file.txt -
URL_FETCH() {
# Create local vars
typeset _URL _OUTPUT_FILE || ERR 'Current shell does not support the non-POSIX "typeset" feature!'
typeset -i _counter _STDOUT
_counter=0
# If constant 'RETRY' hasn't yet been set, create local var and set it to '3'
test -n "${_RETRY}" || typeset -i _RETRY=3
# Make sure URL and output destination were provided
if [ -z "${2}" ] || [ -z "${1}" ]; then
ERR 'No URL and/or output location provided to URL_FETCH function!' ; return 1
elif [ "${2}" = '-' ]; then
_STDOUT=1
_URL="${1}"
_OUTPUT_FILE='/dev/null'
else
_STDOUT=0
_URL="${1}"
_OUTPUT_FILE="${2}"
fi
while true ; do
(( _counter++ )) || true # Increment counter for each download attempt
if [ "${_counter}" -le "${_RETRY}" ]; then
# Sleep 'n' seconds before reattempting download
if [ "${_counter}" -gt 1 ]; then
if [ "${_VERBOSE}" -ne 0 ]; then
printf 'Sleeping for %d seconds before reattempting download...\n\n' "$((_counter * 10))" 1>&2
fi
sleep "$((_counter * 10))"
fi
# Upon successful download from a URL, break the loop and proceed to next URL
if [ "${_STDOUT}" -eq 1 ]; then
# Print to stdout
if myfetch "${_URL}" ; then
return
else
if [ "${_VERBOSE}" -ne 0 ]; then
printf '\nFailed to Fetch List (Attempt #%d): %s\n\n' "${_counter}" "${_URL}" 1>&2
fi
fi
else
# Output to specified filesystem location
if myfetch "${_URL}" > "${_OUTPUT_FILE}" ; then
return
else
if [ "${_VERBOSE}" -ne 0 ]; then
printf '\nFailed to Fetch List (Attempt #%d): %s\n\n' "${_counter}" "${_URL}" 1>&2
fi
fi
fi
else
WARNING "Exceeded Maximum Number of Retries (${_RETRY}) For URL: ${_URL}"
if [ "${_STRICT}" -eq 0 ]; then
# Clean-up any potential garbage from failed download
if [ -f "${_OUTPUT_FILE}" ]; then
rm -f -- "${_OUTPUT_FILE}"
fi
return 0
else
ERR 'Strict Mode Enabled' ; return 1
fi
fi
done
}
ASN_FETCH() {
typeset -u _asn
{ echo '!!' ; for _asn in "${_asn_array[@]}"; do echo "-i origin ${_asn}"; done; echo 'q';} | nc whois.radb.net 43 | myawk -- '$1 == "route:" || $1 == "route6:" {print $2}'
}
PRINT_URL() {
printf '%s\n' "${_BLOCKLISTS}" | SANITIZE_ARRAY
if [ "${_BOGON_4}" -eq 1 ]; then
echo 'https://www.team-cymru.org/Services/Bogons/fullbogons-ipv4.txt'
fi
if [ "${_BOGON_6}" -eq 1 ]; then
echo 'https://www.team-cymru.org/Services/Bogons/fullbogons-ipv6.txt'
fi
}
# ------------------------------------------------------------------------------
# Main Function
# ------------------------------------------------------------------------------
main() {
# Set trap handler
trap TRAP_ABORT ERR INT
# These are declared late because zsh needs ksh array syntax enabled before it can ingest array data
_registrar_url[0]='https://ftp.afrinic.net/pub/stats/afrinic/delegated-afrinic-latest'
_registrar_url[1]='https://ftp.arin.net/pub/stats/arin/delegated-arin-extended-latest'
_registrar_url[2]='https://ftp.lacnic.net/pub/stats/lacnic/delegated-lacnic-latest'
_registrar_url[3]='https://ftp.apnic.net/stats/apnic/delegated-apnic-latest'
_registrar_url[4]='https://ftp.ripe.net/ripe/stats/delegated-ripencc-latest'
_rfc3330[0]='!0.0.0.0/8'
_rfc3330[1]='!10.0.0.0/8'
_rfc3330[2]='!14.0.0.0/8'
_rfc3330[3]='!24.0.0.0/8'
_rfc3330[4]='!39.0.0.0/8'
_rfc3330[5]='!127.0.0.0/8'
_rfc3330[6]='!128.0.0.0/16'
_rfc3330[7]='!169.254.0.0/16'
_rfc3330[8]='!172.16.0.0/12'
_rfc3330[9]='!191.255.0.0/16'
_rfc3330[10]='!192.0.0.0/24'
_rfc3330[11]='!192.0.2.0/24'
_rfc3330[12]='!192.88.99.0/24'
_rfc3330[14]='!192.168.0.0/16'
_rfc3330[14]='!198.18.0.0/15'
_rfc3330[15]='!223.255.255.0/24'
_rfc3330[16]='!224.0.0.0/3'
_rfc5156[0]='!2001:10::/28'
_rfc5156[1]='!2001::/32'
_rfc5156[2]='!2001:db8::/32'
_rfc5156[3]='!2002::/16'
_rfc5156[4]='!3ffe::/16'
_rfc5156[5]='!5f00::/8'
_rfc5156[6]='!::FFFF:0:0/96'
_rfc5156[7]='!fc00::/7'
_rfc5156[8]='!fe80::/10'
_rfc5156[9]='!ff00::/8'
# Mark program info read-only
readonly version release_date release_name
# Mark pf-badhost constants as read-only
readonly _rfc3330 _rfc5156 _registrar_url _SUBNET_MERGE_PERL
# Mark user-defined lists as read-only
readonly _COUNTRY_CODES _ASN_LIST _BLOCKLISTS _USER_RULES
# Initialize counters
typeset -i _array_index=0 _a_counter=0 _g_counter=0 _l_counter=0 _r_counter=0
# Initialize case (in)sensitive vars
typeset -u _asn _cc
typeset -l _opt_arg
# Initialize local vars
typeset _i
# Initialize global configuration vars
_CHECK_ONLY=0 ; _NO_UID_CHECK=0 ; _PRINT_ONLY=0 ; _VERBOSE=1
# Command-line option handling
while getopts 46ABDE:F:GH:J:K:O:R:T:VZ:a:b:g:hj:l:no:r:u:w:x _opts ; do
case "${_opts}" in
4) _IPV4=1 ; _IPV6=0 ;; # Force IPv4-only mode
6) _IPV4=0 ; _IPV6=1 ;; # Force IPv6-only mode
A) _AGGREGATE=1 ;; # Enable subnet aggregation
B) _IPV4=1 ; _IPV6=1 ;; # Force both address families
D) _NO_UID_CHECK=1 ;; # Disable user checking
E) authlog_unzip="${OPTARG}" ;; # set tool to unzip authlog
F) netget="${OPTARG}" ;; # set curl/fetch/ftp/wget preference
G) _GEOBLOCK=1 ;; # Enable Geoblocking
H) _CLOUD_BRUTEFORCE_MITIGATION=1 ; _LOGIN_LIMIT="${OPTARG}" ;; # Enable SSH authlog analysis
J) authlog_path1="${OPTARG}" ;;
K) authlog_path2="${OPTARG}" ;;
O) typeset -l -r _OS_TYPE="${OPTARG}" ;;
R) _RETRY="${OPTARG}" ;; # Maximum number of URL fetch attempts
T) # Tor Filtering
_opt_arg="${OPTARG}"
case "${_opt_arg}" in
allow) _TOR_WHITELIST=1 ;;
block) _TOR_BLOCK_ALL=1 ;;
block_exit) _TOR_BLOCK_EXIT=1 ;;
*) ERR "Invalid option for '-T' : '${OPTARG}'" ;;
esac ;;
V) _VERBOSE=0 ;;
Z) getroot="${OPTARG}" ;;
a) # Filter single ASN
_asn="${OPTARG}"
IS_ASN "${_asn}" || ERR "Invalid ASN: '${OPTARG}'"
_asn_array[${_a_counter}]="${_asn}"
(( _a_counter++ )) || true ;;
b) # Bogon Filtering
IS_INT "${OPTARG}" || ERR "Invalid option for '-b' : '${OPTARG}'"
case "${OPTARG}" in
4) _BOGON_4=1 ;;
6) _BOGON_6=1 ;;
46|64) _BOGON_4=1 ; _BOGON_6=1 ;;
*) ERR "Invalid option for '-b' : '${OPTARG}'" ;;
esac ;;
j) # Filter bulk ASN from local list
if [ -f "${OPTARG}" ] && [ -r "${OPTARG}" ]; then
for _i in $(SANITIZE_ARRAY < "${OPTARG}"); do
_asn="${_i}"
IS_ASN "${_asn}" || ERR "Invalid ASN: '${_i}'"
_asn_array[${_a_counter}]="${_asn}"
(( _a_counter++ )) || true
done
else
ERR "File '${OPTARG}' either not found or has incorrect permissions!"
fi ;;
g) # Block ISO3166 country codes
_cc="${OPTARG}"
_country_code[${_g_counter}]="${_cc}" # Add country to blocklist
(( _g_counter++ )) || true # (Implies '-G')
_GEOBLOCK=1 ;;
h) HELP_MESSAGE ; exit ;;
l) # Add blocklist URL
_user_url[${_l_counter}]="${OPTARG}"
(( _l_counter++ )) || true ;;
n) # Dry run
_CHECK_ONLY=1 ;;
o) # Formatting and runtime options
_opt_arg="${OPTARG}"
case "${_opt_arg}" in
# Log, print & permissions options
log) _LOG=1 ;;
strict) _STRICT=1 ;;
uid-check) _NO_UID_CHECK=0 ;;
pipefail) set -o pipefail ;;
verbose) _VERBOSE=1 ;;
nolog) _LOG=0 ;;
no-strict) _STRICT=0 ;;
no-uid-check) _NO_UID_CHECK=1 ;;
no-verbose) _VERBOSE=0 ;;
# Filtering Options
rfc3330) _RFC3330=1 ;;
rfc5156) _RFC5156=1 ;;
whitelist) _WHITELIST=1 ;;
no-rfc3330) _RFC3330=0 ;;
no-rfc5156) _RFC5156=0 ;;
no-whitelist) _WHITELIST=0 ;;
*) ERR "Invalid option for '-o' : '${OPTARG}'" ;;
esac
;;
r) # Add custom rule
_user_rule[${_r_counter}]="${OPTARG}" # Custom user rules
(( _r_counter++ )) || true ;;
u) # Add blocklist URL in bulk from local list
if [ -f "${OPTARG}" ] && [ -r "${OPTARG}" ]; then
for _i in $(SANITIZE_ARRAY < "${OPTARG}"); do
_user_url[${_l_counter}]="${_i}"
(( _l_counter++ )) || true
done
else
ERR "File '${OPTARG}' either not found or has incorrect permissions!"
fi ;;
w) # Add custom user rules in bulk from local list
if [ -f "${OPTARG}" ] && [ -r "${OPTARG}" ]; then
for _i in $(SANITIZE_ARRAY < "${OPTARG}"); do
_user_rule[${_r_counter}]="${_i}"
(( _r_counter++ )) || true
done
else
ERR "File '${OPTARG}' either not found or has incorrect permissions!"
fi ;;
x) _PRINT_ONLY=1 ; _LOG=0 ; _NO_UID_CHECK=1 ; confpath='/dev/null' ; getroot='false' ;; # Print generated list to stdout
?) HELP_MESSAGE 1>&2 ; exit 2 ;;
esac
done
# Mark commandline flags as read-only
readonly _CHECK_ONLY _NO_UID_CHECK _PRINT_ONLY _VERBOSE
# Mark user-defined booleans as read-only
readonly _AGENT _LOG _STRICT _IPV4 _IPV6 _AGGREGATE \
_GEOBLOCK _BOGON_4 _BOGON_6 _CLOUD_BRUTEFORCE_MITIGATION \
_TOR_WHITELIST _TOR_BLOCK_ALL _TOR_BLOCK_EXIT \
_RFC3330 _RFC5156 _WHITELIST
# Set variables based on specified operating system
# We use 'test -n' here to check for config overrides provided via commandline argument
case "${_OS_TYPE}" in
dragonflybsd)
test -n "${getroot}" || getroot="$(CHECK_CMD doas)"
test -n "${netget}" || netget='fetch'
test -n "${authlog_path1}" || authlog_path1='/var/log/auth.log'
test -n "${authlog_path2}" || authlog_path2='/var/log/auth.log.0.gz'
test -n "${authlog_unzip}" || authlog_unzip="$(CHECK_CMD zcat)"
;;
freebsd)
test -n "${getroot}" || getroot="$(CHECK_CMD doas)"
test -n "${netget}" || netget='fetch'
test -n "${authlog_path1}" || authlog_path1='/var/log/auth.log'
test -n "${authlog_path2}" || authlog_path2='/var/log/auth.log.0.bz2'
test -n "${authlog_unzip}" || authlog_unzip="$(CHECK_CMD bzcat)"
;;
macos)
test -n "${getroot}" || getroot="$(CHECK_CMD sudo)"
test -n "${netget}" || netget='curl'
test -n "${authlog_path1}" || authlog_path1='/dev/null'
test -n "${authlog_path2}" || authlog_path2='/dev/null'
test -n "${authlog_unzip}" || authlog_unzip="$(CHECK_CMD gzip)"
;;
netbsd)
# NetBSD does annoying things with their $PATH, so make sure we set what we need
PATH='/usr/pkg/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
test -n "${getroot}" || getroot="$(CHECK_CMD doas)"
test -n "${netget}" || netget='curl'
test -n "${authlog_path1}" || authlog_path1='/var/log/authlog'
test -n "${authlog_path2}" || authlog_path2='/var/log/authlog.0.gz'
test -n "${authlog_unzip}" || authlog_unzip="$(CHECK_CMD zcat)"
;;
openbsd)
test -n "${getroot}" || getroot="$(CHECK_CMD doas)"
test -n "${netget}" || netget='ftp'
test -n "${authlog_path1}" || authlog_path1='/var/log/authlog'
test -n "${authlog_path2}" || authlog_path2='/var/log/authlog.0.gz'
test -n "${authlog_unzip}" || authlog_unzip="$(CHECK_CMD zcat)"
;;
custom)
test -n "${getroot}" || ERR "Custom OS type specified - please set doas/sudo preference with '-Z' option"
test -n "${netget}" || ERR "Custom OS type specified - please set curl/fetch/ftp/wget preference with '-F' option"
if [ "${_CLOUD_BRUTEFORCE_MITIGATION}" -eq 1 ]; then
test -n "${authlog_path1}" || ERR "Custom OS type specified - please specifiy path to SSH authlog with '-J' option"
test -n "${authlog_path2}" || ERR "Custom OS type specified - please specifiy path to secondary SSH authlog with '-K' option"
test -n "${authlog_unzip}" || ERR "Custom OS type specified - please specifiy zcat/bzcat for SSH authlog analysis with '-E' option"
fi
;;
*)
printf '\n\nUnknown Operating System Specified. Available Options Are:\n * -OpenBSD\n * -FreeBSD\n * -NetBSD\n * -DragonflyBSD\n * -MacOS\n\nQuitting Without Making Changes...\n\n'
exit 1
;;
esac
# Mark operating system specific variables as read-only
readonly getroot netget authlog_path1 authlog_path2 authlog_unzip
# Config test / dry run
if [ "${_CHECK_ONLY}" -eq 1 ]; then
if VAR_SANITY_CHECK && PRE_EXEC_TESTS ; then
printf 'Config looks sane!\n' 1>&2 ; exit 0
else
ERR 'Invalid config!'
fi
fi
# Ensure user-provided values are sane
VAR_SANITY_CHECK
# These are marked late because VAR_SANITY_CHECK() may modify them
readonly _RETRY _LOGIN_LIMIT
# Run pre-execution tests to ensure that conditions are sane
PRE_EXEC_TESTS
# Mark pre-exec tests results as read-only
readonly awk_patch perl_exist agg4 agg6 go_agg
### Add values from config area to arrays
### User-defined rules
# Determine array index position
if [ "${#_user_rule[@]}" -ge 1 ]; then
_array_index=$((${#_user_rule[@]} + 1))
else
_array_index=0
fi
# Add user rules specified in config to array
for _i in $(printf '%s\n' "${_USER_RULES}" | SANITIZE_ARRAY); do
_user_rule[${_array_index}]="${_i}"
(( _array_index++ )) || true
done
### Blocklist URLs
# Determine array index position
if [ "${#_user_url[@]}" -ge 1 ]; then
_array_index=$((${#_user_url[@]} + 1))
else
_array_index=0
fi
# Add blocklist URLs specified in config to array
for _i in $(PRINT_URL); do
_user_url[${_array_index}]="${_i}"
(( _array_index++ )) || true
done
### ISO3166 country codes
# Determine array index position
if [ "${#_country_code[@]}" -ge 1 ]; then
_array_index=$((${#_country_code[@]} + 1))
else
_array_index=0
fi
# Add country codes specified in config to array
for _i in $(SANITIZE_COUNTRY_CODES); do
_country_code[${_array_index}]="${_i}"
(( _array_index++ )) || true
done
### ASN filtering
# Determine array index position
if [ "${#_asn_array[@]}" -ge 1 ]; then
_array_index=$((${#_asn_array[@]} + 1))
else
_array_index=0
fi
# Add ASN's specified in config to array
for _i in $(printf '%s\n' "${_ASN_LIST}" | SANITIZE_ARRAY); do
_asn="${_i}"
IS_ASN "${_asn}" || ERR "Invalid ASN: '${_i}'"
_asn_array[${_array_index}]="${_asn}"
(( _array_index++ )) || true
done
# Mark arrays as read-only
readonly _country_code _user_rule _user_url
# Temp file/dir vars
listdir="$(mktemp -d || TMP_FILE_ABORT)"
geodir="$(mktemp -d || TMP_FILE_ABORT)"
workdir="$(mktemp -d || TMP_FILE_ABORT)"
scratchdir="$(mktemp -d || TMP_FILE_ABORT)"
v4list="$(TMP_FILE_SCRATCH)"
v6list="$(TMP_FILE_SCRATCH)"
user_rules="$(TMP_FILE_SCRATCH)"
finout="$(TMP_FILE_SCRATCH)"
oldconf="$(TMP_FILE_SCRATCH)"
authlog="$(TMP_FILE_SCRATCH)"
gztemp="$(TMP_FILE_SCRATCH)"
tor_rawlist="$(TMP_FILE_SCRATCH)"
tor_blacklist="$(TMP_FILE_SCRATCH)"
tor_whitelist="$(TMP_FILE_SCRATCH)"
# Mark temporary file locations as read-only
readonly listdir geodir workdir scratchdir v4list v6list \
user_rules finout oldconf authlog gztemp \
tor_rawlist tor_blacklist tor_whitelist
# Set working directory
cd -- "${workdir}" || TMP_FILE_ABORT
# Fetch blocklist urls
for _i in "${_user_url[@]}"; do
URL_FETCH "${_i}" "$(TMP_FILE)"
done
# Run Geoblock function if enabled
if [ "${_GEOBLOCK}" -eq 1 ]; then
# Fetch registrar datasets
for _i in "${_registrar_url[@]}"; do
URL_FETCH "${_i}" "$(TMP_FILE_GEOBLOCK)"
done
### Add ASN info of blocked countries to ASN array
# Determine array index position
if [ "${#_asn_array[@]}" -ge 1 ]; then
_array_index=$((${#_asn_array[@]} + 1))
else
_array_index=0
fi
for _asn in $(GEO_ASN); do
IS_ASN "${_asn}" || ERR "Invalid ASN: '${_asn}'"
_asn_array[${_array_index}]="${_asn}"
(( _array_index++ )) || true
done
### Parse non-ASN registered IP data
GEOBLOCKER
fi
# Mark _asn_array as read-only
readonly _asn_array
# Perform Tor filtering if enabled
if [ "${_TOR_BLOCK_ALL}" -eq 1 ] || [ "${_TOR_BLOCK_EXIT}" -eq 1 ] || [ "${_TOR_WHITELIST}" -eq 1 ]; then
TOR_FILTER
fi
# Do ASN filtering if ASN found in array
if [ "${#_asn_array[@]}" -gt 0 ]; then
ASN_FETCH > "$(TMP_FILE)"
fi
# Fetch user-provided custom lists
CUSTOM_LISTS
# Gen user rules
for _i in "${_user_rule[@]}"; do
printf '%s\n' "${_i}"
done > "${user_rules}"
# CLOUD_BRUTEFORCE_MITIGATION
if [ "${_CLOUD_BRUTEFORCE_MITIGATION}" -eq 1 ]; then
CLOUD_BRUTEFORCE_MITIGATION
fi
# Generate lists to load into PF
LIST_GEN
# If -x option is specified, we only print the list to stdout
# without modifying the current pf-badhost table
if [ "${_PRINT_ONLY}" -eq 1 ]; then
cat -- "${finout}"
else
# Install Newly Generated Blocklist
LIST_INSTALL
fi
# Print Blocklist Stats
WARNING "$(PRINT_STATS)"
# Clean up after ourselves
CLEANUP
}
# ZSH needs to run in compatability mode to prevent it from puking
if command -v emulate >/dev/null 2>&1 ; then
emulate -LR ksh
fi
# Make sure shell supports typeset
command -v typeset >/dev/null 2>&1 || ERR 'Are you running a modern shell? Current shell does not appear to support the non-POSIX "typeset" command...'
# Execute main function
main "$@"