#!/bin/bash #====================================================================== # geo-nft.sh Geolocation for nftables # See https://github.com/wirefalls/geo-nft/wiki/Copyright for copyright # information. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # A bash script to create nftables set definition files containing # country-specific IPv4 and IPv6 address ranges for geolocation filtering. # https://github.com/wirefalls/geo-nft # https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 # # This script requires write access to the pathnames of the geo_conf and # errorlog files as well as the install directory (base_dir), which by # default is: /etc/nftables/geo-nft. # # Supply the -s command line argument to silence verbose output. #====================================================================== # Standard script variables. # Semantic version number of this script. geo_nft_ver=v2.2.10 # Filename of this script. script_name="geo-nft.sh" # User configuration pathname/filename. geo_conf="/etc/geo-nft.conf" # Error log pathname/filename. This file logs errors in addition to the systemd Journal. errorlog="/var/log/geo-nft-error.log" # Geolocation database filename. dbip_filename="dbip-country-lite-$(date +%Y-%m).csv" # Download URL. dbip_url="https://download.db-ip.com/free/$dbip_filename.gz" # Current date/time. datetime="$(date +"%Y-%m-%d %H:%M:%S")" # Default base directory where this script writes it's files. base_dir="/etc/nftables/geo-nft" # Check for command line arguments and process them. for val in "$@" do if [ "$val" = "-s" ]; then # Allow script output to be suppressed when run as a service. # Call the script with the -s argument to run silent. silent="yes" else printf "\n%s\n" "Command line argument invalid: $val" fi done # Print script messages to the screen when script is run manually. # Output can be silenced with the -s argument when run as a service. print_line() { if [ ! "$silent" = "yes" ] && [ "$#" -gt 0 ]; then for text in "$@" do if [ "$text" = "\n" ]; then printf '\n' elif [ "$text" = "\t" ]; then printf '\t' else printf '%s' "$text" fi done fi } # Print error messages to the screen and the error log file. error_log() { if [ "$#" -gt 0 ]; then # If the error log has more than 50 lines then rotate it. if [ -s "$errorlog" ] && [ "$(awk 'END{print NR}' $errorlog)" -ge 50 ]; then print_line "\n" "Rotating error log $errorlog" "\n" mv -f $errorlog "$errorlog.1" fi # Write error messages to the error log file. printf '%s\n' "[$datetime] - $base_dir/$script_name:" >> $errorlog print_line "\n" "$script_name $geo_nft_ver:" "\n" for err in "$@" do print_line "\t" "$err" "\n" printf '\t%s\n' "$err" >> $errorlog done print_line '\n' printf '\n' >> $errorlog else print_line "\n" "$script_name: The 'error_log' function needs at least one argument." "\n" fi } # Verify that the nft program is available and store it's pathname. nft="$(command -v nft)" if [ ! $? -eq 0 ]; then error_log "Unable to find the 'nft' program to determine it's pathname. Verify that" \ "the nft program is located in a directory in your PATH environment" \ "variable. The nft program is part of the 'nftables' package. Exiting..." exit 1 fi # Verify the status of nftables check_nftables() { nftables_status="$(systemctl is-active nftables.service)" if [ "$nftables_status" != "active" ]; then print_line "\n" error_log "nftables is not active. Verify that nftables is installed" \ "and running with: sudo systemctl status nftables" \ "nftables can be restarted with: sudo systemctl restart nftables" fi } # Create the user configuration file /etc/geo-nft.conf with default settings. make_config() { # Check the base_dir directory first for the existence of the geo-nft.sh script. If not # found then check present working directory for the script. If found there then set the # base_dir variable to the present working directory, which sets the base_dir variable # in /etc/geo-nft.conf as well. if [ ! -s "$base_dir/$script_name" ]; then if [ -s "$PWD/$script_name" ]; then base_dir=$PWD else error_log "Create user config: Unable to find $script_name in $PWD Exiting..." \ "Change directory (cd) to the base geo-nft directory and run this script again." exit 1 fi fi printf '%s\n' "# Geolocation for nftables configuration file" > "$geo_conf" printf '%s\n' "# https://github.com/wirefalls/geo-nft" >> "$geo_conf" printf '%s\n' "# Generated by $script_name $geo_nft_ver" >> "$geo_conf" printf '%s\n' "# $(date)" >> "$geo_conf" printf '%s\n' "#==================================================" >> "$geo_conf" printf '\n' >> "$geo_conf" printf '%s\n' "# Internet Protocols supported. At least one of the" >> "$geo_conf" printf '%s\n' "# ipv4 or ipv6 settings must be set to yes." >> "$geo_conf" printf '%s\n' "# Enable the creation of IPv4 country sets (yes/no)." >> "$geo_conf" printf '%s\n' "enable_ipv4=$enable_ipv4" >> "$geo_conf" printf '\n' >> "$geo_conf" printf '%s\n' "# Enable the creation of IPv6 country sets (yes/no)." >> "$geo_conf" printf '%s\n' "enable_ipv6=$enable_ipv6" >> "$geo_conf" printf '\n\n' >> "$geo_conf" printf '%s\n' "# Define the base directory where the $script_name script" >> "$geo_conf" printf '%s\n' "# is located and writes it's files to. Avoid pathnames" >> "$geo_conf" printf '%s\n' "# with spaces, links or special characters." >> "$geo_conf" printf '%s\n' "# Default: /etc/nftables/geo-nft" >> "$geo_conf" printf '%s\n' "base_dir=$base_dir" >> "$geo_conf" printf '\n' >> "$geo_conf" printf '%s\n' "# Enable restarting nftables after a database update." >> "$geo_conf" printf '%s\n' "# Be aware that enabling this may break established" >> "$geo_conf" printf '%s\n' "# connections between your system and other computers," >> "$geo_conf" printf '%s\n' "# such as ssh sessions, connections to websites, etc." >> "$geo_conf" printf '%s\n' "# It's recommended to set this to 'no' for most use" >> "$geo_conf" printf '%s\n' "# cases to allow increased error checking provided by" >> "$geo_conf" printf '%s\n' "# setting 'enable_refill' to yes below." >> "$geo_conf" printf '%s\n' "# If you set this to yes then set enable_refill to no." >> "$geo_conf" printf '%s\n' "restart_nftables=$restart_nftables" >> "$geo_conf" printf '\n' >> "$geo_conf" printf '%s\n' "# Enable geolocation sets to be flushed and refilled after" >> "$geo_conf" printf '%s\n' "# the periodic database update (yes/no). Only enable" >> "$geo_conf" printf '%s\n' "# this after manually testing the refill-sets.nft script" >> "$geo_conf" printf '%s\n' "# as described in the User Guide." >> "$geo_conf" printf '%s\n' "# If you set this to yes then set restart_nftables to no." >> "$geo_conf" printf '%s\n' "enable_refill=$enable_refill" >> "$geo_conf" printf '\n' >> "$geo_conf" printf '%s\n' "# Enable creation of include-all files to include" >> "$geo_conf" printf '%s\n' "# all country sets on older versions of nftables" >> "$geo_conf" printf '%s\n' "# that don't support include wildcards (yes/no)." >> "$geo_conf" printf '%s\n' "enable_include_all=$enable_include_all" >> "$geo_conf" } # Create the refill-sets configuration file /etc/nftables/geo-nft/refill-sets.conf with # all settings commented out. make_refill_config() { if [ -n "$refill_conf" ] && [ ! -s "$refill_conf" ]; then print_line "\n" "Creating refill-sets configuration file:" "\n" "$refill_conf" "\n" printf '%s\n' "# Refill-sets configuration file" > "$refill_conf" printf '%s\n' "# Generated by $script_name $geo_nft_ver" >> "$refill_conf" printf '%s\n' "# Automatically flush and refill geolocation sets." >> "$refill_conf" printf '%s\n' "# The refill-sets.nft nftables script will be" >> "$refill_conf" printf '%s\n' "# auto-generated using information provided here." >> "$refill_conf" printf '%s\n' "# Uncomment any settings below and modify as needed." >> "$refill_conf" printf '%s\n' "# Comment out all lines to disable this feature (default)." >> "$refill_conf" printf '\n\n' >> "$refill_conf" printf '%s\n' "# Specify 'include' lines to add to refill-sets.nft." >> "$refill_conf" printf '%s\n' "# You can add as many include lines as required." >> "$refill_conf" printf '%s\n' "# ==============================================" >> "$refill_conf" printf '%s\n' "#include \"/etc/nftables/geo-nft/include-all.ipv4\"" >> "$refill_conf" printf '%s\n' "#include \"/etc/nftables/geo-nft/include-all.ipv6\"" >> "$refill_conf" printf '%s\n' "#include \"/etc/nftables/geo-nft/countrysets/*\"" >> "$refill_conf" printf '\n\n' >> "$refill_conf" printf '%s\n' "# Specify which sets to flush and fill with refill-sets.nft." >> "$refill_conf" printf '%s\n' "# Sets should be defined (without elements) in nftables.conf." >> "$refill_conf" printf '%s\n' "# Use 'define-ipv4' or 'define-ipv6' at the beginning of each" >> "$refill_conf" printf '%s\n' "# line to define the IP address type. There should be 5 fields" >> "$refill_conf" printf '%s\n' "# on each line separated by spaces. Multiple country codes" >> "$refill_conf" printf '%s\n' "# should be comma separated without spaces between codes as" >> "$refill_conf" printf '%s\n' "# shown below." >> "$refill_conf" printf '%s\n' "#" >> "$refill_conf" printf '%s\n' "# Define-Protocol Table Family Table Name Set Name Country Codes To Fill Set" >> "$refill_conf" printf '%s\n' "# ==================================================================================" >> "$refill_conf" printf '%s\n' "# For 'netdev' table uncomment either or both of the following:" >> "$refill_conf" printf '%s\n' "#define-ipv4 netdev filter geo-netdev4 AD,BI" >> "$refill_conf" printf '%s\n' "#define-ipv6 netdev filter geo-netdev6 AQ,BI" >> "$refill_conf" printf '\n' >> "$refill_conf" printf '%s\n' "# For 'inet' table (IPv4-IPv6) uncomment either or both of the following:" >> "$refill_conf" printf '%s\n' "#define-ipv4 inet filter geo-inet4 AD" >> "$refill_conf" printf '%s\n' "#define-ipv6 inet filter geo-inet6 AQ" >> "$refill_conf" printf '\n' >> "$refill_conf" printf '%s\n' "# For 'ip' table (IPv4 only) uncomment the following:" >> "$refill_conf" printf '%s\n' "#define-ipv4 ip filter geo-ip4 AD" >> "$refill_conf" printf '\n' >> "$refill_conf" printf '%s\n' "# For 'ip6' table (IPv6 only) uncomment the following:" >> "$refill_conf" printf '%s\n' "#define-ipv6 ip6 filter geo-ip6 AD" >> "$refill_conf" printf '\n' >> "$refill_conf" printf '%s\n' "# Add any additional 'define-ipv4' or 'define-ipv6' lines here:" >> "$refill_conf" fi } # Read user settings from the /etc/geo-nft.conf file. If the # file doesn't exist then create it and use default settings. check_config() { if [ -s "$geo_conf" ]; then # The /etc/geo-nft.conf file exist and has a file size greater than zero, so import the settings. # Import the value for enable_ipv4. Remove single/double quotes, tabs and blank spaces, and convert to lowercase. local value=$(grep -Po 'enable_ipv4=\K.*' $geo_conf | sed "s/['\"\t ]//g" | awk '{print tolower($0)}') if [ "$value" = "yes" ] || [ "$value" = "no" ]; then enable_ipv4="$value" else error_log "The 'enable_ipv4' variable not set to yes or no in $geo_conf" \ "Using the default value: $enable_ipv4" fi # Import the value for enable_ipv6. Remove single/double quotes, tabs and blank spaces, and convert to lowercase. value=$(grep -Po 'enable_ipv6=\K.*' $geo_conf | sed "s/['\"\t ]//g" | awk '{print tolower($0)}') if [ "$value" = "yes" ] || [ "$value" = "no" ]; then enable_ipv6="$value" else error_log "The 'enable_ipv6' variable not set to yes or no in $geo_conf" \ "Using the default value: $enable_ipv6" fi # Import the value for base_dir. Remove double quotes and tabs, trailing slashes, leading and # trailing single quotes and blank spaces, and don't change case of pathname. value=$(grep -Po 'base_dir=\K.*' $geo_conf | sed "s/[\"\t]//g; s:/*$::; s/^[' ]*//; s/[' ]*$//") if [ -n "$value" ] && [ -d "$value" ]; then if [ -s "$value/$script_name" ]; then base_dir="$value" else error_log "The base directory defined in $geo_conf doesn't contain '$script_name', exiting..." exit 1 fi else error_log "The base directory defined in $geo_conf doesn't exist." "$value directory not found, exiting..." exit 1 fi # Import the value for restart_nftables. Remove single/double quotes, tabs and blank spaces, and convert to lowercase. value=$(grep -Po 'restart_nftables=\K.*' $geo_conf | sed "s/['\"\t ]//g" | awk '{print tolower($0)}') if [ "$value" = "yes" ] || [ "$value" = "no" ]; then restart_nftables="$value" else error_log "The 'restart_nftables' variable not set to yes or no in $geo_conf" \ "Using the default value: $restart_nftables" fi # Import the value for enable_refill. Remove single/double quotes, tabs and blank spaces, and convert to lowercase. value=$(grep -Po 'enable_refill=\K.*' $geo_conf | sed "s/['\"\t ]//g" | awk '{print tolower($0)}') if [ "$value" = "yes" ] || [ "$value" = "no" ]; then enable_refill="$value" else error_log "The 'enable_refill' variable not set to yes or no in $geo_conf" \ "Using the default value: $enable_refill" fi # Import the value for enable_include_all. Remove single/double quotes, tabs and blank spaces, and convert to lowercase. value=$(grep -Po 'enable_include_all=\K.*' $geo_conf | sed "s/['\"\t ]//g" | awk '{print tolower($0)}') if [ "$value" = "yes" ] || [ "$value" = "no" ]; then enable_include_all="$value" else error_log "The 'enable_include_all' variable not set to yes or no in $geo_conf" \ "Using the default value: $enable_include_all" fi # Check the /etc/geo-nft.conf file version number. Update if it was created with an older version of this script. local conf_ver=$(sed -n -e "s/^# Generated by "$script_name" //p" "$geo_conf") if [ -n "$conf_ver" ]; then if [ "$conf_ver" != "$geo_nft_ver" ]; then if [ -s "$base_dir/$script_name" ]; then cd "$base_dir" print_line "\n" "Updating user configuration file $geo_conf to version: $geo_nft_ver" "\n" make_config fi fi else # If the file version number can't be read then continue on as it's not a critical error. print_line "\n" "Unable to read the file version number: $geo_conf, continuing..." "\n" fi else # The geo-nft.conf file is empty or doesn't exist, so create it. print_line "\n" "Creating user configuration file: $geo_conf" "\n" make_config fi # Set relative path variables. # Directory where the country set files are stored. cc_dir="$base_dir/countrysets" # Current database compressed archive pathname. dbip_gz="$base_dir/$dbip_filename.gz" # Current database pathname. dbip_csv="$base_dir/$dbip_filename" # Include files to load all geolocation sets at once. Required for older # versions of nftables that don't properly support include wildcards. include_file4="$base_dir/include-all.ipv4" include_file6="$base_dir/include-all.ipv6" # Refill-sets configuration file. refill_conf="$base_dir/refill-sets.conf" # Refill-sets nftables script filename. refill_file="$base_dir/refill-sets.nft" } # Verify that a directory exists and is writable, and exit if not. Pass the pathname of a # directory as an argument. Pass a second argument '-c' if the missing directory should be created. check_dir() { if [ -d "$1" ]; then if [ ! -w "$1" ]; then error_log "The $1 directory isn't writable. Exiting..." exit 1 fi else if [ -n "$1" ]; then if [ "$2" = "-c" ]; then print_line "\n" "The $1 directory doesn't exist, creating..." "\n" mkdir -p "$1" fi if [ -d "$1" ]; then if [ ! -w "$1" ]; then error_log "The $1 directory isn't writable. Exiting..." exit 1 fi else error_log "The $1 directory doesn't exist. Exiting..." exit 1 fi else error_log "check_dir: The directory string isn't valid. Exiting..." exit 1 fi fi } # Verify that required programs are available. check_programs() { local reqd_programs="awk curl grep gunzip sed sort stat" for p in $reqd_programs do command -v "$p" > /dev/null 2>&1 if [ ! $? -eq 0 ]; then error_log "This script requires the '$p' program, not found in \$PATH. Exiting..." exit 1 fi done } # Verify at least one of the variables $enable_ipv4 or $enable_ipv6 is set to 'yes'. # Change both settings to 'yes' if neither setting is set to yes (default). check_protocol() { if [ ! "$enable_ipv4" = "yes" ] && [ ! "$enable_ipv6" = "yes" ]; then print_line "\n" "Neither variable 'enable_ipv4' or 'enable_ipv6' is set to 'yes' in /etc/geo-nft.conf." "\n" \ "Check your settings and set at least one of those two variables to 'yes'." "\n" \ "Both settings will default to 'yes' for this run." "\n" enable_ipv4="yes" enable_ipv6="yes" fi } # Check if semantic version number of installed program is at least required minimum version. # Supply 'curr_version' and 'reqd_version' as arguments. version_atleast() { local curr_version=$(printf '%s' "$1" | awk -F. '{printf("%03d%03d%03d\n", $1,$2,$3)}') local reqd_version=$(printf '%s' "$2" | awk -F. '{printf("%03d%03d%03d\n", $1,$2,$3)}') [ "$curr_version" -ge "$reqd_version" ] } # Print a notice on how to include all geolocation sets based on the installed nftables version # reported by the nft program. print_notice() { version_atleast $(nft -v | awk '{gsub(/[^0-9.]/, "", $2); print $2}') "0.9.4" && \ print_line "\n" "Your nftables version is at least version 0.9.4, so you" "\n" \ "can include all geolocation sets in your configuration file" "\n" \ "$refill_conf with:" "\n" \ "include \"$cc_dir/*\"" "\n" || \ print_line "\n" "Your nftables version is less than version 0.9.4, so you" "\n" \ "can include all geolocation sets in your configuration file" "\n" \ "$refill_conf with:" "\n" \ "include \"$include_file4\"" "\n" \ "include \"$include_file6\"" "\n" } # Set user settings to default values. set_defaults() { # Enable ipv4 support ("yes" or "no"). enable_ipv4="yes" # Enable ipv6 support ("yes" or "no"). enable_ipv6="yes" # Enable restarting nftables after a database update ("yes" or "no"). restart_nftables="no" # Enable sets to be flushed/refilled after updating the geolocation database ("yes" or "no"). enable_refill="no" # Enable creation of the include-all.ipv4 and/or include-all.ipv6 files to allow loading # all geolocation set files at once on older versions of nftables. Set to 'no' if # running a version of nftables >= v0.9.4 version_atleast $(nft -v | awk '{gsub(/[^0-9.]/, "", $2); print $2}') "0.9.4" && \ enable_include_all="no" || enable_include_all="yes" } # Auto-generate the nftables script 'refill-sets.nft' from settings in # the configuration file 'refill-sets.conf'. check_refill_config() { if [ -n "$refill_conf" ] && [ -s "$refill_conf" ]; then # The 'refill-sets.conf' file exists and is not empty, so try to import the settings needed to create the # 'refill-sets.nft' script. print_line "\n" "Checking for settings in $refill_conf" "\n" # Create an array to store the output that will be written to the refill-sets.nft script. local refill_sets_array=("#!${nft} -f") refill_sets_array+=("") refill_sets_array+=("#=====================================================") refill_sets_array+=("# Auto-generated by $script_name $geo_nft_ver - Do not modify") refill_sets_array+=("# $(date)") refill_sets_array+=("# $refill_file") refill_sets_array+=("#") refill_sets_array+=("# This script can be manually loaded for testing with:") refill_sets_array+=("# sudo nft -f $refill_file") refill_sets_array+=("#=====================================================") refill_sets_array+=("") while : do # Create an array to store the 'include' statements from the 'refill-sets.conf' file. # Grep lines that begin with 'include' and add only the pathname/filename to the array. mapfile include_array < <(grep ^'include' "$refill_conf" | awk '{ print $2 }') if [ "${#include_array[@]}" -gt 0 ]; then # The 'refill-sets.conf' file has include statements, so verify that they point to files that exist. refill_sets_array+=("# Includes Section") # Read the include lines. while read -r iline && [ -n "$iline" ] do # Remove leading and trailing double quotes, tabs and blank spaces. iline=$(sed -e 's/^[ \t]*//;s/[ \t]*$//;s/\"//g' <<<"$iline") # Separate the pathname and filename. local pname="${iline%/*}" local fname="${iline##*/}" # Verify that the line points to existing files. If the filename contains an asterisk, enclose # only the pathname in double quotes, otherwise enclose the pathname/filename in double quotes. if [ -n "$(grep '*' <<<"$fname")" ]; then if stat -t "$pname"/$fname >/dev/null 2>&1; then # Files exist, so add the line to the array. refill_sets_array+=("include \"$iline\";") else error_log "Bad 'include' line in $refill_conf" "Bad line: $iline" bad_line="yes" break fi else if stat -t "$iline" >/dev/null 2>&1; then # File exists, so add the line to the array. refill_sets_array+=("include \"$iline\";") else error_log "Bad 'include' line in $refill_conf" "Bad line: $iline" bad_line="yes" break fi fi done <<<"${include_array[@]}" else # No 'include' lines found in '$refill-sets.conf', so break out of the loop and skip the automatic # generation of the 'refill-sets.nft' script. no_line="yes" break fi refill_sets_array+=("") refill_sets_array+=("# Defines Section") # Import fields from 'define-ipv4' lines if 'enable_ipv4=yes' and there are no bad or missing lines so far. if [ "$enable_ipv4" = "yes" ] && [ ! "$bad_line" = "yes" ] && [ ! "$no_line" = "yes" ]; then # Create an array to store the 'define-ipv4' lines from the 'refill-sets.conf' file. mapfile define4_array < <(grep ^'define-ipv4' "$refill_conf") if [ "${#define4_array[@]}" -gt 0 ]; then while read -r line; do # Verify that the current line has 5 fields if [ "$(awk '{ print NF }' <<<"$line")" -ne 5 ]; then error_log "Bad 'define-ipv4' line in $refill_conf" \ "Define lines must have 5 fields separated by spaces." \ "Example: define-ipv4 netdev filter geo-netdev4 AD,BI" "Bad line: $line" bad_line="yes" break fi # Extract fields from the current line. local table_family="$(awk '{ print $2 }' <<<"$line")" local table_name="$(awk '{ print $3 }' <<<"$line")" local set_name="$(awk '{ print $4 }' <<<"$line")" local country_codes="$(awk '{ print $5 }' <<<"$line" | sed 's/\,/\n/g')" # Verify that field strings in the current line are not empty. if [ -z "$table_family" ] || [ -z "$table_name" ] || \ [ -z "$set_name" ] || [ -z "$country_codes" ]; then error_log "Unable to read field in $refill_conf" \ "from line: $line" bad_line="yes" break fi # Verify that the current line points to a valid set defined in the main ruleset. nft list set "$table_family" "$table_name" "$set_name" > /dev/null 2>&1 if [ $? -ne 0 ]; then error_log "The following 'define-ipv4' line in $refill_conf" \ "does not point to a valid nftables set defined in your main ruleset (nftables.conf):" \ "$line" bad_line="yes" break fi # Create an array to store country codes read from the current line. local cc4_array=() while read -r cc; do # Capitalize the country code. cc="$(awk '{print toupper($0)}' <<<"$cc")" # Test if the country code is blank. if [ -z "$cc" ]; then error_log "There's a blank country code in your 'define-ipv4' line in $refill_conf." \ "Remove the blank country code from the line shown below. The blank entry will be skipped." \ "Bad line: $line" continue fi # Test if the country code is already in the array (country code repeated in refill-sets.conf list). if [[ "${cc4_array[*]}" =~ (^|[^[:alpha:]])$cc([^[:alpha:]]|$) ]]; then error_log "Country code '$cc' is duplicated in your 'define-ipv4' line in $refill_conf." \ "Remove any duplicates from the line shown below. The duplicate entry will be skipped." \ "Bad line: $line" continue fi # Verify that the country code definition file exists in the countrysets directory. if [ -s "$cc_dir/$cc.ipv4" ]; then cc4_array+=("\$$cc.ipv4") else error_log "Country code $cc specified in $refill_conf" \ "doesn't exist in this months geolocation database." \ "The missing country code was not added to the set." \ "Line: $line" cc_line="yes" continue fi done <<<"$country_codes" if [ "${#cc4_array[@]}" -gt 0 ]; then # Create 'flush' line in the array. refill_sets_array+=("flush set $table_family $table_name $set_name") # Create 'add element' line in the array. if [ "${#cc4_array[@]}" -eq 1 ]; then refill_sets_array+=("$(printf "%s %s" \ "add element $table_family $table_name $set_name" \ "$(printf "%s" "${cc4_array[@]}")")") else refill_sets_array+=("$(printf "%s %s" \ "add element $table_family $table_name $set_name" \ "{ $(printf "%s, " "${cc4_array[@]}")}")") fi else error_log "No country codes can be added to set $set_name" \ "Country code(s) specified in $refill_conf" \ "not found in this months geolocation database." break fi refill_sets_array+=("") done <<<$(printf "%s" "${define4_array[@]}") else # No 'define-ipv4' lines were found in refill-sets.conf, so break out of # the loop and skip the automatic generation of the 'refill-sets.nft' script. no_line4="yes" break fi fi # Import fields from 'define-ipv6' lines if 'enable_ipv6=yes' and there are no bad or missing lines so far. if [ "$enable_ipv6" = "yes" ] && [ ! "$bad_line" = "yes" ] && [ ! "$no_line" = "yes" ]; then # Create an array to store the 'define-ipv6' lines from the 'refill-sets.conf' file. mapfile define6_array < <(grep ^'define-ipv6' "$refill_conf") if [ "${#define6_array[@]}" -gt 0 ]; then while read -r line; do # Verify that the current line has 5 fields if [ "$(awk '{ print NF }' <<<"$line")" -ne 5 ]; then error_log "Bad 'define-ipv6' line in $refill_conf" \ "Define lines must have 5 fields separated by spaces." \ "Example: define-ipv6 netdev filter geo-netdev6 AD,BI" "Bad line: $line" bad_line="yes" break fi # Extract fields from the current line. local table_family="$(awk '{ print $2 }' <<<"$line")" local table_name="$(awk '{ print $3 }' <<<"$line")" local set_name="$(awk '{ print $4 }' <<<"$line")" local country_codes="$(awk '{ print $5 }' <<<"$line" | sed 's/\,/\n/g')" # Verify that field strings in the current line are not empty. if [ -z "$table_family" ] || [ -z "$table_name" ] || \ [ -z "$set_name" ] || [ -z "$country_codes" ]; then error_log "Unable to read field in $refill_conf" \ "from line: $line" bad_line="yes" break fi # Verify that the current line points to a valid set defined in the main ruleset. nft list set "$table_family" "$table_name" "$set_name" > /dev/null 2>&1 if [ $? -ne 0 ]; then error_log "The following 'define-ipv6' line in $refill_conf" \ "does not point to a valid nftables set defined in your main ruleset (nftables.conf):" \ "$line" bad_line="yes" break fi # Create an array to store country codes read from the current line. local cc6_array=() while read -r cc; do # Capitalize the country code. cc="$(awk '{print toupper($0)}' <<<"$cc")" # Test if the country code is blank. if [ -z "$cc" ]; then error_log "There's a blank country code in your 'define-ipv6' line in $refill_conf." \ "Remove the blank country code from the line shown below. The blank entry will be skipped." \ "Bad line: $line" continue fi # Test if the country code is already in the array (country code repeated in refill-sets.conf list). if [[ "${cc6_array[*]}" =~ (^|[^[:alpha:]])$cc([^[:alpha:]]|$) ]]; then error_log "Country code $cc is duplicated in your 'define-ipv6' line in $refill_conf." \ "Remove any duplicates from the line shown below. The duplicate entry will be skipped." \ "Bad line: $line" continue fi # Verify that the country code definition file exists in the countrysets directory. if [ -s "$cc_dir/$cc.ipv6" ]; then cc6_array+=("\$$cc.ipv6") else error_log "Country code $cc specified in $refill_conf" \ "doesn't exist in this months geolocation database." \ "The missing country code was not added to the set." \ "Line: $line" cc_line="yes" continue fi done <<<"$country_codes" if [ "${#cc6_array[@]}" -gt 0 ]; then # Create 'flush' line in the array. refill_sets_array+=("flush set $table_family $table_name $set_name") # Create 'add element' line in the array. if [ "${#cc6_array[@]}" -eq 1 ]; then refill_sets_array+=("$(printf "%s %s" \ "add element $table_family $table_name $set_name" \ "$(printf "%s" "${cc6_array[@]}")")") else refill_sets_array+=("$(printf "%s %s" \ "add element $table_family $table_name $set_name" \ "{ $(printf "%s, " "${cc6_array[@]}")}")") fi else error_log "No country codes can be added to set $set_name" \ "Country code(s) specified in $refill_conf" \ "not found in this months geolocation database." break fi refill_sets_array+=("") done <<<"$(printf "%s" "${define6_array[@]}")" else # No 'define-ipv6' lines were found in refill-sets.conf, so break out of # the loop and skip the automatic generation of the 'refill-sets.nft' script. no_line6="yes" break fi fi # Break out of the main loop after one run. break done # Automatically generate the nftables script 'refill-sets.nft' if there are no bad or missing lines in # the 'refill-sets.conf' file. if [ "$bad_line" = "yes" ]; then # Invalid settings were found in 'refill-sets.conf', so skip the automatic generation of # the 'refill-sets.nft' script. print_line "Invalid settings were found in 'refill-sets.conf'." "\n" \ "Automatic generation of $refill_file will be skipped." "\n" if [ "$enable_refill" = "yes" ]; then print_line "The 'enable_refill' variable will be set to 'no' for this run." "\n" enable_refill="no" fi else if [ "$no_line" = "yes" ]; then # No 'include' lines were found in 'refill-sets.conf', so skip the automatic generation of # the 'refill-sets.nft' script. print_line "No settings found. Skipping automatic generation of:" "\n" "$refill_file" "\n" \ "To use this feature configure settings in $refill_conf" "\n" if [ "$enable_refill" = "yes" ]; then print_line "The 'enable_refill' variable will be set to 'no' for this run." "\n" enable_refill="no" fi elif [ "$no_line4" = "yes" ]; then # No 'define-ipv4' lines were found in 'refill-sets.conf', even though enable_ipv4=yes, # so skip the automatic generation of the 'refill-sets.nft' script. print_line "No 'define-ipv4' lines were found even though enable_ipv4=yes." "\n" \ "Skipping automatic generation of:" "\n" "$refill_file." "\n" \ "To use this feature configure settings in $refill_conf" "\n" if [ "$enable_refill" = "yes" ]; then print_line "The 'enable_refill' variable will be set to 'no' for this run." "\n" enable_refill="no" fi elif [ "$no_line6" = "yes" ]; then # No 'define-ipv6' lines were found in 'refill-sets.conf', even though enable_ipv6=yes, # so skip the automatic generation of the 'refill-sets.nft' script. print_line "No 'define-ipv6' lines were found even though enable_ipv6=yes." "\n" \ "Skipping automatic generation of:" "\n" "$refill_file" "\n" \ "To use this feature configure settings in $refill_conf." "\n" if [ "$enable_refill" = "yes" ]; then print_line "The 'enable_refill' variable will be set to 'no' for this run." "\n" enable_refill="no" fi else if [ "$cc_line" = "yes" ]; then # At least one country code specified in 'refill-sets.conf' was not found in this # month's geolocation database. The remaining country codes were found and imported # successfully. print_line "Remaining settings found and imported successfully." \ "\n" "Automatically generating $refill_file" "\n" else # All settings in the 'refill-sets.conf' file were found and imported successfully. print_line "Settings found and imported successfully." \ "\n" "Automatically generating $refill_file" "\n" fi # Generate the 'refill-sets.nft' script. printf "%s\n" "${refill_sets_array[@]}" > "$refill_file" fi fi else # The refill-sets.conf doesn't exist, so create it. make_refill_config if [ "$enable_refill" = "yes" ]; then print_line "The 'enable_refill' variable will be set to 'no' for this run." "\n" enable_refill="no" fi fi } # Flush and refill geolocation sets after the monthly database update. flush_refill_sets() { # Run the refill script defined by $refill_file above. The 'enable_refill' variable must # be set to "yes" for this to work. if [ "$enable_refill" = "yes" ]; then if [ -n "$refill_file" ] && [ -s "$refill_file" ]; then if [ "$nftables_status" = "active" ]; then print_line "\n" "Refilling updated country-specific sets by running the script:" "\n" print_line "$refill_file" "\n" nft -f "$refill_file" if [ $? -eq 0 ]; then print_line "Refill successful." "\n" else error_log "Refilling geolocation sets failed." \ "See 'systemctl status nftables' and 'journalctl -xe' for details." exit 1 fi else error_log "Unable to refill geolocation sets because nftables is not active, exiting..." exit 1 fi else error_log "Unable to refill geolocation sets, refill script $base_dir/$refill_file not found." \ "Ensure file exists or set 'enable_refill=no' in $geo_conf." exit 1 fi fi } # Download the free geolocation database from db-ip.com # IP Geolocation by DB-IP https://db-ip.com Licensed under # (CC BY-SA 4.0) https://creativecommons.org/licenses/by-sa/4.0/legalcode download_db() { if [ -s "$base_dir/$script_name" ]; then cd "$base_dir" else error_log "The base directory defined in $geo_conf doesn't contain '$script_name', exiting..." exit 1 fi if [ ! -s "$dbip_csv" ]; then print_line "\n" "Downloading the free geolocation database from https://db-ip.com." "\n" print_line "IP Geolocation by DB-IP https://db-ip.com Licensed under" "\n" print_line "(CC BY-SA 4.0) https://creativecommons.org/licenses/by-sa/4.0/legalcode" "\n" "\n" if [ "$silent" = "yes" ]; then curl -f -L -O -s -S "$dbip_url" if [ $? -ne 0 ]; then error_log "Failed to download $dbip_url. Exiting..." exit 1 fi else curl -f -L -O "$dbip_url" if [ $? -ne 0 ]; then error_log "Failed to download $dbip_url. Exiting..." exit 1 fi fi if [ -s "$dbip_gz" ]; then # The download was successful. rm -f dbip-country-lite-*.csv gunzip -f "$dbip_gz" if [ $? -ne 0 ] || [ ! -s "$dbip_csv" ]; then # The gunzip failed so error out. error_log "Failed to unzip $dbip_gz. Exiting..." exit 1 fi else error_log "Failed to download $dbip_url. Exiting..." exit 1 fi else print_line "\n" "The latest database csv file already exists locally; using existing file:" "\n" "$dbip_csv" "\n" fi } # Make the nftables geolocation sets. make_sets_db() { if [ -s "$dbip_csv" ]; then # The geolocation database file exists and has a size greater than zero, so begin the set generation. print_line "\n" "Creating country-specific nftables sets..." "\n" # Create an array to store a list of all valid country codes found in the database csv file. # Filter out only the 'ZZ' country code used for 'no location and owner' since it isn't valid. # https://db-ip.com/faq.php print_line "\n" "Creating a list of all country codes found in the database csv file." "\n" "\n" local cc_list_array=($(awk -F"," '{ print $3 }' "$dbip_csv" | sort -u | sed -e '/ZZ/d')) # Verify that the $cc_list_array list exists and has at least 200 elements. if [ ! "$cc_list_array" ] || [ ${#cc_list_array[@]} -lt 200 ]; then error_log "The country code list 'cc_list_array' is missing or incomplete. Exiting..." exit 1 fi # Create country-specific nftables sets. print_line "Generating nftables geolocation sets in:" "\n" print_line "$cc_dir" "\n" "\n" print_line "This may take a moment, please wait..." "\n" "\n" print_line "Some countries may only have IPv4 addresses or IPv6 addresses." "\n" "\n" # Clear the 'countrysets' directory. rm -f "$cc_dir"/* if [ "$enable_include_all" != "yes" ]; then # The enable_include_all variable is not set to yes, so remove any include-all files if they exist. rm -f "$include_file4" rm -f "$include_file6" fi # Generate an IPv4 set for each country if $enable_ipv4 is set to 'yes' above. if [ "$enable_ipv4" = "yes" ]; then # Create 'include_all4_array' to store data for the 'include_file4' output file. # The output file allows loading all IPv4 country code sets at once. if [ "$enable_include_all" = "yes" ]; then local include_all4_array=() include_all4_array+=("# Generated by $script_name $geo_nft_ver") include_all4_array+=("# $(date)") include_all4_array+=("# Make all IPv4 country code sets available to your ruleset.") include_all4_array+=("# Only needed with versions of nftables <= 0.9.3") include_all4_array+=("# which don't properly support include wildcards.") include_all4_array+=("# Load this file with (omit the leading '#' mark):") include_all4_array+=("#") include_all4_array+=("# include \"$include_file4\"") include_all4_array+=("" "") fi for line in "${cc_list_array[@]}" do # Count the number of elements in the new set file. # If the starting and ending IP address are equal, list them as a single address for nftables compatibility. local element_count4=$(grep "$line" "$dbip_csv" | grep -v : | \ awk -F"," '{ if ($1==$2) print "\t" $1 ","; else print "\t" $1 "," $2 "," fi }' | \ grep -o "," | awk 'END{print NR}') # Verify that the set being generated has at least one element, otherwise skip it. if [ "$element_count4" -eq 0 ]; then rm -f "$cc_dir/$line.ipv4" print_line "No IPv4 addresses in database for country code $line, skipping..." "\n" continue else if [ "$enable_include_all" = "yes" ]; then # The set has elements so add the country code line to the include_all4_array. include_all4_array+=("include \"$cc_dir/$line.ipv4\";") fi fi # Add the file header text and db-ip.com license information. printf '%s\n' "# Generated by $script_name $geo_nft_ver" > "$cc_dir/$line.ipv4" printf '%s\n' "# $(date)" >> "$cc_dir/$line.ipv4" printf '%s\n' "# IP Geolocation by DB-IP https://db-ip.com Licensed under" >> "$cc_dir/$line.ipv4" printf '%s\n' "# (CC BY-SA 4.0) https://creativecommons.org/licenses/by-sa/4.0/legalcode" >> "$cc_dir/$line.ipv4" # Add the element count and country code used to create this set. printf '%s\n' "# Country code used to create this set: $line" >> "$cc_dir/$line.ipv4" printf '%s\n\n' "# Number of elements in this set: $element_count4" >> "$cc_dir/$line.ipv4" # Add the 'define' set line. printf '%s\n' "define $line.ipv4 = {" >> "$cc_dir/$line.ipv4" # Grep the geolocation database file for country code matches for the current $line. # If the starting and ending IP address are equal, list them as a single address for nftables compatibility. # sed removes the comma on the last line of the element list. grep "$line" "$dbip_csv" | grep -v : | \ awk -F"," '{ if ($1==$2) print "\t" $1 ","; else print "\t" $1 "-" $2 "," fi }' | \ sed '$s/,$//' >> "$cc_dir/$line.ipv4" # Add the closing brace to the output file. printf '%s' "}" >> "$cc_dir/$line.ipv4" done if [ "$enable_include_all" = "yes" ]; then # Generate the 'include-all.ipv4' output file. printf "%s\n" "${include_all4_array[@]}" > "$include_file4" fi else rm -f "$cc_dir"/*.ipv4 rm -f "$include_file4" fi # Generate an IPv6 set for each country if $enable_ipv6 is set to 'yes' above. if [ "$enable_ipv6" = "yes" ]; then # Create 'include_all6_array' to store data for the 'include_file6' output file. # The output file allows loading all IPv6 country code sets at once. if [ "$enable_include_all" = "yes" ]; then local include_all6_array=() include_all6_array+=("# Generated by $script_name $geo_nft_ver") include_all6_array+=("# $(date)") include_all6_array+=("# Make all IPv6 country code sets available to your ruleset.") include_all6_array+=("# Only needed with versions of nftables <= 0.9.3") include_all6_array+=("# which don't properly support include wildcards.") include_all6_array+=("# Load this file with (omit the leading '#' mark):") include_all6_array+=("#") include_all6_array+=("# include \"$include_file6\"") include_all6_array+=("" "") fi for line in "${cc_list_array[@]}" do # Count the number of elements in the new set file. # If the starting and ending IP address are equal, list them as a single address for nftables compatibility. local element_count6=$(grep "$line" "$dbip_csv" | grep : | \ awk -F"," '{ if ($1==$2) print "\t" $1 ","; else print "\t" $1 "," $2 "," fi }' | \ grep -o "," | awk 'END{print NR}') # Verify that the set being generated has at least one element, otherwise skip it. if [ "$element_count6" -eq 0 ]; then rm -f "$cc_dir/$line.ipv6" print_line "No IPv6 addresses in database for country code $line, skipping..." "\n" continue else if [ "$enable_include_all" = "yes" ]; then # The set has elements so add the country code line to the include_all6_array. include_all6_array+=("include \"$cc_dir/$line.ipv6\";") fi fi # Add the file header text and db-ip.com license information. printf '%s\n' "# Generated by $script_name $geo_nft_ver" > "$cc_dir/$line.ipv6" printf '%s\n' "# $(date)" >> "$cc_dir/$line.ipv6" printf '%s\n' "# IP Geolocation by DB-IP https://db-ip.com Licensed under" >> "$cc_dir/$line.ipv6" printf '%s\n' "# (CC BY-SA 4.0) https://creativecommons.org/licenses/by-sa/4.0/legalcode" >> "$cc_dir/$line.ipv6" # Add the element count and country code used to create this set. printf '%s\n' "# Country code used to create this set: $line" >> "$cc_dir/$line.ipv6" printf '%s\n\n' "# Number of elements in this set: $element_count6" >> "$cc_dir/$line.ipv6" printf '%s\n' "define $line.ipv6 = {" >> "$cc_dir/$line.ipv6" # Grep the geolocation database file for country code matches for the current $line. # If the starting and ending IP address are equal, list them as a single address for nftables compatibility. grep "$line" "$dbip_csv" | grep : | \ awk -F"," '{ if ($1==$2) print "\t" $1 ","; else print "\t" $1 "-" $2 "," fi }' | \ sed '$s/,$//' >> "$cc_dir/$line.ipv6" # Add the closing brace to the output file. printf '%s' "}" >> "$cc_dir/$line.ipv6" done if [ "$enable_include_all" = "yes" ]; then # Generate the 'include-all.ipv6' output file. printf "%s\n" "${include_all6_array[@]}" > "$include_file6" fi else rm -f "$cc_dir"/*.ipv6 rm -f "$include_file6" fi print_line "\n" "Country set creation complete..." "\n" else error_log "The database file $dbip_csv is missing. Exiting..." exit 1 fi } # Main Function main() { # Start a timer for the script run time. local starttime=$(date +%s) # Verify that the $geo_conf directory exists and is writable. check_dir "${geo_conf%/*}" # Verify that the $errorlog directory exists and is writable. check_dir "${errorlog%/*}" # Verify that required programs are available. check_programs # Verify the status of nftables. check_nftables # Print the current date/time. print_line "\n" "$(date)" "\n" # Print the Geo-nft version number. print_line "\n" "Geolocation for nftables $geo_nft_ver" "\n" # Print the bash version string. print_line "\n" "bash version $BASH_VERSION" "\n" # Print the nftables version string from the nft program. print_line "\n" "$(nft -v)" "\n" # Print the location of the nft program. print_line "\n" "Found 'nft' in: ${nft%/*}" "\n" # Set user default settings. set_defaults # Read user settings from the /etc/geo-nft.conf file, otherwise use defaults. check_config # Verify at least one of the variables 'enable_ipv4' or 'enable_ipv6' is set to 'yes'. check_protocol # Verify that the base directory exists and is writable. check_dir "$base_dir" # Verify that the countrysets directory exists and is writable. Create the directory # if it doesn't exist. check_dir "$cc_dir" -c # Checks are complete so download the geolocation database. download_db # Generate the country-specific nftables sets. make_sets_db # Check the 'refill-sets.conf' file for user settings and create the 'refill_sets.nft' script if # settings are valid. if [ "$nftables_status" = "active" ]; then check_refill_config fi # Flush and refill updated geolocation sets by running the refill script defined by '$refill_file' # above. The '$enable_refill' variable must be set to "yes" for this to work, and the # restart_nftables setting will be set to no as the two settings are mutually exclusive. if [ "$enable_refill" = "yes" ]; then restart_nftables="no" flush_refill_sets fi # Enable restarting nftables after a database update. The '$restart_nftables' variable must # be set to "yes" for this to work, and the enable_refill setting must be set to no as the # two settings are mutually exclusive. if [ "$restart_nftables" = "yes" ]; then print_line "\n" "Restarting nftables..." "\n" systemctl restart nftables > /dev/null 2>&1 if [ $? -eq 0 ]; then print_line "Restart successful" "\n" else error_log "Restarting nftables failed." "Check 'systemctl status nftables'." \ "Also check 'journalctl -xe' for additional details." exit 1 fi fi # Print a notice on how to include all geolocation sets based on the installed nftables version. print_notice # Display the script run time. print_line "\n" "Script run time: $(($(date +%s) - $starttime))s" "\n" "\n" # Print status even if running silent so there's an entry in the system log. if [ "$silent" = "yes" ]; then printf '%s\n' "$script_name: Finished successfully." else print_line "Finished!" "\n" "\n" fi } main "$@" exit 0