πŸ‡ΏπŸ‡¦ cntgate - script to block access to all countries except South Africa for a host πŸ“œ

πŸ‡ΏπŸ‡¦ cntgate - script to block access to all countries except South Africa for a host πŸ“œ

Script to accept only South African connections on a Linux host using nftables

Β·

5 min read

This is a script to all access to a Linux host except if it is from South Africa:

#!/bin/bash
# Extra cntgate: A country blocking script by r00igev@@r/mybroadband
# Version 0.0.4 February 2024
# usage extra-cntgate.sh <configuration file>
# eg: extra-cntgate.sh /etc/extra/extra-cntgate.conf
# Location: /usr/local/sbin/extra-cntgate.sh

PATH=/usr/sbin:/usr/local/bin:/usr/bin:/bin:

function exists() { command -v "$1" >/dev/null 2>&1 ; }

# Check that configuration file has been specified
if [[ -z "$1" ]]; then
  echo "Error: please specify a configuration file, e.g. $0 /etc/extra/extra-cntgate.conf"
  exit 1
fi

# Shellcheck source=extra-cntgate.conf
if ! source "$1"; then
  echo "Error: can't load configuration file $1"
  exit 1
fi

# Check if all commands exist
if ! exists curl && exists egrep && exists grep && exists nft && exists sed && exists sort && exists wc && exists iprange && figlet ; then
  echo >&2 "Error: searching PATH fails to find executables among: curl egrep grep nft sed sort wc iprange figlet"
  exit 1
fi

figlet -f slant Extra cntgate
echo "Extra enabled edge. Driving SD-WAN adoption in South Africa! https://fusionsdwan.co.za"

# Check if BLOCK_POLICY is defined
if [ -z "$BLOCK_POLICY" ]; then
  echo "Error: BLOCK_POLICY is not defined. Please set the BLOCK_POLICY variable."
  exit 1
fi

# Do CIDR optimization if set
DO_OPTIMIZE_CIDR=no
if exists iprange && [[ ${OPTIMIZE_CIDR:-yes} != no ]]; then
  DO_OPTIMIZE_CIDR=yes
fi

# Remove comments from exceptions files
if [ -f "$EXTERNAL_BLOCKLIST_EXCEPTIONS" ]; then
   EXTERNAL_EXCEPTIONS_TMP=$(mktemp)
   for exception in $(sed -r -e 's/\s*#.*$//;/^$/d' "$EXTERNAL_BLOCKLIST_EXCEPTIONS")
      do
         exception_array+=( "$exception" )
         echo $exception >> $EXTERNAL_EXCEPTIONS_TMP
   done
   echo "Created temporary exceptions at $EXTERNAL_EXCEPTIONS_TMP."
fi

# Create the nftables set if needed (or abort if does not exist and FORCE=no)
# Check if the template file exists
if [ ! -f "$NFT_TEMPLATE" ]; then
  echo "Error: Template file not found at $NFT_TEMPLATE."
  exit 1
fi

# Use the nft command to delete the table
nft delete table ip "$BLOCKLIST_NAME"redirect 2>/dev/null
nft delete table inet "$BLOCKLIST_NAME" 2>/dev/null

echo "nftables table '$BLOCKLIST_NAME' dropped."

# Read the template file, replaces ${BLOCK_POLICY} with the actual value, and create the nftables table
nft -f <(sed -e "s/\${BLOCK_POLICY}/$BLOCK_POLICY/g" -e "s/\${WAN_IF}/$WAN_IF/g" -e "s/\${CNT_CACHE}/$CNT_CACHE/g" "$NFT_TEMPLATE")

echo "nftables table '$BLOCKLIST_NAME' with block policy $BLOCK_POLICY."

BLOCKLIST_TMP=$(mktemp)
echo -n "Downloading blocklists:"
for i in "${BLOCKLISTS[@]}"
do
  IP_TMP=$(mktemp)
  (( HTTP_RC=$(curl -L -A "extra/script/mybroadband" --connect-timeout 10 --max-time 10 -o "$IP_TMP" -s -w "%{http_code}" "$i") ))
  if (( HTTP_RC == 200 || HTTP_RC == 302 || HTTP_RC == 0 )); then # "0" because file:/// returns 000
    command grep -Po '^(?:\d{1,3}\.){3}\d{1,3}(?:/\d{1,2})?' "$IP_TMP" | sed -r 's/^0*([0-9]+)\.0*([0-9]+)\.0*([0-9]+)\.0*([0-9]+)$/\1.\2.\3.\4/' >> "$BLOCKLIST_TMP"
    [[ ${VERBOSE:-yes} == yes ]] && echo -n "."
  elif (( HTTP_RC == 503 )); then
    echo -e "\\nUnavailable (${HTTP_RC}): $i"
  else
    echo >&2 -e "\\nWarning: curl returned HTTP response code $HTTP_RC for URL $i"
  fi
  rm -f "$IP_TMP"
done

