#!/bin/sh

# Copyright 2020 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.

# Version 0.4p0 "Violence Begets Violence" -- Released July 10th 2020

# In loving memory of Ron Sather

# This script downloads some of the most popular IP Blocklists, but you can add 
# any lists you like. Simply add a line in the format below: 
# Example: "$netget" "$netgetarg" - https://www.example.com/example.txt  > "$(tmp_file)"
# IPv4 or IPv6 lists containing individual addresses or CIDR blocks are supported.
# The address parser also supports mixed lists. 

# To enable blocking by country, authlog analysis or enable IPv6 and optional blocklists, see below.

# IPv6 Notes:
# pf-badhost requires IPv6 lists to be preformatted to be RFC-5952 compliant.
# Several 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. 

# -------------------------------------------------------------------

### Abort Sequences and Housekeeping

# Safely abort if config checks fail
abort() {
	printf '\n/etc/pf-badhost.txt Contains Bad Data! Reverting changes and bailing out...\n\n' ; cp /dev/null /etc/pf-badhost.txt ; cleanup ; exit 1
}

network_check_abort() {
	printf '\nNo Network Connectivity! Quitting without making changes...\n\n' ; cleanup ; exit 1
}

list_update_abort() {
	printf '\nFailed to update /etc/pf-badhost.txt! Please ensure the file has correct permissions and that the partition has free space!\n\n' ; cleanup ; exit 1
}

tmp_file_abort() {
	printf '\nSomething went wrong!\n\n It seems we failed to create and/or write to a temporary file! Please ensure that "/tmp" has free space!\n\nReverting changes and bailing out...\n\n' ; cleanup ; exit 1
}

trapper() {
	cleanup && exit
}

cleanup() {
	rm -rf "$tmpdir" "$outdir" "$geodir" "$scratchdir"
}

help_message() {
	printf '\npf-badhost Version 0.4\nJordan Geoghegan <jordan@geoghegan.ca>\n\n'
	printf 'pf-badhost provides blocklists from quality trusted sources to feed into the PF firewall\n\n'
	printf 'Supported Operating Systems:\n  OpenBSD\n  FreeBSD\n  NetBSD\n  DragonflyBSD\n  MacOS\n'
	printf 'Alternate OS can be supplied as an argument: "-freebsd" or "-dragonflybsd" etc\n'
	printf 'OS arguments are case insensitive\n'
}


### Temp File Creation

tmp_file() {
	mktemp "$tmpdir/tmp.XXXXXXXX" || tmp_file_abort
}

tmp_file_geodir() {
	mktemp "$geodir/geo.XXXXXXXX" || tmp_file_abort
}

tmp_file_outdir() {
	mktemp "$outdir/out.XXXXXXXX" || tmp_file_abort
}

tmp_file_scratchdir() {
	mktemp "$scratchdir/scratch.XXXXXXXX" || tmp_file_abort
}


### IP Validator Functions

# 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
	nice grep -E -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
	tr -cd '[:alnum:]:./\n' | nice grep -E -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}))?'
}


### Subnet Aggregation Functions

# IPv4 CIDR/Address Aggregator [pure Perl version of ISC "aggregate" utility]
subnet_merge=$(cat <<'EOF'
#!/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
EOF
)

