nftables remplace iptables comme framework de filtrage réseau par défaut sur la plupart des distributions Linux modernes depuis plusieurs années, mais cette migration reste rarement terminée en pratique : une couche de compatibilité fait tourner d’anciennes règles iptables sans jamais forcer leur réécriture native, un choix pragmatique qui a ses propres pièges.

Ce que nftables change réellement

iptables gérait le filtrage IPv4 séparément d’ip6tables (IPv6), arptables (ARP) et ebtables (pont Ethernet), quatre outils distincts avec leur propre syntaxe. nftables unifie ces quatre domaines dans un seul framework, avec un seul langage de règles, éliminant la duplication de logique qui existait auparavant entre iptables et ip6tables pour des règles conceptuellement identiques.

# nftables : une seule table couvre IPv4 et IPv6
# ensemble, là où iptables exigeait deux commandes séparées
nft add rule inet filter input ip saddr 10.0.0.0/8 accept
nft add rule inet filter input ip6 saddr fd00::/8 accept

L’avantage silencieux : le remplacement atomique de règles

La comparaison la plus souvent citée oppose nftables à un script bash enchaînant plusieurs commandes iptables : pendant les fractions de seconde que met le script à s’exécuter, le pare-feu est dans un état partiellement configuré. C’est exactement ainsi que le wiki nftables pose le problème, et nft -f le supprime — le fichier entier est lu, l’objet de configuration construit en mémoire à côté de l’existant, puis échangé en une seule opération atomique.

Il faut préciser contre quoi cette comparaison tient. Le même wiki situe l’équivalence sans détour : préfixer le fichier d’une directive de flush donne « le remplacement atomique de ruleset équivalent à ce que fournit iptables-restore ». L’atomicité n’est donc pas ce que nftables invente face à un iptables bien utilisé ; c’est ce qu’il rend accessible sans script. Ce qu’il ajoute réellement, c’est la portée : le format iptables-save/iptables-restore est structuré par table — un bloc *filterCOMMIT par table — et le flush comme la restauration s’y expriment table par table (--noflush porte sur « le contenu de la table respective », -T ne restaure qu’une table nommée). Un flush ruleset en tête de fichier nft couvre toutes les tables et toutes les familles dans la même transaction.

Et il y a un piège de premier ordre dans ce fichier :

# Sans directive de flush en tête de fichier, nft -f
# AJOUTE au ruleset existant. Atomiquement, mais il ajoute :
# recharger trois fois donne trois exemplaires des règles.
head -1 new-ruleset.nft
# flush ruleset   <- la ligne qui fait du chargement un remplacement

nft -f new-ruleset.nft

flush table filter suffit pour une table, mais ne vide pas les sets qu’elle contient : c’est flush ruleset qu’il faut pour repartir réellement à zéro.

La couche de compatibilité : iptables-nft

La plupart des distributions modernes font tourner iptables-nft, une couche de compatibilité qui traduit les commandes iptables classiques vers le backend nftables, sans jamais exiger de réécrire les scripts existants. Cette traduction fonctionne bien pour l’essentiel des cas d’usage courants, mais crée un piège réel : des règles nftables natives et des règles iptables traduites via cette couche peuvent coexister dans des tables séparées, invisibles les unes aux autres au premier coup d’œil.

# Vérifie si iptables tourne en mode traduction
# vers nftables, ou en mode legacy natif
iptables --version
# iptables v1.8.7 (nf_tables) : mode traduction
# iptables v1.8.7 (legacy)    : mode natif historique

firewalld, ufw, Docker : le frontend ne dit pas quel backend écrit

Sur un serveur moderne, presque personne n’écrit ses règles à la main : un frontend s’en charge. Et les trois plus répandus ne répondent pas de la même façon à la question « qui écrit dans quel sous-système ».

firewalld la tranche explicitement. Sa configuration porte un paramètre FirewallBackend dont les valeurs possibles sont nftables — le défaut — ou iptables, ce dernier documenté comme déprécié et voué à disparaître. Une exception est écrite noir sur blanc dans le manuel : les règles direct et passthrough utilisent toujours les backends historiques iptables, ip6tables et ebtables. Une configuration firewalld qu’on croit entièrement native garde donc un pied dans x_tables dès que quelqu’un a posé une règle directe. Second détail qui surprend : NftablesTableOwner vaut yes par défaut, ce qui donne à firewalld la propriété exclusive du ruleset généré et empêche toute autre entité de le modifier. Quand un ajout manuel dans cette table ne tient pas, ce n’est pas un bug, c’est le réglage.

ufw ne tranche rien, parce que la question ne se pose pas pour lui. Son manuel le décrit comme « en partie un frontend pour iptables-restore » et précise qu’il charge ses fichiers de règles dans le noyau via iptables-restore et ip6tables-restore. ufw ne parle donc jamais nftables : il parle iptables. Là où la commande iptables du PATH est iptables-nft, ses règles atterrissent quand même dans nf_tables, traduites — le mécanisme de la section précédente, appliqué sans que personne ne l’ait décidé.

Docker a son propre commutateur : l’option de daemon firewall-backend choisit entre iptables et nftables, avec iptables par défaut, et la documentation précise que pour les réseaux bridge les deux offrent les mêmes fonctionnalités. L’avertissement qui accompagne ce réglage mérite d’être lu avant de le basculer : le support nftables, introduit dans Docker 29.0.0, est documenté comme expérimental, « les options de configuration, le comportement et l’implémentation sont tous susceptibles de changer ». Il ne peut pas être activé quand le daemon tourne en mode Swarm, les règles des réseaux overlay n’ayant pas encore été migrées. Et dans ce mode, Docker n’active pas lui-même l’IP forwarding et ne crée pas de politique drop par défaut — deux choses qu’il fait avec le backend iptables, et dont l’absence ne se voit pas tant qu’on ne la cherche pas. Deux interactions y sont documentées et méritent d’être connues avant de diagnostiquer quoi que ce soit. Avec firewalld actif, Docker crée une zone docker de cible ACCEPT et une politique docker-forwarding qui autorise le forwarding depuis n’importe quelle zone vers celle-ci. Avec ufw, la documentation Docker est franche : les deux « utilisent les règles de pare-feu d’une manière qui les rend incompatibles entre eux ». Le trafic vers un port publié est routé dans la table nat, donc détourné avant d’atteindre les chaînes INPUT et OUTPUT sur lesquelles ufw s’appuie, ce qui revient à ignorer sa configuration.

Le piège du diagnostic : deux vues qui ne montrent pas tout

Un administrateur qui vérifie uniquement iptables -L peut manquer des règles nftables natives ajoutées par un autre outil (Docker, certains logiciels de pare-feu modernes gèrent directement nftables sans passer par la couche de compatibilité). Inversement, nft list ruleset montre les règles nftables natives, mais ne représente pas toujours intuitivement les règles injectées via la couche de traduction iptables-nft, selon la table dans laquelle elles atterrissent.

Ce défaut de lisibilité en cache un plus grave, de couverture : il n’existe pas de commande unique qui montre tout. iptables-legacy écrit dans le sous-système kernel x_tables, tandis que nft et iptables-nft écrivent dans nf_tables. Deux stockages disjoints. nft list ruleset est donc aveugle aux règles legacy — exactement le cas de coexistence qui pose problème. Le wiki nftables met explicitement en garde contre l’usage simultané des deux familles d’outils, qui revient à solliciter les deux sous-systèmes en même temps, avec des résultats imprévisibles. Et l’angle mort n’est pas cosmétique : le même wiki détaille, dans sa page de dépannage, ce qui se passe quand les deux coexistent sur un hôte — c’est le verdict le plus restrictif qui l’emporte. Une règle de blocage qui n’apparaît dans aucune sortie de nft list ruleset bloque quand même le trafic.

# La vue fiable est une paire, pas une commande :
# le côté nf_tables (natif + traduit via iptables-nft)
nft list ruleset
# le côté x_tables, invisible à la commande précédente
iptables-legacy-save

# Sur Debian/Ubuntu, savoir quel backend est branché
# derrière la commande "iptables" du PATH
update-alternatives --display iptables

Le réflexe de diagnostic n’est donc pas de remplacer iptables -L par nft list ruleset, mais de regarder les deux sous-systèmes avant de conclure qu’une règle n’existe pas. Ce même besoin de voir la vraie source de vérité plutôt qu’une vue partielle rejoint la logique derrière Cilium et eBPF, qui contourne entièrement ce risque de double comptabilité en remplaçant iptables par un dataplane unique côté Kubernetes.

Terminer la migration : traduire, puis réécrire

Si la migration ne se termine jamais, c’est d’abord que rien ne l’y oblige. Les notes de publication de Debian 10 sont explicites : depuis iptables 1.8.2, le paquet fournit iptables-nft et iptables-legacy, la variante nftables est le défaut, la variante legacy reste installée, et update-alternatives permet de basculer de l’une à l’autre. Le défaut a changé ; l’ancienne route est restée ouverte.

Les outils pour la terminer vraiment sont livrés dans le tarball iptables, avec les commandes qu’ils remplacent. iptables-translate traduit une commande isolée, ce qui est la bonne façon d’apprendre la syntaxe. iptables-restore-translate traduit un ruleset entier.

# Une règle, pour apprendre la syntaxe
iptables-translate -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT
# nft add rule ip filter INPUT tcp dport 22 ct state new counter accept

# Le ruleset entier, en deux temps
iptables-save > save.txt
iptables-restore-translate -f save.txt > ruleset.nft
nft -f ruleset.nft

La traduction est mécanique, et c’est sa limite : elle reproduit les mêmes chaînes, sous les mêmes noms, avec la même structure linéaire. Elle sort de x_tables sans rien faire gagner d’autre. Le wiki nftables le présente d’ailleurs comme une étape, pas comme une arrivée : après la migration, on est « encouragé à mettre en œuvre les nouveaux mécanismes nftables tels que les sets, les maps, les verdict maps et les concaténations ».

C’est là que se situe l’argument de performance, et il vaut d’être énoncé pour ce qu’il est. Une chaîne iptables s’inspecte règle par règle ; le wiki décrit précisément les verdict maps comme un moyen de « réduire le nombre d’inspections de listes linéaires nécessaires pour classifier vos paquets ». Un set nftables, lui, se consulte plutôt qu’il ne se parcourt : ses éléments sont représentés en interne par des structures de données de performance, tables de hachage et arbres rouge-noir. Les notes de Debian 10 résument le même point côté projet — classification de paquets plus rapide grâce aux infrastructures génériques de sets et de maps.

Concrètement, quatre règles qui énumèrent quatre types ICMPv6 deviennent une règle et un set :

# Ce que la traduction produit : la structure linéaire d'origine,
# quatre règles inspectées l'une après l'autre.
# Ce que la réécriture produit : un set, consulté d'un coup.
nft add rule ip6 filter input icmpv6 type \
  { nd-neighbor-solicit, echo-request, nd-router-advert, nd-neighbor-advert } accept

Le gain se joue donc sur la longueur de la liste parcourue. C’est un argument pour un long ruleset, beaucoup moins pour une poignée de règles — et c’est exactement pour ça que la migration reste inachevée sur tant d’hôtes. La couche de compatibilité fonctionne, et la seule étape qui rapporterait quelque chose est celle que rien ne réclame.

À retenir

nftables unifie IPv4, IPv6, ARP et le filtrage de pont Ethernet dans un seul framework, et son apport sur le rechargement est une question de portée plutôt que de nouveauté : une transaction qui couvre le ruleset entier, toutes tables et toutes familles confondues, là où le format iptables-restore s’exprime table par table. La couche de compatibilité iptables-nft, active par défaut sur la plupart des distributions modernes, fait tourner les anciens scripts sans jamais forcer leur réécriture, un choix pragmatique qui laisse coexister des règles natives et traduites, parfois invisibles les unes aux autres selon l’outil de diagnostic utilisé. Aucune commande ne donne à elle seule l’état réel du kernel : nft list ruleset couvre nf_tables, iptables-legacy-save couvre x_tables, et il faut les deux pour affirmer qu’une règle n’existe pas — un réflexe de diagnostic qui reste pertinent même sur une infrastructure largement conteneurisée où le pare-feu de l’hôte continue d’exister sous les couches d’abstraction. Et cette question « qui écrit où » ne se pose presque jamais à iptables ou nft directement : elle se pose à firewalld, à ufw et à Docker, qui y répondent chacun différemment, et dont la documentation est le seul endroit où la réponse est écrite.