Cybersecurity - Linux - Network

Geo-blocking con firewalld

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 firewalld

Prima 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 --reload

Aggiungere 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 --reload

Per 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}.zone

Il 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=inet

Possiamo ora caricare i prefissi scaricati nell’ipset

firewall-cmd --permanent \
  --ipset=geoblock-${COUNTRY_CODE} \
  --add-entries-from-file=/tmp/${COUNTRY_CODE}.zone

Creare 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 --reload

Verificare 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" drop

Per controllare se un IP specifico appartiene all’insieme

firewall-cmd --ipset=geoblock-${COUNTRY_CODE} --query-entry=1.0.1.0/24

# output atteso
yes

Aggiungere 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 --reload

Per consultare i pacchetti bloccati

journalctl -k | grep GEOBLOCK-CN
journalctl -k -f | grep GEOBLOCK-CN   # in tempo reale

Ogni 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=0

Se 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 --reload

Aggiornamento 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"
EOF

e rendiamo eseguibile lo script

chmod +x /usr/local/bin/update-geoblock.sh

Lo 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 IP

Aggiorniamo 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 cn

Se 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 ir

Lo 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-CN

Per 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 -10

Per visualizzare le porte di destinazione più colpite

journalctl -k --since today | grep GEOBLOCK-CN \
  | grep -oP 'DPT=\K[0-9]+' \
  | sort | uniq -c | sort -rn

Per seguire il flusso in tempo reale con un contatore aggiornato ogni cinque secondi

watch -n 5 "journalctl -k --since today | grep -c GEOBLOCK-CN"