# Pure Perl version of ISC "aggregate" utitity [code stored in above variable "subnet_merge"]
# WARNING: ~10x slower than C based utilty [see below]
sub_aggr_perl() {
	nice perl -e "$subnet_merge" | sed 's/\/32$//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_aggr_c() {
	awk '!/\/[1-9]|\/[1-2][[:digit:]]|\/3[0-2]$/ {$0=$0"\/32"}1' | nice aggregate -qt | 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_aggr_py() {
	nice aggregate6 -t -4
}

# Call "aggregate6" utility to aggregate IPv6 address list. 
sub_aggr6_py() {
	nice aggregate6 -t -6
}

sub_aggr() {
	## IPv4 Logic
	# If aggregate{6} or Perl not installed,  we can't do subnet aggregation
	if [ "$agg4" = "0" ] && [ "$agg6" = "0" ] && [ "$perl_exist" = "0" ]; then
		printf '\nSubnet Aggregation Enabled, but Perl, aggregate and/or aggregate6 not found!\nUnable to perform IPv4 Subnet Aggregation!\n\n'
		noopt
	fi
	# If aggregate{6} utility not found, use Perl function
	if [ "$agg4" = "0" ] && [ "$agg6" = "0" ] && [ "$perl_exist" = "1" ]; then
		printf '\nFalling back to pure Perl aggregator...\n\n'
		find "$tmpdir" -type f -print0 | xargs -0 cat | parse_v4 | sub_aggr_perl > "$v4list" || tmp_file_abort
	fi
	# aggregate C utility
	if [ "$agg4" = "1" ] && [ "$agg6" = "0" ]; then
		find "$tmpdir" -type f -print0 | xargs -0 cat | parse_v4 | sub_aggr_c > "$v4list" || tmp_file_abort
	fi
	# Use aggregate6 IPv4 support if C based aggregate util not found
	if [ "$agg4" = "0" ] && [ "$agg6" = "1" ]; then
		find "$tmpdir" -type f -print0 | xargs -0 cat | parse_v4 | sub_aggr_py > "$v4list" || tmp_file_abort
	fi
	# Prefer the C based 'aggregate' util over the Python based 'aggregate6' util
	if [ "$agg4" = "1" ] && [ "$agg6" = "1" ]; then
		find "$tmpdir" -type f -print0 | xargs -0 cat | parse_v4 | sub_aggr_c > "$v4list" || tmp_file_abort
	fi

	## IPv6 Logic
	# If IPv6 is enabled and 'aggregate6' is installed, then we can aggregate IPv6 prefixes too!
	if [ "$IPv6" = "1" ] && [ "$agg6" = "1" ]; then
		find "$tmpdir" -type f -print0 | xargs -0 cat | parse_v6 | sub_aggr6_py > "$v6list" || tmp_file_abort
	fi
	# If IPv6=1 but aggregate6 util not found...
	if [ "$IPv6" = "1" ] && [ "$agg6" = "0" ]; then
		echo "aggregate6 utility not found, unable to aggregate IPv6 addresses..."
		find "$tmpdir" -type f -print0 | xargs -0 cat | parse_v6 | tr '[:upper:]' '[:lower:]' | sort -Vu > "$v6list" || tmp_file_abort
	fi
}

noopt() {
	if [ "$IPv6" = "1" ]; then
		find "$tmpdir" -type f -print0 | xargs -0 cat | parse_v4 | sort -Vu > "$v4list" || tmp_file_abort
		find "$tmpdir" -type f -print0 | xargs -0 cat | parse_v6 | tr '[:upper:]' '[:lower:]' | sort -Vu > "$v6list" || tmp_file_abort
	else
		find "$tmpdir" -type f -print0 | xargs -0 cat | parse_v4 | sort -Vu > "$v4list" || tmp_file_abort
	fi
}


### GeoBlock Functions

# Download GeoIP Data from Registries
geoblock_ftp() {
	"$netget" "$netgetarg" -  https://ftp.afrinic.net/pub/stats/afrinic/delegated-afrinic-latest > "$(tmp_file_geodir)"
	"$netget" "$netgetarg" -  https://ftp.arin.net/pub/stats/arin/delegated-arin-extended-latest > "$(tmp_file_geodir)"
	"$netget" "$netgetarg" -  https://ftp.lacnic.net/pub/stats/lacnic/delegated-lacnic-latest > "$(tmp_file_geodir)"
	"$netget" "$netgetarg" -  https://ftp.apnic.net/stats/apnic/delegated-apnic-latest > "$(tmp_file_geodir)"
	"$netget" "$netgetarg" -  https://ftp.ripe.net/ripe/stats/delegated-ripencc-latest > "$(tmp_file_geodir)"
}

# Requires patched awk released after June 2020) (works with mawk or gawk)
geoblock_clean() {
	echo "$country_codes" | tr -d '[:blank:]' | tr '[:lower:]' '[:upper:]' | grep -E '^[[:alpha:]]{2}$' > "$codes"
	
	# Generate Country IP CIDR Blocks
	while read -r cc; do
	    find "$geodir" -type f -exec nice awk -v country="$cc" -F '|' '{if ($2 == country && $3 == "ipv4") printf("%s/%d\n", $4, 32-log($5)/log(2))} {if ($2 == country && $3 == "ipv6") printf("%s/%d\n", $4, $5)}' {} +
	done < "$codes" > "$(tmp_file)"
}

# Work around awk bug (fix pending as of 2020/06/07)
geoblock_workaround() {
	echo "$country_codes" | tr -d '[:blank:]' | tr '[:lower:]' '[:upper:]' | grep -E '^[[:alpha:]]{2}$' > "$codes"
	
	# Generate Country IP CIDR Blocks
	while read -r cc; do
	   find "$geodir" -type f -exec nice awk -v country="$cc" -F '|' '{if ($2 == country && $3 == "ipv4") print($4, $5)}' {} + | nice awk  '{printf("%s/%d\n", $1, 32-log($2)/log(2))}'
	   if [ "$IPv6" = "1" ]; then
	   	find "$geodir" -type f -exec nice awk -v country="$cc" -F '|' '{if ($2 == country && $3 == "ipv6") printf("%s/%d\n", $4, $5)}' {} +
	   fi
	done < "$codes" > "$(tmp_file)"
}

geoblock() {
	geoblock_ftp 
	if [ "$awkpatch" = "1" ]; then
		geoblock_clean	
	else
		geoblock_workaround
	fi
}


### Authlog analysis functions

hail_mary_mitigate() {
	# IPv4 Support
	if [ "$ostype" != "-macos" ]; then
	     "$getroot" "$authlog_unzip" -f "$authlog_path1" "$authlog_path2" | authlog_proc | parse_v4 | whitelist | awk '{ a[$0]++ }END{ for(i in a) print a[i],i }' | awk -v threshold="$threshold" '$1>threshold {print $2}' | sort -Vu
	fi
	# IPv6 Support
	if [ "$IPv6" = "1" ] && [ "$ostype" != "-macos" ]; then
	     "$getroot" "$authlog_unzip" -f "$authlog_path1" "$authlog_path2" | authlog_proc | parse_v6 | whitelist | awk '{ a[$0]++ }END{ for(i in a) print a[i],i }' | awk -v threshold="$threshold" '$1>threshold {print $2}' | sort -Vu
	fi

	if [ "$ostype" = "-macos" ]; then
		echo "MacOS does not support authlog analysis :("
	fi
}

# Hail Mary Mitigation preproccessor
authlog_proc() {
	awk '{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)}' 
}


### Misc Tests/Functions

awk_test() {
	echo 5e58386636aa775c2106140445 | awk 'END {print log(2)}' 2>&1 | grep -c 'log result out of range'
}

perl_test() {
	perl -v >/dev/null 2>&1
}

aggregate_test() {
	echo "192.0.2.0/24" | aggregate -q >/dev/null 2>&1
}

aggregate6_test() {
	aggregate6 -h >/dev/null 2>&1
}

whitelist() {
	# Add addresses to whitelist from SSH Auth log analysis
	# Supports IPv4 and IPv6 addresses
        grep -E -v '192.0.2.5|2001:db8::2'
}

# -------------------------------------------------------------------

### IPv6 Pre-Processors

# SpamHause IPv6 Preprocessor 
spamhausev6_proc() {
	# Function created for consistency
	awk '{print $1}'
}

# -------------------------------------------------------------------

# Variables, Macros and Traps

# Set trap handler
trap trapper EXIT
trap trapper INT

# Print help message
if [ "$1" = "-h" ]; then
  help_message && exit
fi

# Make sure we're running as "_pfbadhost" user
if [ "$(whoami)" != "_pfbadhost" ]; then
	printf '\n\nScript must be run as user "_pfbadhost"\nExiting...\n\n'; exit 1
fi

# If Operating System not specified as commandline argument, assume we're running on OpenBSD
if [ -z "$1" ];
then
  ostype="-openbsd"
else
  ostype="$(echo "$1" | tr '[:upper:]' '[:lower:]')"
fi

# Make sure we make appropriate use of doas/sudo and ftp/curl
case "$ostype" in
-openbsd)
  getroot="doas"
  netget="ftp"
  netgetarg="-o"
  authlog_path1="/var/log/authlog"
  authlog_path2="/var/log/authlog.0.gz"
  authlog_unzip="zcat"
  ;;
