diff --git a/Linux/geoip-firewall.sh b/Linux/geoip-firewall.sh new file mode 100644 index 0000000..a4691cc --- /dev/null +++ b/Linux/geoip-firewall.sh @@ -0,0 +1,699 @@ +#!/bin/bash +# === 🛠️ DEFAULT CONFIGURATION === +DEFAULT_COUNTRIES=("gb") # Default: UK +DEFAULT_MANUAL_IPS=() # e.g., "203.0.113.10" "198.51.100.0/24" +DEFAULT_TCP_PORTS=() # Empty means all TCP ports +DEFAULT_UDP_PORTS=() # Empty means all UDP ports +IPSET_SAVE_PATH="/etc/ipset.conf" +GEOIP_DIR="/usr/share/xt_geoip" +IPSET_NAME="geo-allowed" +CRON_JOB_PATH="/etc/cron.daily/update-xt-geoip" +SYSTEMD_IPSET_SERVICE="/etc/systemd/system/ipset-restore.service" +BLOCKED_IP_LOG="/var/log/geo-blocked-ips.log" +SCRIPT_NAME="$(basename "$0")" +IP_SOURCE="ripe" # Default: RIPE (options: ripe, both) +MAXMIND_YOUR_ACCOUNT_ID="" # Replace with your MaxMind Account ID if using MaxMind +MAXMIND_LICENSE_KEY="" # MaxMind license key for GeoLite2 +[ -n "$MAXMIND_LICENSE_KEY" ] && [ -n "$MAXMIND_YOUR_ACCOUNT_ID" ] && IP_SOURCE="both" # if Maxmind details provided - using Ripe and Max +GEOIPUPDATE_AVAILABLE=false +command -v geoipupdate >/dev/null 2>&1 && GEOIPUPDATE_AVAILABLE=true + +# === Runtime Config === +COUNTRIES=("${DEFAULT_COUNTRIES[@]}") +MANUAL_IPS=("${DEFAULT_MANUAL_IPS[@]}") +TCP_PORTS=("${DEFAULT_TCP_PORTS[@]}") +UDP_PORTS=("${DEFAULT_UDP_PORTS[@]}") +REMOVE=false +STATUS=false +IPV6=false + +# === Help === +usage() { + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " --countries gb,us Comma-separated list of country codes to allow" + echo " --manual-ips ip1,ip2 Comma-separated list of manual IPs/ranges to allow" + echo " --tcp-ports 25,587 Restrict GeoIP filtering to specific TCP ports (default: all)" + echo " --udp-ports 53,123 Restrict GeoIP filtering to specific UDP ports (default: all)" + echo " --ip-source ripe|both IP list source (default: ripe)" + echo " --maxmind-key KEY MaxMind license key for GeoLite2" + echo " --ipv6 Enable IPv6 GeoIP filtering" + echo " --status Check GeoIP configuration status" + echo " --remove Remove all rules and ipset" + echo " -h, --help Show this help message" + exit 1 +} + +# === Parse CLI Arguments === +while [[ "$#" -gt 0 ]]; do + case "$1" in + --countries) IFS=',' read -r -a COUNTRIES <<< "$2"; shift ;; + --manual-ips) IFS=',' read -r -a MANUAL_IPS <<< "$2"; shift ;; + --tcp-ports) IFS=',' read -r -a TCP_PORTS <<< "$2"; shift ;; + --udp-ports) IFS=',' read -r -a UDP_PORTS <<< "$2"; shift ;; + --ip-source) IP_SOURCE="$2"; shift ;; + --maxmind-id) MAXMIND_YOUR_ACCOUNT_ID="$2"; [ -n "$MAXMIND_LICENSE_KEY" ] && [ -n "$MAXMIND_YOUR_ACCOUNT_ID" ] && IP_SOURCE="both"; shift ;; + --maxmind-key) MAXMIND_LICENSE_KEY="$2"; [ -n "$MAXMIND_LICENSE_KEY" ] && [ -n "$MAXMIND_YOUR_ACCOUNT_ID" ] && IP_SOURCE="both"; shift ;; + --ipv6) IPV6=true ;; + --status) STATUS=true ;; + --remove) REMOVE=true ;; + -h|--help) usage ;; + *) echo "ERROR: Unknown option: $1"; usage ;; + esac + shift +done + +# === Error Handling === +check_command() { + command -v "$1" >/dev/null 2>&1 || { echo "ERROR: $1 is required but not installed."; exit 1; } +} + +# === Convert IP Range to CIDR === +convert_range_to_cidr() { + local range="$1" + local start_ip end_ip + IFS='-' read -r start_ip end_ip <<< "$range" + if command -v ipcalc >/dev/null 2>&1; then + ipcalc -r "$start_ip" "$end_ip" | grep -oE '[0-9.]+/[0-9]+' || { echo "WARNING: Failed to convert range $range to CIDR. Skipping."; return 1; } + else + echo "WARNING: ipcalc not installed. Cannot convert range $range to CIDR. Skipping." + return 1 + fi +} + +# === Validate Inputs === +validate_country_code() { + local cc="$1" + if [[ ! "$cc" =~ ^[a-zA-Z]{2}$ ]]; then + echo "ERROR: Invalid country code: $cc. Must be a 2-letter ISO code." + exit 1 + fi +} + +validate_ip() { + local ip="$1" + if [[ "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}(/[0-9]{1,2})?$ ]]; then + return 0 + elif [[ "$IPV6" = true && "$ip" =~ ^[0-9a-fA-F:]+(/[0-9]{1,3})?$ ]]; then + return 0 + else + echo "ERROR: Invalid IP or CIDR: $ip" + exit 1 + fi +} + +# === Detect LAN Subnets === +detect_lan_subnets() { + echo "INFO: Detecting LAN subnets..." + LAN_SUBNETS=() + for iface in $(ip link show | grep -E '^[0-9]+:.*state UP' | cut -d: -f2 | awk '{print $1}'); do + subnets=$(ip addr show "$iface" | grep -oE 'inet [0-9.]+/[0-9]+' | awk '{print $2}') + for subnet in $subnets; do + LAN_SUBNETS+=("$subnet") + echo "INFO: Detected LAN subnet: $subnet" + done + if [ "$IPV6" = true ]; then + ipv6_subnets=$(ip addr show "$iface" | grep -oE 'inet6 [0-9a-fA-F:]+/[0-9]+' | awk '{print $2}') + for subnet in $ipv6_subnets; do + LAN_SUBNETS+=("$subnet") + echo "INFO: Detected LAN IPv6 subnet: $subnet" + done + fi + done +} + +# === Cleanup Function === +remove_firewall_rules() { + echo "INFO: Removing firewalld rules..." + + # Remove custom chain for blocked IPs + iptables -F GEO_BLOCK 2>/dev/null || true + iptables -X GEO_BLOCK 2>/dev/null || true + ip6tables -F GEO_BLOCK 2>/dev/null || true + ip6tables -X GEO_BLOCK 2>/dev/null || true + + # TCP + if [ ${#TCP_PORTS[@]} -eq 0 ]; then + firewall-cmd --permanent --direct --remove-rule ipv4 filter INPUT 0 \ + -p tcp -m set ! --match-set "$IPSET_NAME" src -j GEO_BLOCK 2>/dev/null || true + firewall-cmd --permanent --direct --remove-rule ipv4 filter INPUT 0 \ + -p tcp -m set ! --match-set "$IPSET_NAME" src -j DROP 2>/dev/null || true + else + for port in "${TCP_PORTS[@]}"; do + firewall-cmd --permanent --direct --remove-rule ipv4 filter INPUT 0 \ + -p tcp --dport "$port" -m set ! --match-set "$IPSET_NAME" src -j GEO_BLOCK 2>/dev/null || true + firewall-cmd --permanent --direct --remove-rule ipv4 filter INPUT 0 \ + -p tcp --dport "$port" -m set ! --match-set "$IPSET_NAME" src -j DROP 2>/dev/null || true + done + fi + + # UDP + if [ ${#UDP_PORTS[@]} -eq 0 ]; then + firewall-cmd --permanent --direct --remove-rule ipv4 filter INPUT 0 \ + -p udp -m set ! --match-set "$IPSET_NAME" src -j GEO_BLOCK 2>/dev/null || true + firewall-cmd --permanent --direct --remove-rule ipv4 filter INPUT 0 \ + -p udp -m set ! --match-set "$IPSET_NAME" src -j DROP 2>/dev/null || true + else + for port in "${UDP_PORTS[@]}"; do + firewall-cmd --permanent --direct --remove-rule ipv4 filter INPUT 0 \ + -p udp --dport "$port" -m set ! --match-set "$IPSET_NAME" src -j GEO_BLOCK 2>/dev/null || true + firewall-cmd --permanent --direct --remove-rule ipv4 filter INPUT 0 \ + -p udp --dport "$port" -m set ! --match-set "$IPSET_NAME" src -j DROP 2>/dev/null || true + done + fi + + # IPv6 + if [ "$IPV6" = true ]; then + IPSET_NAME_V6="geo-allowed-v6" + if [ ${#TCP_PORTS[@]} -eq 0 ]; then + firewall-cmd --permanent --direct --remove-rule ipv6 filter INPUT 0 \ + -p tcp -m set ! --match-set "$IPSET_NAME_V6" src -j GEO_BLOCK 2>/dev/null || true + firewall-cmd --permanent --direct --remove-rule ipv6 filter INPUT 0 \ + -p tcp -m set ! --match-set "$IPSET_NAME_V6" src -j DROP 2>/dev/null || true + else + for port in "${TCP_PORTS[@]}"; do + firewall-cmd --permanent --direct --remove-rule ipv6 filter INPUT 0 \ + -p tcp --dport "$port" -m set ! --match-set "$IPSET_NAME_V6" src -j GEO_BLOCK 2>/dev/null || true + firewall-cmd --permanent --direct --remove-rule ipv6 filter INPUT 0 \ + -p tcp --dport "$port" -m set ! --match-set "$IPSET_NAME_V6" src -j DROP 2>/dev/null || true + done + fi + if [ ${#UDP_PORTS[@]} -eq 0 ]; then + firewall-cmd --permanent --direct --remove-rule ipv6 filter INPUT 0 \ + -p udp -m set ! --match-set "$IPSET_NAME_V6" src -j GEO_BLOCK 2>/dev/null || true + firewall-cmd --permanent --direct --remove-rule ipv6 filter INPUT 0 \ + -p udp -m set ! --match-set "$IPSET_NAME_V6" src -j DROP 2>/dev/null || true + else + for port in "${UDP_PORTS[@]}"; do + firewall-cmd --permanent --direct --remove-rule ipv6 filter INPUT 0 \ + -p udp --dport "$port" -m set ! --match-set "$IPSET_NAME_V6" src -j GEO_BLOCK 2>/dev/null || true + firewall-cmd --permanent --direct --remove-rule ipv6 filter INPUT 0 \ + -p udp --dport "$port" -m set ! --match-set "$IPSET_NAME_V6" src -j DROP 2>/dev/null || true + done + fi + ipset flush "$IPSET_NAME_V6" 2>/dev/null || true + ipset destroy "$IPSET_NAME_V6" 2>/dev/null || true + fi + + echo "INFO: Flushing and destroying ipset..." + ipset flush "$IPSET_NAME" 2>/dev/null || true + ipset destroy "$IPSET_NAME" 2>/dev/null || true + + echo "INFO: Removing cron job at $CRON_JOB_PATH" + rm -f "$CRON_JOB_PATH" + + echo "INFO: Removing ipset persistence configuration" + rm -f "$IPSET_SAVE_PATH" + systemctl disable ipset-restore.service 2>/dev/null || true + rm -f "$SYSTEMD_IPSET_SERVICE" + + echo "INFO: Removing blocked IP log" + rm -f "$BLOCKED_IP_LOG" + + echo "INFO: Reloading firewalld..." + firewall-cmd --reload || { echo "ERROR: Failed to reload firewalld."; exit 1; } + + echo "INFO: Firewall rules and GeoIP ipset removed." +} + +# === Status Check === +check_status() { + echo "=== GeoIP Firewall Status ===" + echo "IP Source: $IP_SOURCE" + echo "MaxMind geoipupdate Available: $GEOIPUPDATE_AVAILABLE" + echo "Countries Allowed: ${COUNTRIES[*]}" + echo "Manual IPs: ${MANUAL_IPS[*]:-None}" + echo "TCP Ports: ${TCP_PORTS[*]:-All}" + echo "UDP Ports: ${UDP_PORTS[*]:-All}" + echo "IPv6 Enabled: $IPV6" + echo "ipset Contents:" + ipset list "$IPSET_NAME" 2>/dev/null || echo " ipset $IPSET_NAME not found" + if [ "$IPV6" = true ]; then + ipset list "$IPSET_NAME_V6" 2>/dev/null || echo " ipset $IPSET_NAME_V6 not found" + fi + echo "Firewall Rules:" + firewall-cmd --direct --get-all-rules + echo "Blocked IPs (see $BLOCKED_IP_LOG):" + if [ -f "$BLOCKED_IP_LOG" ]; then + cat "$BLOCKED_IP_LOG" + else + echo " No blocked IPs logged yet." + fi + exit 0 +} + +if [ "$STATUS" = true ]; then + check_status +fi + +if [ "$REMOVE" = true ]; then + remove_firewall_rules + exit 0 +fi + +# === Validate Inputs === +for cc in "${COUNTRIES[@]}"; do + validate_country_code "$cc" +done +for ip in "${MANUAL_IPS[@]}"; do + validate_ip "$ip" +done + +# === Detect Package Manager === +if command -v apt >/dev/null 2>&1; then + PKG_MANAGER="apt" +elif command -v yum >/dev/null 2>&1; then + PKG_MANAGER="yum" +elif command -v dnf >/dev/null 2>&1; then + PKG_MANAGER="dnf" +else + echo "ERROR: No supported package manager (apt/yum/dnf) found." + exit 1 +fi + +# === Install Dependencies === +echo "INFO: Installing dependencies..." +if [ "$PKG_MANAGER" = "apt" ]; then + apt update && apt install -y ipset xtables-addons-common libtext-csv-xs-perl wget curl jq ipcalc || { echo "ERROR: Failed to install dependencies."; exit 1; } + if [ "$IP_SOURCE" = "both" ] && [ -n "$MAXMIND_LICENSE_KEY" ]; then + apt install -y geoipupdate || echo "WARNING: geoipupdate not found in repositories. MaxMind support disabled." + command -v geoipupdate >/dev/null 2>&1 && GEOIPUPDATE_AVAILABLE=true || GEOIPUPDATE_AVAILABLE=false + fi +elif [ "$PKG_MANAGER" = "yum" ] || [ "$PKG_MANAGER" = "dnf" ]; then + $PKG_MANAGER install -y ipset xtables-addons perl-Text-CSV_XS wget curl jq ipcalc || { echo "ERROR: Failed to install dependencies."; exit 1; } + if [ "$IP_SOURCE" = "both" ] && [ -n "$MAXMIND_LICENSE_KEY" ]; then + $PKG_MANAGER install -y geoipupdate || echo "WARNING: geoipupdate not found in repositories. MaxMind support disabled." + command -v geoipupdate >/dev/null 2>&1 && GEOIPUPDATE_AVAILABLE=true || GEOIPUPDATE_AVAILABLE=false + fi +fi + +# === Check Required Commands === +check_command ipset +check_command firewall-cmd +check_command wget +check_command curl +check_command jq +check_command ipcalc + +# === Verify iptables-legacy Backend === +if firewall-cmd --get-ipset-types | grep -q "hash:net"; then + echo "INFO: ipset hash:net supported by firewalld." +else + echo "ERROR: ipset hash:net not supported. Ensure xt_geoip module is loaded." + exit 1 +fi + +# === Locate xtables-addons Binaries === +XTABLES_DIR="" +for dir in /usr/libexec/xtables-addons /usr/lib/xtables-addons /usr/sbin /usr/bin; do + if [ -f "$dir/xt_geoip_dl" ] && [ -f "$dir/xt_geoip_build" ]; then + XTABLES_DIR="$dir" + break + fi +done +if [ -z "$XTABLES_DIR" ]; then + echo "ERROR: Could not find xt_geoip_dl and xt_geoip_build. Ensure xtables-addons is installed correctly." + exit 1 +fi + +# === Validate IP List === +validate_ip_list() { + local file="$1" + if [ ! -s "$file" ]; then + echo "ERROR: IP list file $file is empty or does not exist." + exit 1 + fi + while IFS= read -r ip; do + [[ -z "$ip" ]] && continue + validate_ip "$ip" + done < "$file" +} + +# === Download GeoIP Data === +download_geoip_data() { + local cc="$1" + local tmp_file="/tmp/${cc}.zone" + local ip_list_file="/tmp/${cc}_ips.txt" + local temp_range_file="/tmp/${cc}_ranges.txt" + > "$ip_list_file" # Clear temporary IP list file + > "$temp_range_file" + + # RIPE + echo "INFO: Downloading RIPE IP list for $cc..." + wget -q -O "$tmp_file" "https://stat.ripe.net/data/country-resource-list/data.json?resource=$cc&v=4" || { echo "ERROR: Failed to download RIPE IP list for $cc."; exit 1; } + if [ -s "$tmp_file" ]; then + jq -r '.data.resources.ipv4[]' "$tmp_file" > "$temp_range_file" 2>/dev/null + while IFS= read -r range; do + [[ -z "$range" ]] && continue + if [[ "$range" =~ - ]]; then + convert_range_to_cidr "$range" >> "$ip_list_file" || continue + else + echo "$range" >> "$ip_list_file" + fi + done < "$temp_range_file" + validate_ip_list "$ip_list_file" + if [ "$IPV6" = true ]; then + jq -r '.data.resources.ipv6[]' "$tmp_file" > "${tmp_file}.v6" 2>/dev/null + if [ -s "${tmp_file}.v6" ]; then + validate_ip_list "${tmp_file}.v6" + fi + fi + fi + rm -f "$tmp_file" "$temp_range_file" + + # MaxMind (if license key provided, geoipupdate available, and --ip-source both) + if [ "$GEOIPUPDATE_AVAILABLE" = true ] && [ -n "$MAXMIND_LICENSE_KEY" ] && [ -n "$MAXMIND_YOUR_ACCOUNT_ID" ] && [ "$IP_SOURCE" = "both" ]; then + echo "INFO: Downloading MaxMind GeoLite2-Country CSV for $cc..." + # Download GeoLite2-Country-CSV directly + csv_zip="/tmp/GeoLite2-Country-CSV.zip" + wget -q -O "$csv_zip" "https://download.maxmind.com/geoip/databases/GeoLite2-Country-CSV/download?suffix=zip" \ + --user="$MAXMIND_YOUR_ACCOUNT_ID" --password="$MAXMIND_LICENSE_KEY" || { echo "ERROR: Failed to download MaxMind GeoLite2-CSV."; exit 1; } + unzip -q -o "$csv_zip" -d /tmp || { echo "ERROR: Failed to unzip MaxMind GeoLite2-CSV."; exit 1; } + csv_file=$(find /tmp -name "GeoLite2-Country-Blocks-IPv4.csv" | head -n 1) + if [ -z "$csv_file" ]; then + echo "WARNING: GeoLite2-Country-Blocks-IPv4.csv not found. Falling back to RIPE." + mv "$ip_list_file" "$tmp_file" + rm -f "$csv_zip" + return + fi + # Extract IPs for the country + grep ",$cc," "$csv_file" | cut -d',' -f1 >> "$ip_list_file" || { echo "WARNING: Failed to extract IPs for $cc from MaxMind CSV. Falling back to RIPE."; mv "$ip_list_file" "$tmp_file"; rm -f "$csv_zip" /tmp/GeoLite2-Country_*/GeoLite2-Country-*.csv; return; } + validate_ip_list "$ip_list_file" + mv "$ip_list_file" "$tmp_file" + rm -f "$csv_zip" /tmp/GeoLite2-Country_*/GeoLite2-Country-*.csv + else + if [ -n "$MAXMIND_LICENSE_KEY" ] && [ "$IP_SOURCE" = "both" ]; then + echo "WARNING: geoipupdate or account ID missing. Falling back to RIPE. Install geoipupdate and set MAXMIND_YOUR_ACCOUNT_ID for MaxMind support." + fi + mv "$ip_list_file" "$tmp_file" + fi +} + +# === Build GeoIP Database === +build_geoip_db() { + echo "INFO: Building GeoIP database..." + mkdir -p "$GEOIP_DIR" || { echo "ERROR: Failed to create $GEOIP_DIR."; exit 1; } + cd "$XTABLES_DIR" || { echo "ERROR: Failed to change to $XTABLES_DIR."; exit 1; } + for cc in "${COUNTRIES[@]}"; do + ./xt_geoip_build -D "$GEOIP_DIR" "/tmp/${cc}.zone" || { echo "ERROR: Failed to build GeoIP database for $cc."; exit 1; } + done +} + +# === Create and Populate ipset === +create_ipset() { + echo "INFO: Creating ipset: $IPSET_NAME" + ipset destroy "$IPSET_NAME" 2>/dev/null || true + ipset create "$IPSET_NAME" hash:net family inet || { echo "ERROR: Failed to create ipset $IPSET_NAME."; exit 1; } + if [ "$IPV6" = true ]; then + IPSET_NAME_V6="geo-allowed-v6" + ipset destroy "$IPSET_NAME_V6" 2>/dev/null || true + ipset create "$IPSET_NAME_V6" hash:net family inet6 || { echo "ERROR: Failed to create ipset $IPSET_NAME_V6."; exit 1; } + fi +} + +populate_ipset() { + echo "INFO: Adding country IPs..." + for cc in "${COUNTRIES[@]}"; do + echo "INFO: -> $cc" + download_geoip_data "$cc" + while IFS= read -r ip; do + [[ -z "$ip" ]] && continue + ipset add "$IPSET_NAME" "$ip" || { echo "ERROR: Failed to add $ip to ipset."; exit 1; } + done < "/tmp/${cc}.zone" + if [ "$IPV6" = true ] && [ -f "/tmp/${cc}.zone.v6" ]; then + while IFS= read -r ip; do + [[ -z "$ip" ]] && continue + ipset add "$IPSET_NAME_V6" "$ip" || { echo "ERROR: Failed to add IPv6 $ip to ipset."; exit 1; } + done < "/tmp/${cc}.zone.v6" + fi + rm -f "/tmp/${cc}.zone" "/tmp/${cc}.zone.v6" + done + + echo "INFO: Adding manual IPs..." + for ip in "${MANUAL_IPS[@]}"; do + if [[ "$ip" =~ : ]]; then + ipset add "$IPSET_NAME_V6" "$ip" || { echo "ERROR: Failed to add manual IPv6 $ip to ipset."; exit 1; } + else + ipset add "$IPSET_NAME" "$ip" || { echo "ERROR: Failed to add manual IP $ip to ipset."; exit 1; } + fi + done + + echo "INFO: Adding LAN subnets..." + detect_lan_subnets + for subnet in "${LAN_SUBNETS[@]}"; do + if [[ "$subnet" =~ : ]]; then + ipset add "$IPSET_NAME_V6" "$subnet" || { echo "ERROR: Failed to add LAN IPv6 subnet $subnet to ipset."; exit 1; } + else + ipset add "$IPSET_NAME" "$subnet" || { echo "ERROR: Failed to add LAN subnet $subnet to ipset."; exit 1; } + fi + done +} + +# === Save ipset for Persistence === +save_ipset() { + echo "INFO: Saving ipset to $IPSET_SAVE_PATH for persistence..." + ipset save "$IPSET_NAME" > "$IPSET_SAVE_PATH" || { echo "ERROR: Failed to save ipset."; exit 1; } + if [ "$IPV6" = true ]; then + ipset save "$IPSET_NAME_V6" >> "$IPSET_SAVE_PATH" || { echo "ERROR: Failed to save IPv6 ipset."; exit 1; } + fi +} + +# === Create systemd Service for ipset Restore === +create_systemd_service() { + echo "INFO: Creating systemd service for ipset persistence..." + cat < "$SYSTEMD_IPSET_SERVICE" +[Unit] +Description=Restore ipset on boot +After=network.target firewalld.service + +[Service] +Type=oneshot +ExecStart=/usr/sbin/ipset restore -f $IPSET_SAVE_PATH +RemainAfterExit=yes + +[Install] +WantedBy=multi-user.target +EOF + systemctl enable ipset-restore.service || { echo "ERROR: Failed to enable ipset-restore service."; exit 1; } +} + +# === Setup Blocked IP Logging === +setup_blocked_ip_logging() { + echo "INFO: Setting up blocked IP logging..." + # Check firewalld state + if ! firewall-cmd --state >/dev/null 2>&1; then + echo "INFO: Firewalld is not running or in failed state. Using firewall-offline-cmd..." + # Stop firewalld if running + systemctl stop firewalld 2>/dev/null || true + # Remove any existing GEO_BLOCK chain + firewall-offline-cmd --direct --remove-chain ipv4 filter GEO_BLOCK 2>/dev/null || true + firewall-offline-cmd --direct --remove-rules ipv4 filter GEO_BLOCK 2>/dev/null || true + # Create GEO_BLOCK chain + firewall-offline-cmd --direct --add-chain ipv4 filter GEO_BLOCK || { echo "ERROR: Failed to create GEO_BLOCK chain offline."; exit 1; } + # Add logging and drop rules + firewall-offline-cmd --direct --add-rule ipv4 filter GEO_BLOCK 0 -m limit --limit 1/minute -j LOG --log-prefix "GEO_BLOCK: " --log-level 4 || { echo "ERROR: Failed to add logging rule offline."; exit 1; } + firewall-offline-cmd --direct --add-rule ipv4 filter GEO_BLOCK 1 -j DROP || { echo "ERROR: Failed to add drop rule offline."; exit 1; } + if [ "$IPV6" = true ]; then + # Remove any existing GEO_BLOCK chain for IPv6 + firewall-offline-cmd --direct --remove-chain ipv6 filter GEO_BLOCK 2>/dev/null || true + firewall-offline-cmd --direct --remove-rules ipv6 filter GEO_BLOCK 2>/dev/null || true + # Create GEO_BLOCK chain for IPv6 + firewall-offline-cmd --direct --add-chain ipv6 filter GEO_BLOCK || { echo "ERROR: Failed to create GEO_BLOCK chain for IPv6 offline."; exit 1; } + # Add logging and drop rules for IPv6 + firewall-offline-cmd --direct --add-rule ipv6 filter GEO_BLOCK 0 -m limit --limit 1/minute -j LOG --log-prefix "GEO_BLOCK_V6: " --log-level 4 || { echo "ERROR: Failed to add IPv6 logging rule offline."; exit 1; } + firewall-offline-cmd --direct --add-rule ipv6 filter GEO_BLOCK 1 -j DROP || { echo "ERROR: Failed to add IPv6 drop rule offline."; exit 1; } + fi + # Restart firewalld + systemctl start firewalld || { echo "ERROR: Failed to restart firewalld."; exit 1; } + else + # Remove any existing GEO_BLOCK chain + firewall-cmd --permanent --direct --remove-chain ipv4 filter GEO_BLOCK 2>/dev/null || true + firewall-cmd --permanent --direct --remove-rules ipv4 filter GEO_BLOCK 2>/dev/null || true + # Create GEO_BLOCK chain + firewall-cmd --permanent --direct --add-chain ipv4 filter GEO_BLOCK || { echo "ERROR: Failed to create GEO_BLOCK chain."; exit 1; } + # Add logging and drop rules + firewall-cmd --permanent --direct --add-rule ipv4 filter GEO_BLOCK 0 -m limit --limit 1/minute -j LOG --log-prefix "GEO_BLOCK: " --log-level 4 || { echo "ERROR: Failed to add logging rule."; exit 1; } + firewall-cmd --permanent --direct --add-rule ipv4 filter GEO_BLOCK 1 -j DROP || { echo "ERROR: Failed to add drop rule."; exit 1; } + if [ "$IPV6" = true ]; then + # Remove any existing GEO_BLOCK chain for IPv6 + firewall-cmd --permanent --direct --remove-chain ipv6 filter GEO_BLOCK 2>/dev/null || true + firewall-cmd --permanent --direct --remove-rules ipv6 filter GEO_BLOCK 2>/dev/null || true + # Create GEO_BLOCK chain for IPv6 + firewall-cmd --permanent --direct --add-chain ipv6 filter GEO_BLOCK || { echo "ERROR: Failed to create GEO_BLOCK chain for IPv6."; exit 1; } + # Add logging and drop rules for IPv6 + firewall-cmd --permanent --direct --add-rule ipv6 filter GEO_BLOCK 0 -m limit --limit 1/minute -j LOG --log-prefix "GEO_BLOCK_V6: " --log-level 4 || { echo "ERROR: Failed to add IPv6 logging rule."; exit 1; } + firewall-cmd --permanent --direct --add-rule ipv6 filter GEO_BLOCK 1 -j DROP || { echo "ERROR: Failed to add IPv6 drop rule."; exit 1; } + fi + fi + # Ensure log file exists + touch "$BLOCKED_IP_LOG" + chmod 600 "$BLOCKED_IP_LOG" +} + +# === Add firewalld rules === +add_firewall_rules() { + echo "INFO: Adding firewalld rules..." + setup_blocked_ip_logging + + # Allow established and related sessions + echo "INFO: Allowing established and related sessions" + firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 0 \ + -m state --state ESTABLISHED,RELATED -j ACCEPT || { echo "ERROR: Failed to add ESTABLISHED/RELATED rule."; exit 1; } + if [ "$IPV6" = true ]; then + firewall-cmd --permanent --direct --add-rule ipv6 filter INPUT 0 \ + -m state --state ESTABLISHED,RELATED -j ACCEPT || { echo "ERROR: Failed to add IPv6 ESTABLISHED/RELATED rule."; exit 1; } + fi + + # TCP + if [ ${#TCP_PORTS[@]} -eq 0 ]; then + echo "INFO: -> All TCP ports" + firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 1 \ + -p tcp -m state --state NEW -m set ! --match-set "$IPSET_NAME" src -j GEO_BLOCK || { echo "ERROR: Failed to add TCP rule."; exit 1; } + else + for port in "${TCP_PORTS[@]}"; do + echo "INFO: -> TCP $port" + firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 1 \ + -p tcp --dport "$port" -m state --state NEW -m set ! --match-set "$IPSET_NAME" src -j GEO_BLOCK || { echo "ERROR: Failed to add TCP rule for port $port."; exit 1; } + done + fi + + # UDP + if [ ${#UDP_PORTS[@]} -eq 0 ]; then + echo "INFO: -> All UDP ports" + firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 1 \ + -p udp -m state --state NEW -m set ! --match-set "$IPSET_NAME" src -j GEO_BLOCK || { echo "ERROR: Failed to add UDP rule."; exit 1; } + else + for port in "${UDP_PORTS[@]}"; do + echo "INFO: -> UDP port $port" + firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 1 \ + -p udp --dport "$port" -m state --state NEW -m set ! --match-set "$IPSET_NAME" src -j GEO_BLOCK || { echo "ERROR: Failed to add UDP rule for port $port". exit 1; } + done + fi + + # IPv6 + if [ "$IPV6" = true ]; then + IPSET_NAME_V6="geo-allowed-v6-v6" + if [ ${#TCP_PORTS[@]} -eq 0 ]; then + echo "INFO: -> All TCP IPv6 ports" + firewall-cmd --permanent --direct --add-rule ipv6 filter INPUT 1 \ + -p tcp -m state --state NEW -m set ! --match-set "$IPSET_NAME_V6" src -j GEO_BLOCK || { echo "ERROR: Failed to add TCP IPv6 rule."; exit 1; } + else + for port in "${TCP_PORTS[@]}"; do + echo "INFO: -> TCP IPv6 $port" + firewall-cmd --permanent --direct --add-rule ipv6 filter INPUT 1 \ + -p tcp --dport "$port" -m state --state NEW -m set ! --match-set "$IPSET_NAME_V6" src -j GEO_BLOCK || { echo "ERROR: Failed to add TCP IPv6 rule for port $port."; exit 1; } + done + fi + if [ ${#UDP_PORTS[@]} -eq 0 ]; then + echo "INFO: -> All UDP IPv6 ports" + firewall-cmd --permanent --direct --add-rule ipv6 filter INPUT 1 \ + -p udp -m state --state NEW -m set ! --match-set "$IPSET_NAME_V6" src -j GEO_BLOCK || { echo "ERROR: Failed to add UDP IPv6 rule."; exit 1; } + else + for port in "${UDP_PORTS[@]}"; do + echo "INFO: -> UDP IPv6 $port" + firewall-cmd --permanent --direct --add-rule ipv6 filter INPUT 1 \ + -p udp --dport "$port" -m state --state NEW -m set ! --match-set "$IPSET_NAME_V6" src -j GEO_BLOCK || { echo "ERROR: Failed to add UDP IPv6 rule for port $port."; exit 1; } + done + fi + fi +} + +# === Cron Job for Daily GeoIP Update === +create_cron_job() { + echo "INFO: Installing daily cron job at $CRON_JOB_PATH" + cat < "$CRON_JOB_PATH" +#!/bin/bash +cd "$XTABLES_DIR" || { echo "ERROR: Failed to change to $XTABLES_DIR."; exit 1; } +ipset flush "$IPSET_NAME" +if [ "$IPV6" = true ]; then + ipset flush "$IPSET_NAME_V6" +fi +for cc in ${COUNTRIES[*]}; do + ip_list_file="/tmp/\${cc}_ips.txt" + temp_range_file="/tmp/\${cc}_ranges.txt" + > "\$ip_list_file" + > "\$temp_range_file" + wget -q -O "/tmp/\${cc}.zone" "https://stat.ripe.net/data/country-resource-list/data.json?resource=\${cc}&v=4" || { echo "ERROR: Failed to download RIPE IP list for \${cc}."; exit 1; } + if [ -s "/tmp/\${cc}.zone" ]; then + jq -r '.data.resources.ipv4[]' "/tmp/\${cc}.zone" > "\$temp_range_file" 2>/dev/null + while IFS= read -r range; do + [[ -z "\$range" ]] && continue + if [[ "\$range" =~ - ]]; then + ipcalc -r "\${range//-/ }" | grep -oE '[0-9.]+/[0-9]+' >> "\$ip_list_file" || continue + else + echo "\$range" >> "\$ip_list_file" + fi + done < "\$temp_range_file" + if [ "$IPV6" = true ]; then + jq -r '.data.resources.ipv6[]' "/tmp/\${cc}.zone" > "/tmp/\${cc}.zone.v6" 2>/dev/null + fi + fi + if [ "$GEOIPUPDATE_AVAILABLE" = true ] && [ -n "$MAXMIND_LICENSE_KEY" ] && [ -n "$MAXMIND_YOUR_ACCOUNT_ID" ] && [ "$IP_SOURCE" = "both" ]; then + csv_zip="/tmp/GeoLite2-Country-CSV.zip" + wget -q -O "$csv_zip" "https://download.maxmind.com/geoip/databases/GeoLite2-Country-CSV/download?suffix=zip" \ + --user="$MAXMIND_YOUR_ACCOUNT_ID" --password="$MAXMIND_LICENSE_KEY" || { echo "WARNING: Failed to download MaxMind GeoLite2-CSV. Falling back to RIPE."; } + unzip -q -o "$csv_zip" -d /tmp || { echo "WARNING: Failed to unzip MaxMind GeoLite2-CSV. Falling back to RIPE."; } + csv_file=$(find /tmp -name "GeoLite2-Country-Blocks-IPv4.csv" | head -n 1) + if [ -n "$csv_file" ]; then + grep ",${cc}," "$csv_file" | cut -d',' -f1 >> "$ip_list_file" || { echo "WARNING: Failed to extract IPs for $cc from MaxMind CSV. Falling back to RIPE."; } + fi + rm -f "$csv_zip" /tmp/GeoLite2-Country_*/GeoLite2-Country-*.csv + fi + mv "\$ip_list_file" "/tmp/\${cc}.zone" + ./xt_geoip_build -D "$GEOIP_DIR" "/tmp/\${cc}.zone" || { echo "ERROR: Failed to build GeoIP database for \${cc}."; exit 1; } + while IFS= read -r ip; do + [[ -z "\$ip" ]] && continue + ipset add "$IPSET_NAME" "\$ip" || { echo "ERROR: Failed to add \$ip to ipset."; exit 1; } + done < "/tmp/\${cc}.zone" + if [ "$IPV6" = true ] && [ -f "/tmp/\${cc}.zone.v6" ]; then + while IFS= read -r ip; do + [[ -z "\$ip" ]] && continue + ipset add "$IPSET_NAME_V6" "\$ip" || { echo "ERROR: Failed to add IPv6 \$ip to ipset."; exit 1; } + done < "/tmp/\${cc}.zone.v6" + fi + rm -f "/tmp/\${cc}.zone" "/tmp/\${cc}.zone.v6" "\$temp_range_file" +done +for ip in ${MANUAL_IPS[*]}; do + if [[ "\$ip" =~ : ]]; then + ipset add "$IPSET_NAME_V6" "\$ip" || { echo "ERROR: Failed to add manual IPv6 \$ip to ipset."; exit 1; } + else + ipset add "$IPSET_NAME" "\$ip" || { echo "ERROR: Failed to add manual IP \$ip to ipset."; exit 1; } + fi +done +LAN_SUBNETS=() +for iface in \$(ip link show | grep -E '^[0-9]+:.*state UP' | cut -d: -f2 | awk '{print \$1}'); do + subnets=\$(ip addr show "\$iface" | grep -oE 'inet [0-9.]+/[0-9]+' | awk '{print \$2}') + for subnet in \$subnets; do + LAN_SUBNETS+=("\$subnet") + ipset add "$IPSET_NAME" "\$subnet" || { echo "ERROR: Failed to add LAN subnet \$subnet to ipset."; exit 1; } + done + if [ "$IPV6" = true ]; then + ipv6_subnets=\$(ip addr show "\$iface" | grep -oE 'inet6 [0-9a-fA-F:]+/[0-9]+' | awk '{print \$2}') + for subnet in \$ipv6_subnets; do + LAN_SUBNETS+=("\$subnet") + ipset add "$IPSET_NAME_V6" "\$subnet" || { echo "ERROR: Failed to add LAN IPv6 subnet \$subnet to ipset."; exit 1; } + done + fi +done +ipset save "$IPSET_NAME" > "$IPSET_SAVE_PATH" || { echo "ERROR: Failed to save ipset."; exit 1; } +if [ "$IPV6" = true ]; then + ipset save "$IPSET_NAME_V6" >> "$IPSET_SAVE_PATH" || { echo "ERROR: Failed to save IPv6 ipset."; exit 1; } +fi +echo "INFO: GeoIP update completed successfully." +EOF + chmod +x "$CRON_JOB_PATH" || { echo "ERROR: Failed to create cron job."; exit 1; } +} + +# === Main Execution === +create_ipset +populate_ipset +save_ipset +create_systemd_service +add_firewall_rules +create_cron_job + +echo "INFO: Reloading firewalld..." +firewall-cmd --reload || { echo "ERROR: Failed to reload firewalld."; exit 1; } + +echo "INFO: GeoIP firewall filtering is now active."