nftables, et son interface nft, est le (pas si) nouveau pare-feu de Linux (remplaçant de Netfilter, dont l'interface était iptables).
J'ai déjà rapporté quelques syntaxes ici (à la fin) et là.
Désormais, on peut appliquer une même règle à UDP et à TCP. Exemple pour le DNS (qui fonctionne sur UDP et sur TCP) : meta l4proto { udp, tcp } th dport 53 accept. Exemple pour Torrent : meta l4proto { udp, tcp } th dport 6881-6891 accept. Avant, ce n'était possible que via une surcouche comme ufw.
Les sélecteurs meta permettent plein d'autres choses, j'vais y revenir infra.
Pour appliquer une même règle en IPv4 et en IPv6, il faut utiliser une table de type inet. Avant, cela se faisait uniquement avec une surcouche comme ufw. Pour appliquer une règle uniquement aux flux IPv6 ou IPv4 au sein d'une table inet, on peut utiliser meta protocol ip6 ou meta nfproto ipv6. Notons que nftables n'a pas besoin d'une telle précision pour une limitation du débit via un ensemble (sets) : la version d'IP sera déduite du type du set.
Comme quand ils étaient un sous-système dédié du noyau Linux, les ensembles (sets), nommés ou non, permettent d'appliquer une même règle à plusieurs interfaces réseau, adresses IP, ports, ou, nouveauté, à plusieurs types (ex. les types ICMP). (en sus des ipsets, iptables permettait de factoriser les ports avec son module multiport.)
Plus forts que les ensembles, il y a les dictionnaires (ou tableaux associatifs). Pratique pour du couplage géographique. Quand c'est poussé à l'extrême, notamment pour les règles de NAT, je trouve ça illisible.
Toujours aucun moyen de factoriser un même type ICMP et ICMPv6 (comme echo-request ‒ ping), ni IPv4 et IPv6 dès que l'on peut préciser une source ou une destination (même avec un nom de domaine, c'est soit l'un, soit l'autre). nft interprète à sa sauce certaines commandes, donc une abstraction qui ajouterait deux règles à partir d'une n'est pas inconcevable.
Avec Netfilter/iptables, le trafic arrive dans des chaînes prédéfinies et incontournables (ex. : chaîne INPUT de la table filter). On pouvait renvoyer le trafic dans des sous-chaînes personnelles via des règles recourant à la cible JUMP.
Désormais, il n'y a plus de chaînes prédéfinies. Exemple : la chaîne INPUT existe si tu la nommes ainsi.
On peut accrocher (hook) des chaînes à plusieurs endroits du traitement des paquets. Une priorité permet d'ordonner les chaînes. Un paquet traversera toutes les chaînes dans l'ordre des priorités jusqu'à rencontrer un accept final ou un drop.
J'insiste : un drop est définitif, le traitement d'un paquet s'arrête, le paquet n'ira pas dans les chaînes de priorités plus hautes. En revanche, un accept peut être remise en cause par une chaîne ultérieure. Source.
Cet enchaînement de chaînes est plus pratique et lisible pour la cohabitation entre logiciels qui tripotent le pare-feu. Exemples : cumul de règles personnelles et de règle libvirt ; cumul avec fail2ban.
On peut capturer des flux réseau par propriétaire ou par groupe d'une socket d'émission : skuid, skgid.
Très pratique pour filtrer les flux sortants quand la destination n'est pas connue (ex. : flux sortants d'un serveur emails ou d'un récursif DNS, mises à jour du système, etc.). Ça autorise de ne pas permettre un flux sortant à destination de tel port (ou autre) à tous les logiciels (une application compromise reste donc confinée aux ports qu'on lui a autorisé).
iptables permettait ça avec son module owner. (Il pouvait même filtrer par nom du programme, mais uniquement sur un système non-SMP.)
La nouveauté par rapport à il y a 15-20 ans, c'est qu'un plus grand nombre de logiciels s'exécutent avec un utilisateur dédié. Exemple : apt = utilisateur _apt.
Avec iptables, une règle pouvait avoir une unique cible. Il fallait donc une règle pour journaliser (log) et une règle pour agir. nftables permet de cumuler counter, log et une action (comme drop). \o/
La cible log permet toujours d'ajouter un préfixe (log prefix "TEST", par ex.) afin de distinguer, dans le journal, la provenance d'une trace (= quelle règle l'a journalisé).
Plusieurs caractéristiques d'un paquet ne sont pas journalisées par défaut, comme le proprio de la socket. Il est possible d'activer ceci : log flags skuid.
Pour debug, on peut aussi utiliser nft monitor dont j'ai parlé ici.
Par défaut, la limitation de débit s'applique par nombre de paquets ou par débit (quantité transférée par intervalle de temps).
Pour limiter le nombre de connexions (pour une IP ou un réseau ou…), il suffit de l'appliquer aux seuls paquets d'initialisation d'une connexion TCP (drapeau SYN), voir ci-dessous, ou d'utiliser le suivi des connexions (ce qui est déconseillé en cas d'attaque par déni de service).
Avant : icmp type echo-request limit rate 10/minute accept.
Après : ct state new icmp type echo-request limit rate 10/minute accept.
Dans le premier cas, une même commande ping n'obtiendra plus de réponse après environ 10 secs. Dans le deuxième cas, si.
(Je rappelle qu'une même connexion réseau peut acheminer plusieurs requêtes applicatives. Ex. : dès HTTP/1.1, un client peut demander plusieurs ressources web dans une même connexion TCP, c'est le pipelining HTTP. Dès lors, une limite en nombre de connexions réseau peut impliquer un plus grand nombre de requêtes applicatives.)
Pour appliquer une règle en fonction des flags TCP, la syntaxe a un peu évolué par rapport à iptables.
Pour contrer les attaques SYN flood.
Pour agir au-delà d'une certaine quantité cumulée de données (pas d'un débit, ça, c'est de la limitation de débit, dont j'ai déjà cause ici).
La vérification de la syntaxe d'un fichier de règles s'effectue au chargement de celui-ci (nft -f). nft ne propose aucune option pour vérifier sans charger.
De même, il vaut mieux recharger (systemctl reload nftables) que redémarrer le pare-feu (restart), car restart implique stop qui implique nft flush ruleset qui purge les règles en cours d'application. Si la syntaxe de la configuration est incorrecte, pouf, plus de pare-feu. Un « flush ruleset » dans le fichier de règles ne sera exécuté que si la syntaxe de l'ensemble du fichier est OK, donc aucune absence du pare-feu.
Quand on gère les flux de son infrastructure, on est tenté de filtrer les flux vers des destinations tierces qu'on ne maîtrise pas en utilisant des noms de domaine (ex. : ip daddr toto.example accept).
Sauf qu'au redémarrage, on se retrouvera sans pare-feu : « Error: Could not resolve hostname: Temporary failure in name resolution ».
Hé oui, on charge toujours un pare-feu avant le réseau, précisément pour protéger la machine dès le début. Pour confirmer cela : systemctl list-dependencies nftables. Charger les règles après le réseau est une mauvaise idée.
Déporter le chargement des règles contrôlant les flux sortants est compliqué (en sus d'être une mauvaise idée pour la raison que je viens de rappeler). Il faudrait faire un jeu de règle séparé, avec une unit systemd séparée pour le charger, ce qui rend propice aux erreurs et oublis le chargement intégral du pare-feu (je rappelle que iptables -D n'a pas d'équivalent, pour supprimer une règle, il faut récupérer son handle).
Utiliser des noms, c'est faire dépendre d'autrui son pare-feu, ce qui est une giga mauvaise idée. Si la destination ajoute une deuxième IP à son nom, vlam, le pare-feu ne démarrera plus (« Error: Hostname resolves to multiple addresses »). Si elle se met à pratiquer l'équilibrage de charge avec un TTL court, vlam, la règle de pare-feu laissera passer que par intermittence.
Je préconise de tout faire pour filtrer sur d'autres critères (skuid, etc.), quitte à ce que ça soit moins précis (autoriser tout un port, au lieu de destination + port).
Si un filtrage à partir d'un nom de domaine est absolument nécessaire, le moins pire est d'utiliser des sets avec un script pour les remplir. Ce script, qui sera déclenché quand le réseau sera activé, devra prendre en compte les différentes possibilités : plusieurs adresses IP, CNAME, absence de réponse, erreur de résolution, etc. Mais, au moins, un défaut n'entraînera pas le pare-feu dans sa chute.