-freebsd)
  getroot="doas"
  netget="fetch"
  netgetarg="-qo"
  authlog_path1="/var/log/auth.log"
  authlog_path2="/var/log/auth.log.0.bz2"
  authlog_unzip="bzcat"
  ;;
-netbsd)
  getroot="/usr/pkg/bin/doas"
  netget="ftp"
  netgetarg="-o"
  authlog_path1="/var/log/authlog"
  authlog_path2="/var/log/authlog.0.gz"
  authlog_unzip="zcat"
  ;;
-dragonflybsd)
  getroot="doas"
  netget="ftp"
  netgetarg="-o"
  authlog_path1="/var/log/auth.log"
  authlog_path2="/var/log/auth.log.0.gz"
  authlog_unzip="zcat"
  ;;
-macos)
  getroot="sudo"
  netget="curl"
  netgetarg="-so"
  authlog_path1="/dev/null"
  authlog_path2="/dev/null"
  authlog_unzip="/dev/null"
  ;;
*)
  printf '\n\nUnknown Operating System Specified. Available Options Are:\n  OpenBSD\n  FreeBSD\n  NetBSD\n  DragonflyBSD\n  MacOS\n'
  printf '\nQuitting Without Making Changes...\n'
  exit 1
  ;;
esac

# Check for network connectivity to GitHub, bail out if fail
"$netget" "$netgetarg" - https://github.com > /dev/null || network_check_abort

