Introduzione a nftables

Questa guida prende ampio spunto da questo documento.

Introduzione


Quest’estate durante il passaggio a Buster ho ahimè scoperto che iptables aveva le ore contate e che sarebbe stato sostituito da nftables. Notizia triste perché ho un ricordo particolare legato a iptables. Molti (troppi) anni fa infatti, mentre leggevo il “Packet Filtering Howto”, mi colpì questa frase di Paul ‘Rusty’ Russell:

While I’m here, I want to clear up some people’s misconceptions: I am no kernel guru. I know this, because my kernel work has brought me into contact with some of them: David S. Miller, Alexey Kuznetsov, Andi Kleen, Alan Cox. However, they’re all busy doing the deep magic, leaving me to wade in the shallow end where it’s safe.

Traducendo grossolanamente: l’autore di iptables, uno dei software più complessi nel mondo Linux, ringrazia i suoi amici hacker che, occupati come sono a fare le loro magie nelle profondità del kernel, gli permettono di guadare tranquillamente nelle secche dove è più sicuro. Prego? Ok che viviamo tutti sulle spalle dei giganti, ma allora quale sarebbe il posto di un comune mortale come me? Quello della polvere spostata dai loro passi??

Ciancio alle bande


Ok, dopo aver dato l’addio ad iptables ed esserci asciugati le lacrime possiamo cominciare.

nftables utilizza la stessa infrastruttura di netfilter (immagine rubata dal wiki di nftables):

                                             Local
                                            process
                                              ^  |      .-----------.
                   .-----------.              |  |      |  Routing  |
                   |           |-----> input /    \---> |  Decision |----> output \
--> prerouting --->|  Routing  |                        .-----------.              \
                   | Decision  |                                                     --> postrouting
                   |           |                                                    /
                   |           |---------------> forward --------------------------- 
                   .-----------.

Nell’ottica di semplificare il più possibile questa guida, ci concentreremo solo su 2 hook: input e output. Penso che sia chiaro, comunque con input si intende tutto ciò che da internet entra nel nostro PC, con output tutto ciò che dal nostro PC parte verso internet.

Innanzitutto creiamo il contenitore principale in nftables: la tabella. Le tabelle sono contenitori di catene, le catene sono contenitori di regole.

nft add table ip filter

A differenza di iptables, nftables non crea catene di default. Cominciamo con la prima: siccome il nostro PC non fa da router, tutto ciò che riceviamo e che non è diretto a noi lo buttiamo. Creiamo quindi la catena FORWARD associata all’hook forward con policy drop.

nft add chain ip filter FORWARD { type filter hook forward priority 0\; policy drop\; }

Per resettare tutto:

nft flush ruleset

Questo cancella tutto: regole, catene e tabelle. Creiamo le catene INPUT e OUTPUT:

nft add chain ip filter INPUT { type filter hook input priority 0\; policy drop\; }
nft add chain ip filter OUTPUT { type filter hook output priority 0\; policy drop\; }

In questo modo la rete è bloccata se non aprite qualche porta. Apriamo per esempio la porta udp 53 (per la risoluzione dei domini):

nft add rule filter OUTPUT udp dport 53 accept

Per aprire più porte:

nft add rule filter OUTPUT tcp dport { 22, 80, 443 } counter accept

L’opzione “counter” serve per contare quanti pacchetti/bytes sono passati.

Tutto molto bello, peccato però che non sapremo mai se qualcuno ci risponderà perché al momento stiamo bloccando tutto in ingresso:

nft add rule filter INPUT ct state established,related accept

Così è già meglio! Adesso almeno riusciamo a navigare!

Scripting


Facciamo un passettino indietro: creiamoci lo script per caricare le regole. Sarà molto semplice e “molto uguale” a quello nella guida di iptables.

1. Funzione che stampa le regole:

nft_list() {
    nft list ruleset
}

2. Funzione che cancella le regole:

nft_clear() {
    nft flush ruleset
}

3. Funzione che carica le regole. E qui la novità: usiamo la notazione di nft.

nft_load() {
    nft -f - << EOF
EOF
}

Qualsiasi cosa dentro “EOF” è interpretato come se fosse un script nft, non bash. Non useremo più “nft add” ad ogni riga come abbiamo fatto fino ad adesso, ma addotteremo la sintassi di nft: questo, oltre a rendere lo script più leggibile, è anche consigliato dagli sviluppatori perché ci permette di caricare le regole in modo atomico. Ok io e molti come me, probabilmente, non noteremo mai la differenza, ma male non fa.

4. Il “main”

case $1 in
    t|test)
        nft_clear
        nft_load
        nft_list
        sleep 10
        nft_clear
        nft_list
        exit 0
        ;;
    l|load)
        nft_clear
        nft_load
        nft_list
        exit 0
        ;;
    c|clear)
        nft_clear
        nft_list
        exit 0
        ;;
esac

nft_list

Il case controlla l’unica opzione e con

  • “c” cancella tutte le regole
  • “l” carica le regole
  • “t” carica le regole, aspetta 10 secondi e poi le cancella (utile per non rimanere fuori da un server remoto)
  • senza opzione stampa le regole

Cominciamo con qualcosa di scolastico (nel senso di inutile ma esemplificativo): vogliamo contare quanti pacchetti tcp, udp e icmp entrano:

    nft -f - << EOF

    table ip filter {
        chain tcp-chain {
            counter
        }

        chain udp-chain {
            counter
        }

        chain icmp-chain {
            counter
        }
        chain INPUT {
            type filter hook input priority 0; policy accept;

            ip protocol vmap { tcp : jump tcp-chain, udp : jump udp-chain, icmp : jump icmp-chain }
        }
}
EOF

