Il traffico indesiderato proveniente da specifiche aree geografiche è un problema comune per i server esposti su Internet: tentativi di accesso SSH, scan delle porte, bot che sondano servizi web. Bloccare singoli IP è inefficace in quanto cambiano continuamente, mentre bloccare l’intero spazio di indirizzi di un paese è un approccio più robusto.
Vediamo come costruire un sistema di geo-blocking usando firewalld e ipset. Ipset mantiene in memoria i range IP del paese da bloccare come un insieme hash consultabile in tempo reale mentre firewalld costruisce le regole che lo referenziano. Le liste di indirizzi per ogni paese vengono scaricate da ipdeny, un servizio che pubblica prefissi CIDR aggiornati quotidianamente per tutti i paesi identificati dal loro codice ISO a due lettere. La procedura è identica per qualsiasi paese, basta sostituire il codice.
Come funziona
I prefissi di rete assegnati a ciascun paese sono documentati dalle Regional Internet Registries (RIPE per l’Europa, APNIC per l’Asia-Pacifico, ARIN per il Nord America, e così via). Ipdeny raccoglie queste informazioni e le pubblica come file di testo con un prefisso CIDR per riga, organizzati per codice ISO: cn.zone per la Cina, ru.zone per la Russia, de.zone per la Germania, e così via.
Ipset è un modulo del kernel Linux che mantiene insiemi di indirizzi IP in strutture hash in memoria. A differenza delle regole iptables classiche che esaminano ogni pacchetto confrontandolo con ciascuna regola in sequenza, ipset confronta l’IP sorgente di un pacchetto con migliaia di range. Questo lo rende l’unica soluzione pratica per lavorare con le liste di un intero paese, che possono contenere centinaia o migliaia di prefissi.
Firewalld, il frontend di gestione del firewall su Linux, supporta nativamente gli ipset e permette di costruire rich rule che li referenziano direttamente.
Prerequisiti
In questo articolo utilizzeremo AlmaLinux come distribuzione ma firewalld è supportato e disponibile su molte altre distribuzioni. Qualora non sia installato possiamo farlo con
dnf install -y firewalld
systemctl enable --now firewalldPrima di attivare firewalld su un server raggiungibile via SSH, assicurarsi che la porta 22 sia nella configurazione permanente. Se non lo è
firewall-cmd --permanent --add-service=ssh
firewall-cmd --reloadAggiungere eventuali altri servizi necessari per il corretto funzionamento dei servizi presenti sul server, se per esempio è attivo un WEB Server dovremo aggiungere
firewall-cmd --permanent --add-service=https
firewall-cmd --reloadPer una guida sull’utilizzo di firewall-cmd è possibile leggere questo post: firewall-cmd: Configurare il Firewall su Linux con facilità
Configurazione
Definire il codice ISO del paese da bloccare. Il codice deve essere in minuscolo in quanto è il formato usato da ipdeny per i nomi dei file. Il codice in maiuscolo servirà come prefisso nei log per rendere le righe filtrabili.
COUNTRY_CODE="cn"
COUNTRY_LABEL="CN"Scaricare la lista dei prefissi in un file temporaneo
curl -o /tmp/${COUNTRY_CODE}.zone https://www.ipdeny.com/ipblocks/data/countries/${COUNTRY_CODE}.zoneIl file risultante è un elenco di prefissi CIDR, uno per riga
1.0.1.0/24
1.0.2.0/23
1.0.8.0/21
...Creare l’ipset in firewalld con tipo hash:net, il tipo adatto a contenere prefissi di rete
firewall-cmd --permanent \
--new-ipset=geoblock-${COUNTRY_CODE} \
--type=hash:net \
--option=family=inetPossiamo ora caricare i prefissi scaricati nell’ipset
firewall-cmd --permanent \
--ipset=geoblock-${COUNTRY_CODE} \
--add-entries-from-file=/tmp/${COUNTRY_CODE}.zoneCreare la rich rule che blocca tutto il traffico il cui IP sorgente appartiene all’ipset
firewall-cmd --permanent --add-rich-rule="rule source ipset=geoblock-${COUNTRY_CODE} drop"Ricaricare firewalld per applicare le modifiche
firewall-cmd --reloadVerificare che l’ipset contenga entries e che la rich rule sia attiva
firewall-cmd --info-ipset=geoblock-${COUNTRY_CODE} | head
# Output atteso
geoblock-cn
type: hash:net
options: family=inet
entries: 1.0.1.0/24 1.0.2.0/23 1.0.8.0/21 1.0.32.0/19 ...
firewall-cmd --list-rich-rules
# Output atteso
rule source ipset="geoblock-cn" dropPer controllare se un IP specifico appartiene all’insieme
firewall-cmd --ipset=geoblock-${COUNTRY_CODE} --query-entry=1.0.1.0/24
# output atteso
yesAggiungere il logging
La regola drop scarta i pacchetti silenziosamente, senza lasciare traccia. Per vedere i tentativi bloccati è necessario aggiungere esplicitamente un’azione di log alla rich rule. Il log viene scritto dal kernel nel journal di systemd e può essere filtrato per prefisso.
# Rimuovere la regola senza logging
firewall-cmd --permanent \
--remove-rich-rule="rule source ipset=geoblock-${COUNTRY_CODE} drop"
# Aggiungere la regola con logging
firewall-cmd --permanent \
--add-rich-rule="rule source ipset=geoblock-${COUNTRY_CODE} log prefix=\"GEOBLOCK-${COUNTRY_LABEL}\" level=\"info\" drop"
# Applicare le modifiche
firewall-cmd --reloadPer consultare i pacchetti bloccati
journalctl -k | grep GEOBLOCK-CN
journalctl -k -f | grep GEOBLOCK-CN # in tempo realeOgni riga di log contiene l’IP sorgente, l’IP di destinazione e la porta di destinazione come nel seguente esempio
Jun 19 23:29:21 localhost kernel: GEOBLOCK-CNIN=eth0 OUT= MAC=02:01:e1:41:bd:b7:82:01:e1:41:bd:b7:08:00 SRC=110.249.202.17 DST=203.0.113.5 LEN=60 TOS=0x00 PREC=0x00 TTL=48 ID=12852 DF PROTO=TCP SPT=28640 DPT=443 WINDOW=42340 RES=0x00 SYN URGP=0Se il volume di tentativi è elevato, aggiungere un limite al numero di logging evita di saturare il journal. Il parametro limit value=”60/m” riduce la scrittura a 60 righe al minuto mantenendo comunque una visibilità rappresentativa
# Rimuovere la regola senza limiti al logging
firewall-cmd --permanent \
--remove-rich-rule="rule source ipset=geoblock-${COUNTRY_CODE} log prefix=\"GEOBLOCK-${COUNTRY_LABEL}\" level=\"info\" drop"
# Aggiungere la regola con il limite di 60 righe ogni minuto
firewall-cmd --permanent \
--add-rich-rule="rule source ipset=geoblock-${COUNTRY_CODE} log prefix=\"GEOBLOCK-${COUNTRY_LABEL}\" level=\"info\" limit value=\"60/m\" drop"
# Applicare le modifiche
firewall-cmd --reloadAggiornamento automatico
I range IP assegnati ai paesi vengono riassegnati periodicamente dalle Regional Internet Registries: un prefisso che appartiene alla Cina oggi potrebbe essere riassegnato a un altro paese tra qualche mese. Senza aggiornamenti periodici l’ipset diventa gradualmente impreciso.
Lo script seguente gestisce l’aggiornamento in modo sicuro: scarica la nuova lista, verifica che sia valida prima di applicarla, sostituisce le entries esistenti e ricarica firewalld. Se il download fallisce o il file risulta malformato, lo script si interrompe senza toccare l’ipset.
cat <<'EOF' > /usr/local/bin/update-geoblock.sh
#!/bin/bash
set -euo pipefail
COUNTRY_CODE="${1:?Specificare il codice paese, es: update-geoblock.sh cn}"
COUNTRY_LABEL="${COUNTRY_CODE^^}"
IPSET_NAME="geoblock-${COUNTRY_CODE}"
ZONE_URL="https://www.ipdeny.com/ipblocks/data/countries/${COUNTRY_CODE}.zone"
NEW_FILE="/tmp/${COUNTRY_CODE}.zone.new"
OLD_FILE="/tmp/${COUNTRY_CODE}.zone.old"
LOG_FILE="/var/log/geoblock-update.log"
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') [${COUNTRY_CODE}] - $1" >> "$LOG_FILE"
}
if ! curl -sf -o "$NEW_FILE" "$ZONE_URL"; then
log "ERRORE: download fallito, ipset non modificato"
exit 1
fi
if [ ! -s "$NEW_FILE" ] || ! grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$' "$NEW_FILE"; then
log "ERRORE: file vuoto o malformato, ipset non modificato"
exit 1
fi
if ! firewall-cmd --permanent --get-ipsets | grep -qw "${IPSET_NAME}"; then
firewall-cmd --permanent --new-ipset="${IPSET_NAME}" --type=hash:net --option=family=inet
log "INFO: ipset ${IPSET_NAME} creato"
fi
firewall-cmd --permanent --ipset="${IPSET_NAME}" --get-entries > "$OLD_FILE" 2>/dev/null || true
if [ -s "$OLD_FILE" ]; then
firewall-cmd --permanent --ipset="${IPSET_NAME}" --remove-entries-from-file="$OLD_FILE"
fi
firewall-cmd --permanent --ipset="${IPSET_NAME}" --add-entries-from-file="$NEW_FILE"
if ! firewall-cmd --permanent --list-rich-rules | grep -q "${IPSET_NAME}"; then
firewall-cmd --permanent \
--add-rich-rule="rule source ipset=${IPSET_NAME} log prefix=\"GEOBLOCK-${COUNTRY_LABEL}\" level=\"info\" drop"
log "INFO: rich rule per ${IPSET_NAME} creata"
fi
firewall-cmd --reload
COUNT=$(wc -l < "$NEW_FILE")
log "OK: ${IPSET_NAME} aggiornato con ${COUNT} range IP"
rm -f "$NEW_FILE" "$OLD_FILE"
EOFe rendiamo eseguibile lo script
chmod +x /usr/local/bin/update-geoblock.shLo script accetta il codice paese come argomento e può essere riutilizzato per qualsiasi paese configurato con la stessa convenzione di naming. Prima di schedulare lo script eseguiamo un test manuale
/usr/local/bin/update-geoblock.sh cn
# Output atteso
success
success
success
cat /var/log/geoblock-update.log
# Output atteso
2026-06-19 23:49:56 [cn] - OK: geoblock-cn aggiornato con 8800 range IPAggiorniamo ora il crontab del server configurando l’aggiornamento settimanale, ad esempio ogni domenica alle 4:00
# Editare il crontab
crontab -e
# Aggiungerre la riga
0 4 * * 0 root /usr/local/bin/update-geoblock.sh cnSe si gestiscono più paesi, aggiungere una riga per ciascuno nel crontab
0 4 * * 0 root /usr/local/bin/update-geoblock.sh cn
1 4 * * 0 root /usr/local/bin/update-geoblock.sh ru
2 4 * * 0 root /usr/local/bin/update-geoblock.sh irLo script, qualora ipset e rich rule non siano presenti, li crea automaticamente senza dover effettuare i passaggi spiegati nella procedura.
Monitorare i blocchi
Con il logging attivo è possibile interrogare il journal per ottenere statistiche sui tentativi bloccati. Per il totale della giornata possiamo utilizzare
journalctl -k --since today | grep -c GEOBLOCK-CNPer visualizzare primi dieci IP sorgente per numero di tentativi
journalctl -k --since today | grep GEOBLOCK-CN \
| grep -oP 'SRC=\K[0-9.]+' \
| sort | uniq -c | sort -rn | head -10Per visualizzare le porte di destinazione più colpite
journalctl -k --since today | grep GEOBLOCK-CN \
| grep -oP 'DPT=\K[0-9]+' \
| sort | uniq -c | sort -rnPer seguire il flusso in tempo reale con un contatore aggiornato ogni cinque secondi
watch -n 5 "journalctl -k --since today | grep -c GEOBLOCK-CN"