# See if awk is patched
if [ "$(awk_test)" = 0 ];
then
    awkpatch="1"
else
    awkpatch="0"
fi

# See if aggregate is installed
if perl_test ; then
    perl_exist="1"
else
    perl_exist="0"
fi

# See if aggregate is installed
if aggregate_test ; then
    agg4="1"
else
    agg4="0"
fi

# See if aggregate6 is installed
if aggregate6_test ; then
    agg6="1"
else
    agg6="0"
fi

### Macros / Variables
tmpdir=$(mktemp -d || tmp_file_abort)
outdir=$(mktemp -d || tmp_file_abort)
geodir=$(mktemp -d || tmp_file_abort)
scratchdir=$(mktemp -d || tmp_file_abort)
v4list=$(tmp_file_outdir)
v6list=$(tmp_file_outdir)
userlist=$(tmp_file_outdir)
finout=$(tmp_file_outdir)
authlog=$(tmp_file_scratchdir)
codes=$(tmp_file_scratchdir)

# -------------------------------------------------------------------

### User Configuration Area -- BEGIN

# Configure additional lists to your hearts content. 
# 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
# Enable IPv6
IPv6="0"

# Enable IPv4 Bogon Filter
bogon4="0"

# Enable IPv6 Bogon Filter
bogon6="0"

# Enable Subnet Aggregation
agg="0"

# Enable Geoblocking / Country Blacklisting
geoblock="0"

###################################################################
# Country GeoIP Blacklist
# Enter any ISO-3166 Country Codes you want to block (1 per line)
country_codes=$(cat <<'EOF'
CN
IR
KP
EOF
)
###################################################################

###################################################################
# User Defined Rules: (add or negate addresses from block list)
# echo "!169.254.169.254" >> "$userlist"
# echo "!2001:19f0:ffff::1" >> "$userlist"
# echo "192.0.2.0/24 >> "$userlist"
###################################################################

###################################################################
# Custom Lists:
#
# Combined list of all blocklist.de lists (adds over 30,000 addresses)
# "$netget" "$netgetarg" - https://lists.blocklist.de/lists/all.txt > "$(tmp_file)"
#
# Spamhause DROP lists (Don't Route Or Peer)
 "$netget" "$netgetarg" - https://www.spamhaus.org/drop/drop.txt > "$(tmp_file)"
 "$netget" "$netgetarg" - https://www.spamhaus.org/drop/edrop.txt > "$(tmp_file)"
 "$netget" "$netgetarg" - https://www.spamhaus.org/drop/dropv6.txt | spamhausev6_proc > "$(tmp_file)"
#
# SSH attackers list 
 "$netget" "$netgetarg" - https://lists.blocklist.de/lists/ssh.txt > "$(tmp_file)"
#
# Block Martians
 "$netget" "$netgetarg" - https://www.team-cymru.org/Services/Bogons/bogon-bn-agg.txt > "$(tmp_file)"