Intanto notiamo la sintassi più chiara e stringata di nft; abbiamo creato 3 catene, una per ogni protocollo: tcp-chain, udp-chain e icmp-chain. Dopodiché nella catena INPUT abbiamo la direttiva vmap che dice: “controlla l’ip protocol del pacchetto: se è di tipo tcp, salta nella catena tcp-chain; se è udp, salta nella catena udp-chain, etc…” E’ importante notare che qui non c’è alcuna direttiva drop o accept. Quindi ogni pacchetto entra dentro una delle 3 catene, viene contato e poi ritorna nella catena INPUT per essere valutato da altre eventuali regole.

La direttiva “set” serve per creare degli insiemi di porte, ip, etc… Per esempio, poco sopra abbiamo usato questa regola:

nft add rule filter OUTPUT tcp dport { 22, 80, 443 } counter accept

Usando set diventa:

    table ip filter {
        set tcp_ports { type inet_service; elements = { 22, 80, 443 } }
        chain INPUT {
            type filter hook input priority 0; policy accept;

            tcp dport @tcp_ports counter accept
        }       
}

Utile per esempio se vogliamo creare una blacklist:

    table ip filter {
        set blackhole { type ipv4_addr; timeout 1d; }
                                                                                   
        chain INPUT {
            type filter hook input priority 0; policy accept

            # block the bad guys
            ip saddr @blackhole drop
        }
    }

Abbiamo creato un set vuoto che accetta indirizzi IPv4. Ora ogni pacchetto proveniente da indirizzi IP presenti nel set blackhole verrà scartato per 1 giorno. Per aggiungere indirizzi alla blacklist:

nft add element ip filter blackhole { 192.168.1.4 }

Altro esempio interessante è la creazione di una whitelist:

    table ip filter {
        set whitelist { type ipv4_addr; elements = { 192.168.1.1, 192.168.1.2, 192.168.1.3, 192.168.1.4 } }

        chain INPUT {
            type filter hook input priority 0; policy drop

            # accept everything from our VIPs
            ip saddr @whitelist accept
        }
    }

Il discorso è identico a quella della blacklist, solo che in questo caso la lista è già popolata.

Mettiamo tutto a sistema


Bene, ora fermiamoci un attimo e ragioniamo su quello che vogliamo ottenere dal nostro firewall. Abbiamo un PC che non offre servizi all’esterno, quindi possiamo bloccare tutto quello in arrivo tranne ciò che è una risposta ad una nostra precedente connessione. Useremo firefox per navigare, ci connetteremo ai nostri server con ssh, useremo thunderbird per ricevere/inviare la posta, etc… Abbiamo quindi bisogno di aprire un po’ di porte in uscita e di lasciare che la comunicazione prosegua.

    table ip filter {
        set tcp_ports { type inet_service; elements = { 22, 25, 43, 80, 143, 443, 587, 993, 9001 }}
        set udp_ports { type inet_service; elements = { 53, 123 }}
        set whitelist { type ipv4_addr; elements = { 192.168.1.1, 192.168.1.2, 192.168.1.3, 192.168.1.4 }}
        set blackhole { type ipv4_addr; }

        chain tcp-chain  { counter; }
        chain udp-chain  { counter; }
        chain icmp-chain { counter; }

        chain INPUT {
            type filter hook input priority 0; policy drop

            # accept any localhost traffic
            iif lo accept

            ip protocol vmap { tcp : jump tcp-chain, udp : jump udp-chain, icmp : jump icmp-chain }

            # accept everything from our VIPs
            ip saddr @whitelist accept

            # block the bad guys
            ip saddr @blackhole drop

            log prefix "nftables: " level info

            # accept traffic originated from us
            ct state established,related accept
           
            icmp type {echo-request, echo-reply} counter accept
        }

        chain OUTPUT {
            type filter hook output priority 0; policy drop

            # accept any localhost traffic
            oif lo accept
            
            # accept everything for our VIPs
            ip daddr @whitelist accept

            tcp dport @tcp_ports accept
            udp dport @udp_ports accept

            icmp type {echo-request, echo-reply} counter accept
        }

        chain FORWARD {
            type filter hook forward priority 0; policy drop
        }
    }

Molte cose le abbiamo già viste, mi soffermo solo sulle novità. Permettiamo il traffico interno:

# chain INPUT
iif lo accept
# chain OUTPUT
oif lo accept

Logghiamo tutti pacchetti tranne quelli che passano attraverso whitelist e blacklist (tornerà utile dopo):

log prefix "nftables: " level info

Permettiamo il ping:

icmp type {echo-request, echo-reply} counter accept

rsyslog


Cosa che faccio sempre è reindirizzare l’output del firewall su un file dedicato. Creiamo il file /etc/rsyslog.d/nftables.conf con:

if $msg contains 'nftables:' then /var/log/nftables.log
&stop

Dopo aver ricaricato rsyslog con:

service rsyslog force-reload

avremo l’output di nftables nel file /var/log/nftables.log grazie alla direttiva “log” inserita più sopra che aggiunge il prefisso “nftables:” ad ogni riga. Questo serve più che altro per non spammare il file /var/log/syslog.

That’s all folks!!

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *