Nftables, el sucesor de iptables

Publicado el 01 octubre 2019 por Drassill
La tecnología evoluciona constantemente, a veces a costa de imposiciones de ciertos criterios, y otros a costa de comparar los pros y los contras. Iptables ha sido siempre el principal referente en el área de firewalls para Linux; es cierto que a raíz de ahí nacieron herramientas intuitivas que intentaban hacer un uso más fácil de éste, pero generalmente se usaba dicha tecnología como referencia. Obviamente, con el paso del tiempo, se han ido optando por diferentes alternativas, pero siempre intentando mantener iptables ahí, pero con la introducción de "Debian 10 (Buster)" y las últimas versiones de Red Hat, se ha ido imponiendo su sucesor, considerando al veterano firewall como obsoleto. Hablamos de nftables.

Nftables es un software creado por netfilter que podría considerarse como la evolución natural de nftables, que ha intentado adaptar la sintaxis a los nuevos tiempos, con el fin de que la escritura de las reglas de este software sea algo más intuitiva que la de su predecesor.
La dinámica que utilizar este software es bastante parecida a la de iptables, en el sentido de que tal y como ocurre con su predecesor,  nftables se divide por tablas, las cuales tienen dentro cadenas, dentro de las cuales se añaden las reglas. En caso de haber usado iptables con anterioridad dichos conceptos os serán familiares, en caso contrario es importante tenerlos claros antes de continuar, ya que en caso contrario el entendimiento de la herramienta será muy difícil. Además es importante saber que las reglas de nftables se pueden definir o bien directamente en consola o bien mediante un fichero de configuración.
TABLAS
Las tablas están compuesta por una o varias cadenas; en iptables éstas seguían una lógica, ya que se agrupaban en cadenas de tipo filter, nat y mangle; en nftables podemos hacer que nuestras tablas tengan las cadenas que queramos, pero lo ideal, en términos de gestión es que sigan una lógica parecida, aunque no es obligatorio. En resumen, una tabla no es más que una o a varias cadenas agrupadas bajo un nombre común desde el cual se hacer referencia a éstas.  Las tablas pueden tener el nombre que queramos, pero es importante tener en cuenta que éstas pertenecen a diferentes familias, y que dependiendo del tipo, se tendrán en cuenta un tipo de paquete de red u otro:
  • ip: La familia por defecto, en caso de que no le digamos nosotros otra cosa. Éste solamente tendrá en cuenta los paquetes de red IPv4.
  • ip6: Como su propio nombre indica, tendrá en cuenta los paquetes IPv6.
  • inet: Una familia híbrida que contemplará ambos protocolos de IP, y que, dependiendo de a lo que hagamos referencia, tendrá en cuenta un tipo de IP u otra, o ambas, Por ejemplo si se hace referencia a una IPv4, se aplicará la regla para los paquetes IPv4, pero si hacemos referencia solamente a un puerto (por ejemplo el 80), sin especificar una IP de origen o de destino, se tendrán en cuenta ambos protocolos. Esta familia no funciona en las cadenas NAT (de esto hablaremos más adelante), al menos de momento, debido al bug 1173, con lo en caso de querer usar cadenas de dicho tipo para paquetes IPv4 e IPv6, habría que crear 2 tablas separadas, una perteneciente a la familia ip y la otra a ip6.
  • arp: Esta familia solamente tendrá en cuenta el tráfico ARP.
  • bridge: Esta familia entraría en acción en las interfaces de tipo bridge, que hayan sido creadas en el sistema.
  • netdev: Aquí se tratará el paquete antes de que sea procesado por la interfaz de red. No se tiene en cuenta si es IPv4 ni IPv6; desde aquí se ve todo, y es útil para el bloqueo de ataques DOS o DDOS, ya que lo deniega antes de ser procesado, tomándole menos tiempo al sistema el desecharlo.

La primera vez que se instala nftables, crea una tabla llamada filter del tipo inet, pero es importante conocer cómo agregar nuevas tablas. 


Para ello, antes de empezar a hacer nada, es muy recomendable cargar todos sus módulos de nftables en el kernel, ya que en caso contrario nos podremos encontrar errores tales como 'Could not process rule: No such file or directory'. Para ello lo recomendado por los propios desarrolladores del proyecto, es crear el fichero /etc/modules-load.d/modules-nftables.conf e introducir el siguiente contenido para después reiniciar el equipo:
nf_conntrack
nf_conntrack_ipv4
nf_conntrack_ipv6
nf_defrag_ipv4
nf_defrag_ipv6
nf_nat
nf_nat_ipv4
nf_tables
nf_tables_inet
nf_tables_ipv4
nf_tables_ipv6
nfnetlink
nft_counter
nft_ct
nft_hash
nft_limit
nft_log
nft_meta
nft_rbtree
nft_reject
nft_reject_inet
nft_reject_ipv4
nft_reject_ipv6

Con todos los preparativos realizados, para crear una nueva tabla escribiremos la siguiente sintaxis:
nft add table 'familia' 'nombre'
Por ejemplo, para crear una tabla llamada "filter" que contemple los paquetes IPv4 e IPv6, haremos:
nft add table inet filter

Con ello ya tendríamos nuestra primera tabla creada, tabla que puede ser borrada o que cuyo contenido puede ser listado o borrado sustituyendo add por delete, list y flush, respectivamente.
CADENAS
Las cadenas son simples contenedores de reglas. A diferencia de en iptables, que ya había una serie de cadenas predefinidas, tales como INPUT, OUTPUT, en nftables no existe ninguna por defecto, sino que tienen que ser creadas. La creación de cualquier cadena tendría una sintaxis como la siguiente:
nft add chain 'familia' 'tabla' 'nombre_cadena' {type 'tipo' hook 'estado_del_paquete' priority 'prioridad'; policy 'politica';} 
Las familias y las tablas ya las conocemos, con respecto al tipo, tenemos cadenas de tres tipos:
  • filter: Este tipo de cadena se encarga del filtrado de paquetes.
  • route: Permite rutar los paquetes salientes
  • nat: Se utiliza para aplicar reglas de nateo de paquetes, es decir, para la conversión de IPs.

Por otro lado tenemos el estado del paquete, el cual viene 'heredado' de su predecesor, iptables, y que dependiendo del tipo que hayamos escogido, se podrá hacer referencia a un estado u otro del paquete. Los estado sería input, output, forward, prerouting y postrouting. Los tres primeros son intuitivos, mientras que prerouting se utilizaría para tratar una interfaz que está a punto de ser rutada  y postrouting se utilizaría para tratar la interfaz tras haber sido rutada.  
Obviamente no todos los tipos se pueden usar en todas las familia al igual que no todos los estados se pueden usar para todos los tipos, con lo que a continuación dejo una pequeña tabla que puede servirnos como guía orientativa que nos puede servir de ayuda en la creación de cadenas:

Además, podemos definir la prioridad que tendrá la cadena con respecto al resto de la misma tabla. Aquella que tenga el número más bajo (pudiendo ser negativo), será aquella que se ejecutará antes.
Además podemos también definir, de forma opcional, la política que queremos que tenga la cadena por defecto, de entres las cuales las más comunes serían: accept o drop, es decir permitir o denegar. En caso de no definir ninguna política, nftables define automáticamente que la política será accept.
Con toda esta información en mano, podemos crear varias cadenas de prueba. Por ejemplo, siguiendo la lógica del ejemplo inicial, en el que hemos creado una tabla llamada filter, vamos a crear las 3 cadenas de filtrado básicas que ya venían predefinidas en iptables, las cuales se llamaban input, output y forward. Estas cadenas, para este ejemplo, estarán abiertas por defecto, es decir que serán 3 cadenas vacías que no harán nada por defecto; solo existir y que dejarán que las reglas definidas dentro de éstos sean los que dictaminen los comportamientos:
Para crear la cadena llamada input, que se encargará de contener las reglas de los paquetes entrantes, haremos:
nft add chain inet filter input { type filter hook input priority 0; policy accept;}

Para las cadenas output y forward haríamos lo equivalente:
Output:
nft add chain inet filter output { type filter hook output priority 0; policy accept;}

Forward:
nft add chain inet filter forward { type filter hook forward priority 0; policy accept;}

REGLAS
Finalmente con todos estos preparativos creados, podemos empezar a crear nuestras reglas. Dichas reglas siempre tendrán que ir dentro de una cadena perteneciente a una tabla; de ahí todos los preparativos realizados hasta ahora.  Dichas reglas, pueden parecer algo abrumadoras al principio por las gran cantidad de posibilidades que ofrecen, pero a su favor hay que decir que son bastante intuitivas. La creación de reglas sigue esta sintaxis
nft add rule 'familia' 'tabla' 'cadena' 'expresión'
Inicialmente todo estaría claro a excepción de la expresión en sí, que sería lo que definiría la regla. Las expresiones son unas reglas que tienen que coincidir bajo ciertas circunstancias y que tras las cuales se deciden ciertas acciones. Esto dicho así puede parecer lioso, pero al final sería definir la regla, su argumento y la acción que se realizará al respecto... Es mejor empezar con un ejemplo sencillo que definiría la regla más sencilla posible; vamos a crear una regla que impida que puedan acceder al puerto 80. La regla se añadiría tal que así:
nft add rule inet filter input tcp dport 80 drop

En esta regla estaríamos diciendo que todo el tráfico TCP al puerto 80 sería bloqueado, y como podéis observar la expresión en sí es intuitiva. Existen muchísimas expresiones y reglas, y aquí sería imposible abarcar todas, pero las más comunes serían:
oifname 'nombre de la interfaz SALIENTE'
iifname 'nombre de la interfaz ENTRANTE'
ip protocol 'protocolo'
ip daddr 'direccion IPv4 de DESTINO'
ip saddr 'direccion IPv4 de ORIGEN'
ip6 daddr 'direccion IPv6 de DESTINO'
ip6 saddr 'direccion IPv6 de ORIGEN'
tcp dport 'puerto de DESTINO'
tcp sport 'puerto de ORIGEN'
udp dport 'puerto de DESTINO'
udp sport 'puerto de ORIGEN'
ct state 'new | established | related | invalid'

Dichas expresiones pueden combinarse entre sí dentro de la misma regla y siempre tienen que terminar con una política; las más comunes son las mencionadas antes: accept y drop. Pero existen también las políticas queue, continue y return; además de una política especial, la cual solamente se puede usar en las cadenas de tipo nat, llamada masquerade. Esta última lo que haría sería transformar la IP de origen del tráfico saliente en la IP de dicha interfaz, lo cual es común en las labores de NATeo, pues lo que se hace es transformar una IP en otra.Usando lo de arriba como referencia podemos hacer combinaciones como estas:
Bloquear todo el trafico al puerto 22 desde el rango de IPs 192.168.1.0/24:
nft add rule inet filter input tcp dport 22 ip saddr 192.168.1.0/24 drop

Bloquear todo el trafico entrante nuevo (ct state new), proveniente del puerto 80. Es decir que nosotros podamos conectarnos a Internet, pero que no se puedan conectar a nuestro puerto 80 desde fuera:
nft add rule inet filter input ct state new drop

Bloquear los pings que accedan por la interfaz con nombre enp0s3:
nft add rule inet filter input iifname enp0s3 ip protocol icmp drop

Esto solo sería una parte de lo que puede hacer nftables, pero nos sirve para hacernos una idea global de la potencia que nos puede ofrecer esta utilidad.
Para listar todo aquello que hemos creado (las tablas, cadenas y reglas) tenemos el comando:
nft list ruleset

El cual nos mostrará nuestra configuración completa; configuración que podemos volcar en el fichero de configuración de nftables con el fin de que sea permanente. Esto se puede hacer a mano o mediante el comando:
nft list ruleset > /etc/nftables.conf

En caso de querer eliminar una regla que hayamos creado, es importante saber que, de momento, no es tan sencillo como sustituir nft add por nft delete; desgraciadamente, es necesario ejecutar primero el comando:
nft list ruleset -a

Allí veremos que al lado de cada tabla y cada regla aparecerá: #handle, seguido de un número; para eliminar la regla, tendremos que hacer referencia al handle con dicho numero. Si por ejemplo la regla de inet filter tuviese el handle 7, tendríamos que escribir:
nft delete rule inet filter input handle 7

Esto último no es del todo intuitivo y lo ideal sería poder eliminarlo de forma "natural", pero de momento es posible.
En definitiva, nos encontramos ante una versión más potente e intuitiva que iptables, que si bien ofrece varias ventajas, también posee un par de desventajas. El hecho de tener que conocer el handle de la regla para poder eliminarla, no es del todo afinado, al igual que la incapacidad de poder usar la familia inet para la tabla nat, o que sea necesario crear la tabla nat junto con sus correspondientes cadenas desde cero para hacer un simple 'masquerade' del tráfico postrouting.
Aún así es recomendable acostumbrarse a utilizarla, pues Debian 10 ya no usa iptables y en un futuro cercano nos veremos obligados a utilizar este firewall en el resto de distribuciones. Además, seguramente con el paso del tiempo irá puliendo esos pequeños detalles que hacen que esta potente herramienta no sea del todo perfecta.
Espero que os haya resultado útil.
Saludos.