Introduction
Nftables est un outil qui permet de faire du filtrage réseau et de prendre le contrôle du flux entrant/sortant sur notre machine.
Disponible depuis le kernel Linux 3.13, nftables remplace progressivement le vieillissant {ip,ip6,arp,eb}tables.
Il dispose en effet de plusieurs avantages non négligeables par rapport à iptables.
Voici quelques unes des améliorations majeures :
- Moins de duplication de code / Mieux écrit / Totalement compatible 64 bits
- Meilleures performances
- Un seul outil pour gérer toutes les couches réseau
- Meilleure gestion du rechargement dynamique des règles
- Classification de paquets plus rapide
- Plusieurs actions possibles par règle (Impossible pour iptables de bloquer un paquet et de logger dans une seule règle)
Architecture
Nftables ajoute au noyau Linux une machine virtuelle (VM) capable d'exécuter du bytecode (code compilé qui n'est pas exécutable directement par un processeur mais par une VM) pour analyser un paquet et décider du traitement de celui-ci.
Le binaire nft va utiliser la librairie libnftables pour convertir la règle saisie par l'utilisateur en structures d'objets Netlink.
Elle est ensuite compilée pour obtenir le bytecode de la VM. Celui-ci intègre les libnftnl pour la gestion des règles et libmnl pour la communication avec le kernel en utilisant des sockets Netlink. Le bytecode est ensuite exécuté par la VM et il prend la décision pour chaque paquet entrant quel traitement effectuer (forward, block...).
Fonctionnement
Nftables va permettre d'appliquer des règles sur certains points d'ancrage (hooks) en fonction du type de famille qui est visé (ip, arp, bridge...).
Ces hooks sont situés à différentes étapes de la gestion de la trame : depuis l'entrée de la couche 3, qui va permettre de filtrer directement sur des paquets de type ether (couche 2) comme IPv4, ARP, PPPoE...,
jusqu'à la couche 7 (applicative) qui va permettre d'obtenir un filtrage plus fin comme par exemple la gestion du trafic SSH.
Hooks
Ingress
Tous les paquets entrants dans le système passent par ce hook. Il est situé entre la couche liaison (2) et réseau (3).
Une interface d'écoute (device dans nft) doit être spécifiée lors de l'ajout d'une règle.
Il permet de filtrer les paquets en fonction de leur Ethertype.
egress
Tous les paquets sortants du système passent par ce hook. Il n'est de ce fait relié à aucune couche en particulier.
Tout comme pour le hook ingress, ce hook a besoin de la mention d'une interface pour pouvoir fonctionner.
prerouting
Les paquets transitent par ce hook juste après le hook ingress (excepté dans le cas de l'ARP). Ils sont donc situés entre la couche liaison (2) et réseau (3) également.
Les paquets sont analysés ici afin de déterminer s'ils sont à destination de notre système. Si c'est le cas ils seront transmis au hook input.
Dans le cas contraire, les hook forward et ensuite postrouting prendront le relais.
forward
Il s'agit du hook invoqué juste après le hook prerouting.
A ce niveau les paquets ne sont plus à destination de notre système. Ils vont être redirigés vers une autre interface réseau.
postrouting
Il fait suite au hook forward. Les décisions de routage ont déjà été prises.
input
Tous les paquets à destination de notre système passent par ce hook.
output
Tous les paquets sortants de notre système passent par ce hook.
Familles et hooks associés
Netdev
Liste des hook disponibles :
- ingress
- egress
Elle permet de faire du filtrage sur des paquets de type Ethertype ainsi que des paquets IPv4 et IPv6.
ARP
Liste des hook disponibles :
- input
- output
Permet la gestion des paquets ARP reçus et envoyés par le système.
Bridge
Liste des hook disponibles :
- prerouting
- forward
- postrouting
- input
- output
Permet la gestion des paquets Ethernet passant par des interfaces Bridge.
IPv4/IPv6/Inet
Liste des hook disponibles :
- prerouting
- forward
- postrouting
- input
- output
Permet la gestion des paquets IPv4, IPv6 ou les 2 sans distinction (Inet).
Schéma représentant les familles et hooks associés :
Configuration
Après avoir vu l'architecture des nftables et son fonctionnement global, on va maintenant passer à l'écriture des règles.
Elles sont découpées en 3 parties :
Eléments de configuration nftables | |
---|---|
Tables |
Ce sont des conteneurs qui contiennent des chaînes. Les tables sont identifiées par leur famille (netdev, arp, ip...) et leur nom défini par l'utilisateur. |
Chaînes |
Ce sont des conteneurs rattachés à des hook qui contiennent des règles. |
Règles |
Actions à appliquer à ces différents hooks. |
Représentation :
Attention : Dans tous les exemples qui suivent, les commandes sont à réaliser en sudo ou root.
Les tables
Créer
#Créer une table
#nft add table [family] table_name
#Si la famille n'est pas renseignée, la famille ip sera sélectionnée
#Exemple :
nft add table inet inet_table
Lister
#Lister les tables
#nft list tables [family]
#Si la famille n'est pas renseignée, les tables de toutes les familles seront listées
#Exemple :
nft list tables inet
table inet inet_table
Supprimer
#Supprimer une table
#nft delete table [family] table_name
#Si la famille n'est pas renseignée, la table de la famille ip sera supprimée
#La table doit être vide de toute règle pour pouvoir être supprimée
#Exemple :
nft delete table inet inet_table
Flusher
#Flusher une table
#nft flush table [family] table_name
#Le flush va permettre de supprimer toutes les règles de la table choisie
#Si la famille n'est pas renseignée, la famille ip sera sélectionnée
#Exemple :
nft flush table inet inet_table
Les chaînes
Types de chaînes
Il y a différents types de chaînes qui appartiennent à des familles et hooks précis :
Type | Familles | Hooks |
filter | toutes | tous |
nat (utilisé pour effectuer du NAT) | ip, ip6, inet | prerouting, input, output, postrouting |
route (utilisé pour rerouter les paquets) | ip, ip6 | output |
Priorité
Le système de propriété va permettre d'appliquer un ordre d'exécution des règles dans le cas où plusieurs chaînes ont le même hook.
La priorité est un entier signé. Les règles de la chaîne avec la priorité la plus basse seront exécutées avant les autres.
Créer
#Créer une chaîne
#nft 'add chain [family] table_name chain_name { type type hook hook priority priority ; }'
#Si la famille n'est pas renseignée, la famille ip sera sélectionnée
#Exemple :
nft 'add chain inet inet_table inet_chain_input { type filter hook input priority 0 ; }'
Lister
#Lister les chaînes
#nft list chains [family]
#Si la famille n'est pas renseignée, les tables de toutes les familles seront listées
#Exemple :
nft list chains inet
#nft list chain [family] table_name chain_name
#Seule la chaîne renseignée sera listée
#Exemple :
nft list chain inet inet_table inet_chain_input
table inet inet_table {
chain inet_chain_input {
type filter hook input priority filter; policy accept;
}
}
Lister avec affichage des ID (handle)
Chaque chaîne et chaque règle possède un identifiant (ID). On va pouvoir s'en servir pour les supprimer/modifier.
Cet affichage est possible en rajoutant l'option -a après la commande nft .
nft -a list chains inet
table inet inet_table {
chain inet_chain_input { # handle 3
type filter hook input priority filter; policy accept;
}
}
Supprimer
#Supprimer une chaîne
#nft delete chain [family] table_name chain_name
#Si la famille n'est pas renseignée, la chaîne de la table de la famille ip sera supprimée
#Exemple
nft delete chain inet inet_table inet_chain_input
#nft delete chain [family] table_name handle <handle>
#Exemple
nft delete chain inet inet_table handle 3
Les règles
Créer
#Créer une règle
#nft add rule [family] table_name chain_name statement
#Si la famille n'est pas renseignée, la famille ip sera sélectionnée
#La règle est ajoutée en dernier dans la liste
# Par exemple : on refuse le ping sur le hook input : une machine qui effectue un ping sur la nôtre n'aura pas de réponse
nft add rule inet inet_table inet_chain_input icmp type echo-request drop
Lister
#Lister les règles
#nft list ruleset [family]
#Si la famille n'est pas renseignée, les règles de toutes les familles seront listées
#Exemple
nft list ruleset inet
table inet inet_table {
chain inet_chain_input {
type filter hook input priority filter; policy accept;
icmp type echo-request drop
}
}
#Lister avec les handles
#Exemple
nft -a list ruleset inet
table inet inet_table { # handle 1
chain inet_chain_input { # handle 1
type filter hook input priority filter; policy accept;
icmp type echo-request drop # handle 2
}
}
Remplacer
#Remplacer une règle
#nft replace rule [family] table_name chain_name handle <index> statement
#Si la famille n'est pas renseignée, la règle de la table de la famille ip sera remplacée
#Exemple
nft replace rule inet inet_table inet_chain_input handle 2 drop
nft -a list ruleset inet
table inet inet_table { # handle 1
chain inet_chain_input { # handle 1
type filter hook input priority filter; policy accept;
drop # handle 2
}
}
Insérer
#Insérer une règle
#nft insert rule [family] table_name chain_name handle <index> statement
#Si la famille n'est pas renseignée, la règle de la table de la famille ip sera insérée
#La règle est insérée juste avant celle qui porte l'index saisi
#Exemple
nft insert rule inet inet_table inet_chain_input handle 2 tcp sport 9000 accept
nft -a list ruleset inet
table inet inet_table { # handle 1
chain inet_chain_input { # handle 1
type filter hook input priority filter; policy accept;
tcp sport 9000 accept # handle 3
drop # handle 2
}
}
Supprimer
#Supprimer une règle
#nft delete rule [family] table_name chain_name handle <handle>
#Si la famille n'est pas renseignée, la règle de la table de la famille ip sera supprimée
#Exemple
nft delete rule inet inet_table inet_chain_input handle 2
nft -a list ruleset inet
table inet inet_table { # handle 1
chain inet_chain_input { # handle 1
type filter hook input priority filter; policy accept;
}
}
Ordre des règles
Attention! L'ordre de création des règles est très important : la première règle à laquelle le paquet correspondra sera appliquée.
Dans l'exemple suivant on cherche à n'accepter que les pings :
nft -a list ruleset
table inet inet_table { # handle 1
chain inet_chain { # handle 1
type filter hook input priority filter; policy accept;
drop # handle 2
icmp type echo-request accept # handle 3
}
}
Ici tous les paquets seront bloqués et donc la règle sur les paquets icmp de type echo-request entrants sera ignorée. Il faut donc inverser les 2 règles :
nft -a list ruleset
table inet inet_table { # handle 1
chain inet_chain { # handle 1
type filter hook input priority filter; policy accept;
icmp type echo-request accept # handle 2
drop # handle 3
}
}
Quelques options utiles de règles
Nous savons maintenant comment créer des règles pour gérer notre trafic. Voyons maintenant quelques options de règles pour avoir des informations complémentaires sur notre trafic.
Logs
Il est possible de générer des logs sur chaque règle. Dans l'exemple suivant on va refuser le ping vers notre machine et logger cette action. Comme présenté au début de cet article, on va pouvoir créer la règle en y ajoutant à la fois le blocage du ping et le log :
nft add rule [family] table_name chain_name statement log [prefix] policy
Écriture complète de la règle :
#Création de la table
nft add table inet inet_table
#Création de la chaîne
nft 'add chain inet inet_table inet_chain_input { type filter hook input priority 0 ; }'
#Création de la règle
nft add rule inet inet_table inet_chain_input icmp type echo-request log prefix \"Attempting ping :\" drop
Sur notre machine, via la commande journalctl -f
, on va pouvoir suivre les logs en temps réel. Suite au ping fait depuis la machine distante on peut maintenant voir le résultat de notre règle :
journalctl -f
Jun 16 11:53:06 fdetre kernel: Attempting ping :IN=enp0s31f6 OUT= MAC=88:6f:d4:ff:b5:1d:b8:27:eb:fe:eb:20:08:00 SRC=10.1.75.194 DST=10.1.75.53 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=60871 DF PROTO=ICMP TYPE=8 CODE=0 ID=1 SEQ=1
Compteur
Il est possible d'afficher le nombre de paquets et la quantité de données (en octets) qui transitent par notre règle via le mot clé counter
. Dans l'exemple suivant on va se servir du counter pour monitorer notre flux sortant vers le port 443 (normalement, du trafic HTTPS).
#Ajout de la chaîne pour filtrer sur le hook output
nft 'add chain inet inet_table inet_chain_output { type filter hook output priority 0 ; }'
#Ajout de la règle pour monitorer le trafic https
nft add rule inet inet_table inet_chain_output tcp dport 443 counter accept
#Après avoir généré du trafic
nft -a list ruleset inet
chain inet_chain_output { # handle 6
type filter hook output priority filter; policy accept;
tcp dport 443 counter packets 1241 bytes 104727 accept # handle 7
}
Connection Tracking
Imaginons que nous voulons récupérer un fichier qui est sur un serveur web mais nous souhaitons également être isolé de toute tentative de connexion extérieure.
Exécution du serveur web sur la machine distante
#python 3 doit être installé
python3 -m http.server 8000
Configuration du PC isolé
On ne va ici s'occuper que du hook input. Pas de restriction particulière sur les paquets sortants de notre machine.
On veut que le PC soit isolé on va donc mettre une règle de drop sur le hook input de cette façon :
# PC
nft add table ip_table
nft 'add chain ip_table ip_chain_input { type filter hook input priority 0 ; }'
nft add rule ip_table ip_chain_input drop
nft -a list ruleset
table ip ip_table { # handle 1
chain ip_chain_input { # handle 1
type filter hook input priority filter; policy accept;
drop # handle 2
}
}
Testons : depuis le PC on essaie de récupérer le fichier sur le serveur :
#PC
wget http://10.1.75.194:8000/file_server
--2022-06-16 17:41:10-- http://10.1.75.194:8000/file_server
Connecting to 10.1.75.194:8000... failed: Connection timed out.
Retrying.
Le téléchargement du fichier a échoué! En effet vu que l'on rejette tous les paquets arrivants sur notre interface réseau via le drop global, on ne peut donc pas recevoir le fichier.
La solution va donc consister à insérer une règle pour accepter les paquets ayant comme port TCP de source 8000 avant la règle de drop :
# PC
nft insert rule ip_table ip_chain_input position 2 tcp sport 8000 accept
nft -a list ruleset
table ip ip_table { # handle 1
chain ip_chain_input { # handle 1
type filter hook input priority filter; policy accept;
tcp sport 8000 accept # handle 3
drop # handle 2
}
}
Effectuons une nouvelle fois le test :
#PC
wget http://10.1.75.194:8000/file_server
2022-06-17 08:16:25 (451 KB/s) - ‘file_server’ saved [26/26]
Connecting to 10.1.75.194:8000... connected.
HTTP request sent, awaiting response... 200 OK
Length: 26 [text/plain]
Saving to: ‘file_rpi’
file_rpi 100%[=====================================>] 26 --.-KB/s in 0s
2022-06-16 16:25:30 (406 KB/s) - ‘file_server’ saved [26/26]
Le fichier a été récupéré!
Mais...
En ouvrant le port de source 8000, le serveur pourrait très bien essayait d'établir une connexion TCP avec le PC en modifiant son propre port source.
Essayons de mettre en pratique ce cas via la commande nping depuis le serveur :
#Serveur
sudo nping -c 1 --tcp -g 8000 -p 8000 10.1.75.110
Starting Nping 0.7.92 ( https://nmap.org/nping ) at 2022-06-24 16:51 CEST
SENT (0.0344s) TCP 10.1.75.194:8000 > 10.1.75.110:8000 S ttl=64 id=51367 iplen=40 seq=3883081097 win=1480
RCVD (0.0351s) TCP 10.1.75.110:8000 > 10.1.75.194:8000 SA ttl=64 id=0 iplen=44 seq=974047579 win=64240 <mss 1460>
Max rtt: 0.586ms | Min rtt: 0.586ms | Avg rtt: 0.586ms
Raw packets sent: 1 (40B) | Rcvd: 1 (46B) | Lost: 0 (0.00%)
Nping done: 1 IP address pinged in 1.07 seconds
Effectivement tout s'est bien passé. En modifiant le port source du serveur (avec l'option -g) on a bien réussi à établir une connexion TCP avec le PC isolé.
Ce n'est pas ce que nous voulons car il s'agit d'un point d'entrée pour d'éventuelles attaques!
C'est ici qu'intervient le Connection Tracking. Il va nous permettre d'accepter les paquets entrants uniquement si une connexion a déjà été établie. De ce fait le serveur ne sera plus en mesure de communiquer avec le PC car il ne pourra plus instancier de connexion. Seul le PC en aura le droit.
Modifions la règle :
#PC
nft -a list ruleset
table ip ip_table { # handle 1
chain ip_chain_input { # handle 1
type filter hook input priority filter; policy accept;
tcp dport 8000 accept # handle 3
drop # handle 2
}
}
#Modification du handle 2
nft replace rule ip_table ip_chain_input handle 3 ct state established,related accept
nft -a list ruleset
table ip ip_table { # handle 1
chain ip_chain_input { # handle 1
type filter hook input priority filter; policy accept;
ct state established,related accept # handle 3
drop # handle 2
}
}
On relance le test de connexion TCP depuis le serveur:
#Serveur
sudo nping -c 1 --tcp -g 8000 -p 8000 10.1.75.110
Starting Nping 0.7.92 ( https://nmap.org/nping ) at 2022-06-24 16:50 CEST
SENT (0.0298s) TCP 10.1.75.194:8000 > 10.1.75.110:8000 S ttl=64 id=36971 iplen=40 seq=737006274 win=1480
Max rtt: N/A | Min rtt: N/A | Avg rtt: N/A
Raw packets sent: 1 (40B) | Rcvd: 0 (0B) | Lost: 1 (100.00%)
Nping done: 1 IP address pinged in 1.07 seconds
La règle a bien fonctionné : on constate bien que l'on a pas reçu de paquets provenant du PC.
Sauvegarde / restauration des règles
Toutes les commandes de création de règles que nous venons de voir via la ligne de commande créent des règles pour la session en cours. Une fois celle-ci fermée, toutes les règles sont supprimées. Il faut donc les sauvegarder pour pouvoir les restaurer ensuite.
Sauvegarde
Il suffit d'utiliser la commande pour afficher les règles complètes et de rediriger sa sortie dans un fichier :
nft list ruleset > connection_tracking.nft
cat connection_tracking.nft
table ip ip_table {
chain ip_chain_input {
type filter hook input priority filter; policy accept;
ct state established,related accept
drop
}
}
L'extension n'a pas d'importance, on peut même ne pas en mettre.
Restauration
On restaure notre configuration nftables avec la commande nft suivante :
nft -f connection_tracking.nft
Conclusion
Nous avons appris au travers de cet article l'essentiel pour comprendre les différentes mécaniques de nftables et comment créer nos propres règles. Pour une utilisation plus soutenue il faudra consulter le man de la commande nft afin de voir toutes les possibilités offertes pour la création de règles plus complexes.