# Optimize CIDR
if [[ ${OPTIMIZE_CIDR} == yes ]]; then
  if [[ ${VERBOSE:-no} == yes ]]; then
    echo -e "\\nAddresses before CIDR optimization: $(wc -l "$BLOCKLIST_TMP" | cut -d' ' -f1)"
  fi
  < "$BLOCKLIST_TMP" iprange --optimize - > "$BLOCKLIST" # 2>/dev/null
  if [[ ${VERBOSE:-no} == yes ]]; then
    echo "Addresses after CIDR optimization:  $(wc -l "$BLOCKLIST" | cut -d' ' -f1)"
  fi
fi

rm -f "$BLOCKLIST_TMP"

echo -n "Populating nft cntgate:"

# Define the name of the set and the filename containing IPs/subnets
set_name="hexceptions-v4"
blocklist_file="$EXTERNAL_EXCEPTIONS_TMP"
# Read the internal exceptions file line by line
while IFS= read -r line; do
  # Add the IP/subnet to the nftables set 2>/dev/null
  nft add element inet cntgate "$set_name" { $line }
  # Increment the count
done < "$blocklist_file"
rm "$EXTERNAL_EXCEPTIONS_TMP"

# Define the name of the set and the filename containing IPs/subnets
set_name="cntlist-v4"
blocklist_file="$BLOCKLIST"
# Count for printing dots
count=0
# Number of IPs to add in each batch - too big and nft will not load it
batch_size=512
# Initialize a string to store IPs with commas
ip_batch=""
# Read the blocklist file line by line
while IFS= read -r line; do
  # Add the IP to the batch with a comma separator
  if [ -z "$ip_batch" ]; then
    ip_batch="$line"
  else
    ip_batch="$ip_batch,$line"
  fi
  # If the batch size is reached, add IPs and print a dot
  if (( count % batch_size == batch_size - 1 )); then
    nft add element inet cntgate "$set_name" { $ip_batch } 2>/dev/null
    ((count += 1))
    echo -n "."
    ip_batch="" # Clear the batch
  fi
  ((count++))
done < "$blocklist_file"
# Add any remaining IPs in the last batch
if [ -n "$ip_batch" ]; then
  nft add element inet cntgate "$set_name" { $ip_batch } 2>/dev/null
  ((count++))
  echo -n "."
fi

if [[ ${VERBOSE:-no} == yes ]]; then
   echo "cntgate completed. $count IPs/subnets added to $set_name."
fi

The configuration file:

# Location: /etc/extra/extra-cntgate.conf
BLOCKLIST_NAME=cntgate # Table used within nftables
BLOCKLIST_TMP=${BLOCKLIST_NAME}-tmp
BLOCKLIST_DIR=/etc/extra
BLOCKLIST=${BLOCKLIST_DIR}/extra-cntgate.list
EXTERNAL_BLOCKLIST_EXCEPTIONS=${BLOCKLIST_DIR}/external-${BLOCKLIST_NAME}.exceptions
NFT_TEMPLATE=${BLOCKLIST_DIR}/nft-cntgate.template # Template file for nftables rules

# Interfaces
BLOCK_INTERFACE=eth0
WAN_IF=eth0

VERBOSE=yes # Set to no for cron jobs, default to yes
FORCE=yes # Will create the nft table if it does not already exist
OPTIMIZE_CIDR=yes # Optimize block list by aggregating overlapping subnets

# Block policy: 'accept', 'drop' or 'reject', default: 'drop'
BLOCK_POLICY=accept

# cnt Blocklists of IPs being accepted
BLOCKLISTS=(
    "file://${BLOCKLIST_DIR}/${BLOCKLIST_NAME}-custom.list" # Optional, for your personal nemeses
    "https://raw.githubusercontent.com/ipverse/rir-ip/master/country/za/ipv4-aggregated.txt" # ZA Country list
)
MAXELEM=131072

The exceptions file (IP being excepted - same as whitelist - these addresses will always be accepted):

# Location: /etc/extra/external-cntgate.exceptions
1.1.1.0/24

The nft template file named nft-cntgate.template is also located in /etc/extra

table inet cntgate {
        set hexceptions-v4 {
                type ipv4_addr
                flags interval
                auto-merge
        }
        set hexceptions-v6 {
                type ipv6_addr
                flags interval
                auto-merge
        }
        set cntlist-v4 {
                type ipv4_addr
                flags interval
                auto-merge
        }
        set cntlist-v6 {
                type ipv6_addr
                flags interval
                auto-merge
        }
        chain input {
                type filter hook input priority -1; policy accept;
                iifname lo accept
                ct state established,related accept
                iifname "${WAN_IF}" ip saddr @hexceptions-v4 counter accept
                iifname "${WAN_IF}" ip6 saddr @hexceptions-v6 counter accept
                iifname "${WAN_IF}" ip saddr @cntlist-v4 counter ${BLOCK_POLICY}
                iifname "${WAN_IF}" ip6 saddr @cntlist-v6 counter ${BLOCK_POLICY}
                iifname "${WAN_IF}" counter drop
        }
}

You can do a daily update using this crontab -e:

#
# m h  dom mon dow   command
@daily  /usr/local/sbin/extra-cntgate.sh /etc/extra/extra-cntgate.conf

Ronald works connecting Internet inhabiting things at Fusion Broadband.

Β