#
# Block Shodan 
 "$netget" "$netgetarg" - https://isc.sans.edu/api/threatlist/shodan/?text > "$(tmp_file)"
#
# Download popular IPv4 blocklists 
 "$netget" "$netgetarg" - https://www.binarydefense.com/banlist.txt > "$(tmp_file)"
 "$netget" "$netgetarg" - https://rules.emergingthreats.net/blockrules/compromised-ips.txt > "$(tmp_file)"
 "$netget" "$netgetarg" - https://rules.emergingthreats.net/fwrules/emerging-Block-IPs.txt > "$(tmp_file)"
 "$netget" "$netgetarg" - https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_level1.netset > "$(tmp_file)"
 "$netget" "$netgetarg" - https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_level2.netset > "$(tmp_file)"
 "$netget" "$netgetarg" - https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_level3.netset > "$(tmp_file)"
#
###################################################################

###################################################################
# Hail Mary Cloud mitigation (authlog analysis)
# Requires adding line to /etc/doas.conf:
# permit nopass _pfbadhost cmd zcat args -f /var/log/authlog /var/log/authlog.0.gz
#
# Set failed log-in threshold for bans
threshold="25"
# Uncomment line below to enable
# hail_mary_mitigate > "$authlog" # Search /var/log/authlog for bruteforcers
###################################################################

###################################################################
# Example Templates:
# IPv4:
# "$netget" "$netgetarg" - https://www.example.com/example.txt  > "$(tmp_file)"
#
# IPv6:
# "$netget" "$netgetarg" - https://www.example.com/example.txt | preprocessor > "$(tmp_file)"
#
# Local File: # Make sure file is readable by user "_pfbadhost"
# cat /path/to/file.txt > "$(tmp_file)"
###################################################################

### User Configuration Area -- END

# -------------------------------------------------------------------


# (Do not edit this section unless you know what you're doing)

if [ "$geoblock" = "1" ]; then
   geoblock
fi

if [ "$bogon4" = "1" ]; then
   "$netget" "$netgetarg" - https://www.team-cymru.org/Services/Bogons/fullbogons-ipv4.txt > "$(tmp_file)"
fi

if [ "$bogon6" = "1" ]; then
   "$netget" "$netgetarg" - https://www.team-cymru.org/Services/Bogons/fullbogons-ipv6.txt > "$(tmp_file)"
fi

if [ "$agg" = "1" ]; then
   sub_aggr
else
   noopt
fi

# Create and move banlist into /etc and reload the pf table
{ echo "# Date Created: $(date)"; \
printf '\n# User Defined Rules:\n\n'; \
cat "$userlist"; \
printf '\n# Rules Generated from /var/log/authlog:\n\n'; \
cat "$authlog"; \
printf '\n# List Generated Rules:\n\n'; \
cat "$v4list" "$v6list"; } > "$finout" || tmp_file_abort
cp "$finout" /etc/pf-badhost.txt || list_update_abort
"$getroot" pfctl -nf /etc/pf.conf || abort   # Ensure proposed changes are valid before reloading table
"$getroot" pfctl -t pfbadhost -T replace -f /etc/pf-badhost.txt || abort

# Print number of addresses in table (expand CIDR ranges)
v4_num="$(grep -E -cv "/[[:digit:]]{1,2}$" "$v4list")"
v4_total="$(grep -E "/[[:digit:]]{1,2}$" "$v4list" | awk -F '/' '{print 2^(32 - $2)}' | awk -v v4num="$v4_num" '{sum+=$1} END {printf "%0.0f", sum + v4num}')"

if [ "$IPv6" = "1" ]; then
   v6_num="$(grep -E -cv "/[[:digit:]]{1,3}$" "$v6list")"
   v6_total="$(grep -E "/[[:digit:]]{1,3}$" "$v6list" | awk -F '/' '{print 2^(128 - $2)}' | awk -v v6num="$v6_num" '{sum+=$1} END {printf "%0.0f", sum + v6num}')"
   printf '\nIPv4 addresses in table:  %s\n' "$v4_total"
   printf 'IPv6 addresses in table:  %s\n\n' "$v6_total" 
else
   printf '\nIPv4 addresses in table:  %s\n\n' "$v4_total"
fi

# Clean up after ourselves
cleanup || echo "WARNING: Failed to delete temporary files!"
