Block ALL Non US Traffic

Complete Fail2Ban Uninstall Guidefail2banRemove Fail2Ban and all configuration files from Ubuntu Server for a clean system. Fail2Ban Setup Guide for Ubuntu ServerSectionCreate a custom action that bans entire /24 subnets (256 IPs) instead of individual IPs:

This section covers host firewalls on Linux servers. The main page is a Bash script that rebuilds the iptables INPUT chain with a default-deny policy, allows loopback, private networks and established connections, downloads the list of IPv4 blocks that ARIN has allocated to US organisations, and accepts traffic from those blocks only. Below the script you will find how the rule chain is evaluated, an important rule-ordering caveat, a faster ipset version, operational pitfalls, and the related Fail2Ban uninstall guide.

1. The script


#!/bin/bash

# Enable error checking and debugging
set -e
set -x

# Function to log messages
log_message() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}

# Function to check if command exists
command_exists() {
    command -v "$1" >/dev/null 2>&1
}

# Check for required tools
for cmd in curl iptables awk grep sort; do
    if ! command_exists "$cmd"; then
        log_message "Error: Required command '$cmd' not found"
        exit 1
    fi
done

log_message "Starting firewall configuration..."

# Create temporary files with cleanup trap
TEMP_DIR=$(mktemp -d)
RAW_IPS_FILE="${TEMP_DIR}/raw_ips"
PROCESSED_IPS_FILE="${TEMP_DIR}/processed_ips"
MERGED_IPS_FILE="${TEMP_DIR}/merged_ips"

trap 'rm -rf "${TEMP_DIR}"' EXIT

# Backup existing rules
if [ -d "/etc/iptables" ]; then
    cp /etc/iptables/rules.v4 "/etc/iptables/rules.v4.backup.$(date +%Y%m%d_%H%M%S)" 2>/dev/null || true
fi

log_message "Setting up basic firewall rules..."

# Clear existing rules
iptables -F
iptables -X

# Set default policies
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT

# Allow loopback
iptables -A INPUT -i lo -j ACCEPT

# Allow all internal network traffic (adjusted to be more inclusive)
iptables -A INPUT -s 10.0.0.0/8 -j ACCEPT     # Private network
iptables -A INPUT -s 172.16.0.0/12 -j ACCEPT  # Private network
iptables -A INPUT -s 192.168.0.0/16 -j ACCEPT # Private network

# Allow established connections
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# Allow specific services (adjust ports as needed)
iptables -A INPUT -p tcp --dport 22   -j ACCEPT # SSH
iptables -A INPUT -p tcp --dport 3000 -j ACCEPT
iptables -A INPUT -p tcp --dport 3389 -j ACCEPT # RDP
iptables -A INPUT -p tcp --dport 443  -j ACCEPT # HTTPS
iptables -A INPUT -p tcp --dport 80   -j ACCEPT # HTTP
iptables -A INPUT -p tcp --dport 8333 -j ACCEPT
iptables -A INPUT -p udp --dport 53   -j ACCEPT # DNS

log_message "Downloading US IP ranges..."
if ! curl -s "https://ftp.ripe.net/pub/stats/arin/delegated-arin-extended-latest" | \
    grep "^arin|.*|ipv4|.*|allocated|.*" | \
    grep -i "US|" | \
    awk -F'|' '{print $4"/"32-log($5)/log(2)}' > "$RAW_IPS_FILE"; then
    log_message "Warning: Failed to download IP ranges, using basic configuration"
fi

# Process IP ranges if download was successful
if [ -s "$RAW_IPS_FILE" ]; then
    log_message "Processing IP ranges..."
    while IFS= read -r ip_range; do
        if [[ $ip_range =~ ^([0-9]{1,3}\.[0-9]{1,3})\.[0-9]{1,3}\.[0-9]{1,3}/[0-9]{1,2}$ ]]; then
            echo "${BASH_REMATCH[1]}.0.0/16"
        fi
    done < "$RAW_IPS_FILE" | sort -u > "$PROCESSED_IPS_FILE"

    # Add US IP ranges
    while IFS= read -r ip_range; do
        if [[ $ip_range =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/[0-9]{1,2}$ ]]; then
            log_message "Adding rule for $ip_range"
            iptables -A INPUT -s "$ip_range" -j ACCEPT
        fi
    done < "$PROCESSED_IPS_FILE"
