257 lines
9.2 KiB
Bash
257 lines
9.2 KiB
Bash
#!/bin/bash
|
||
# === 🛠️ DEFAULT CONFIGURATION ===
|
||
DEFAULT_COUNTRIES=("gb") # "gb" "us"
|
||
DEFAULT_MANUAL_IPS=() # "203.0.113.10" "198.51.100.0/24"
|
||
DEFAULT_TCP_PORTS=() # Empty means all TCP ports by default
|
||
DEFAULT_UDP_PORTS=() # Empty means all UDP ports by default
|
||
IPSET_SAVE_PATH="/etc/ipset.conf"
|
||
|
||
# === 🌐 Constants ===
|
||
GEOIP_DIR="/usr/share/xt_geoip"
|
||
IPSET_NAME="geo-allowed"
|
||
CRON_JOB_PATH="/etc/cron.weekly/update-xt-geoip"
|
||
SYSTEMD_IPSET_SERVICE="/etc/systemd/system/ipset-restore.service"
|
||
SCRIPT_NAME="$(basename "$0")"
|
||
|
||
# === Runtime Config ===
|
||
COUNTRIES=("${DEFAULT_COUNTRIES[@]}")
|
||
MANUAL_IPS=("${DEFAULT_MANUAL_IPS[@]}")
|
||
TCP_PORTS=("${DEFAULT_TCP_PORTS[@]}")
|
||
UDP_PORTS=("${DEFAULT_UDP_PORTS[@]}")
|
||
REMOVE=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 " --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 ;;
|
||
--remove) REMOVE=true ;;
|
||
-h|--help) usage ;;
|
||
*) echo "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; }
|
||
}
|
||
|
||
# === Cleanup Function ===
|
||
remove_firewall_rules() {
|
||
echo "🔧 Removing firewalld rules..."
|
||
|
||
# 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 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 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 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 DROP 2>/dev/null || true
|
||
done
|
||
fi
|
||
|
||
echo "🧼 Flushing and destroying ipset..."
|
||
ipset flush "$IPSET_NAME" 2>/dev/null || true
|
||
ipset destroy "$IPSET_NAME" 2>/dev/null || true
|
||
|
||
echo "🗑️ Removing cron job at $CRON_JOB_PATH"
|
||
rm -f "$CRON_JOB_PATH"
|
||
|
||
echo "🗑️ Removing ipset persistence configuration"
|
||
rm -f "$IPSET_SAVE_PATH"
|
||
systemctl disable ipset-restore.service 2>/dev/null || true
|
||
rm -f "$SYSTEMD_IPSET_SERVICE"
|
||
|
||
echo "♻️ Reloading firewalld..."
|
||
firewall-cmd --reload || { echo "Error: Failed to reload firewalld."; exit 1; }
|
||
|
||
echo "✅ Firewall rules and GeoIP ipset removed."
|
||
}
|
||
|
||
if [ "$REMOVE" = true ]; then
|
||
remove_firewall_rules
|
||
exit 0
|
||
fi
|
||
|
||
# === 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 "📦 Installing dependencies..."
|
||
if [ "$PKG_MANAGER" = "apt" ]; then
|
||
apt update && apt install -y ipset xtables-addons-common libtext-csv-xs-perl wget || { echo "Error: Failed to install dependencies."; exit 1; }
|
||
elif [ "$PKG_MANAGER" = "yum" ] || [ "$PKG_MANAGER" = "dnf" ]; then
|
||
$PKG_MANAGER install -y ipset xtables-addons perl-Text-CSV_XS wget || { echo "Error: Failed to install dependencies."; exit 1; }
|
||
fi
|
||
|
||
# === Check Required Commands ===
|
||
check_command ipset
|
||
check_command firewall-cmd
|
||
check_command wget
|
||
|
||
# === Verify iptables-legacy Backend ===
|
||
if firewall-cmd --get-ipset-types | grep -q "hash:net"; then
|
||
echo "✅ 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
|
||
|
||
# === Download and Build GeoIP DB ===
|
||
echo "🔧 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; }
|
||
./xt_geoip_dl || { echo "Error: Failed to download GeoIP data."; exit 1; }
|
||
./xt_geoip_build -D "$GEOIP_DIR" GeoIPCountryWhois.csv || { echo "Error: Failed to build GeoIP database."; exit 1; }
|
||
|
||
# === Create and Populate ipset ===
|
||
echo "🧰 Creating ipset: $IPSET_NAME"
|
||
ipset destroy "$IPSET_NAME" 2>/dev/null || true
|
||
ipset create "$IPSET_NAME" hash:net || { echo "Error: Failed to create ipset $IPSET_NAME."; exit 1; }
|
||
|
||
echo "🌍 Adding country IPs..."
|
||
for cc in "${COUNTRIES[@]}"; do
|
||
echo " -> $cc"
|
||
wget -q -O "/tmp/${cc}.zone" "https://www.ipdeny.com/ipblocks/data/countries/${cc}.zone" || { echo "Error: Failed to download IP list 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"
|
||
rm -f "/tmp/${cc}.zone"
|
||
done
|
||
|
||
echo "➕ Adding manual IPs..."
|
||
for ip in "${MANUAL_IPS[@]}"; do
|
||
ipset add "$IPSET_NAME" "$ip" || { echo "Error: Failed to add manual IP $ip to ipset."; exit 1; }
|
||
done
|
||
|
||
# === Save ipset for Persistence ===
|
||
echo "💾 Saving ipset to $IPSET_SAVE_PATH for persistence..."
|
||
ipset save "$IPSET_NAME" > "$IPSET_SAVE_PATH" || { echo "Error: Failed to save ipset."; exit 1; }
|
||
|
||
# === Create systemd Service for ipset Restore ===
|
||
echo "🛠️ Creating systemd service for ipset persistence..."
|
||
cat <<EOF > "$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; }
|
||
|
||
# === Add firewalld rules ===
|
||
echo "🔥 Adding firewalld rules..."
|
||
|
||
# TCP
|
||
if [ ${#TCP_PORTS[@]} -eq 0 ]; then
|
||
echo " -> All TCP ports"
|
||
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 0 \
|
||
-p tcp -m set ! --match-set "$IPSET_NAME" src -j DROP || { echo "Error: Failed to add TCP rule."; exit 1; }
|
||
else
|
||
for port in "${TCP_PORTS[@]}"; do
|
||
echo " -> TCP $port"
|
||
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 0 \
|
||
-p tcp --dport "$port" -m set ! --match-set "$IPSET_NAME" src -j DROP || { echo "Error: Failed to add TCP rule for port $port."; exit 1; }
|
||
done
|
||
fi
|
||
|
||
# UDP
|
||
if [ ${#UDP_PORTS[@]} -eq 0 ]; then
|
||
echo " -> All UDP ports"
|
||
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 0 \
|
||
-p udp -m set ! --match-set "$IPSET_NAME" src -j DROP || { echo "Error: Failed to add UDP rule."; exit 1; }
|
||
else
|
||
for port in "${UDP_PORTS[@]}"; do
|
||
echo " -> UDP $port"
|
||
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 0 \
|
||
-p udp --dport "$port" -m set ! --match-set "$IPSET_NAME" src -j DROP || { echo "Error: Failed to add UDP rule for port $port."; exit 1; }
|
||
done
|
||
fi
|
||
|
||
# === Cron Job for Weekly GeoIP Update ===
|
||
echo "🕒 Installing weekly cron job at $CRON_JOB_PATH"
|
||
cat <<EOF > "$CRON_JOB_PATH"
|
||
#!/bin/bash
|
||
cd "$XTABLES_DIR" || exit 1
|
||
./xt_geoip_dl || exit 1
|
||
./xt_geoip_build -D "$GEOIP_DIR" GeoIPCountryWhois.csv || exit 1
|
||
ipset flush "$IPSET_NAME"
|
||
for cc in ${COUNTRIES[*]}; do
|
||
wget -q -O "/tmp/\${cc}.zone" "https://www.ipdeny.com/ipblocks/data/countries/\${cc}.zone" || exit 1
|
||
while IFS= read -r ip; do
|
||
[[ -z "\$ip" ]] && continue
|
||
ipset add "$IPSET_NAME" "\$ip" || exit 1
|
||
done < "/tmp/\${cc}.zone"
|
||
rm -f "/tmp/\${cc}.zone"
|
||
done
|
||
for ip in ${MANUAL_IPS[*]}; do
|
||
ipset add "$IPSET_NAME" "\$ip" || exit 1
|
||
done
|
||
ipset save "$IPSET_NAME" > "$IPSET_SAVE_PATH" || exit 1
|
||
EOF
|
||
chmod +x "$CRON_JOB_PATH" || { echo "Error: Failed to create cron job."; exit 1; }
|
||
|
||
# === Reload firewalld ===
|
||
echo "🔄 Reloading firewalld..."
|
||
firewall-cmd --reload || { echo "Error: Failed to reload firewalld."; exit 1; }
|
||
|
||
echo "✅ GeoIP firewall filtering is now active."
|