fi

# Enable ICMP (ping)
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
iptables -A INPUT -p icmp --icmp-type echo-reply -j ACCEPT

# Log dropped packets
iptables -A INPUT -j LOG --log-prefix "DROPPED_PACKET: " --log-level 4

# Final drop rule
iptables -A INPUT -j DROP

# Save rules
if [ -d "/etc/iptables" ]; then
    iptables-save > /etc/iptables/rules.v4
    log_message "Rules saved to /etc/iptables/rules.v4"
else
    log_message "Warning: /etc/iptables directory not found, rules not saved permanently"
fi

log_message "Verifying rules..."
iptables -L INPUT -n -v

log_message "Firewall configuration complete."

2. How packets traverse the rules

INPUT chain evaluation: first match wins Incoming packet INPUT chain lo, 10/8, 172.16/12, 192.168/16 ACCEPT ESTABLISHED, RELATED ACCEPT tcp 22, 80, 443 ... ACCEPT from anywhere US /16 ranges ACCEPT LOG prefix DROPPED_PACKET then DROP

iptables evaluates the rules of a chain from top to bottom and stops at the first rule whose match succeeds; the chain policy (-P INPUT DROP) applies only if nothing matched. The script therefore builds an ordered list: trusted interfaces and private ranges, return traffic for connections the server opened, the service ports, then one ACCEPT per US network block, then a LOG rule and a final explicit DROP.

The US list is built from ARIN's delegated-arin-extended-latest file, whose lines look like arin|US|ipv4|23.0.0.0|65536|...|allocated|.... The awk step converts the address count to a prefix length with 32 - log2(count), and the following loop widens every block to its enclosing /16 and de-duplicates, which cuts tens of thousands of entries down to a few thousand rules at the cost of allowing some non-US neighbours.

Important: because the service-port rules (22, 80, 443, 3000, 3389, 8333, 53) accept from any source and sit above the country rules, the geo filter does not restrict those ports at all; it only affects other ports. To actually limit SSH or RDP to US addresses, remove those port rules and accept the ports inside the country match instead, as in the ipset version below.

3. A faster version with ipset

One set lookup instead of thousands of rules Download ARIN list US ipv4 blocks ipset hash:net add each CIDR swap into us_nets atomic update 1 iptables rule -m set --match-set

Thousands of individual rules are evaluated linearly for every new connection. An ipset stores all networks in a hash structure that is checked in roughly constant time, and the set can be rebuilt and swapped atomically without touching the iptables rules.

#!/bin/bash
set -euo pipefail
ipset create us_nets hash:net -exist
ipset create us_nets_new hash:net -exist
ipset flush us_nets_new
curl -s https://ftp.arin.net/pub/stats/arin/delegated-arin-extended-latest |
  awk -F'|' '$2=="US" && $3=="ipv4" && ($7=="allocated"||$7=="assigned") {
       n=$5; p=32; while (n>1) {n/=2; p--}; print $4"/"p }' |
  while read -r net; do ipset add us_nets_new "$net" -exist; done
ipset swap us_nets_new us_nets
ipset destroy us_nets_new

iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -m set --match-set us_nets src -j ACCEPT
iptables -A INPUT -p tcp -m multiport --dports 80,443 -j ACCEPT   # public web
iptables -A INPUT -j DROP
ipset save > /etc/ipset.conf

Blocks whose size is not a power of two are rounded to the nearest prefix here; a stricter script would split them into exact CIDRs. Persist the set across reboots with ipset save/restore or the ipset-persistent package, loaded before the iptables rules.

4. Pitfalls

Complete Fail2Ban uninstall guidefail2banFail2Ban bans individual abusive IPs from log patterns, which complements a country-level allow list. Security overviewSection Dockercontainersnote that published container ports add their own iptables rules.