web-dev-qa-db-fra.com

Docker brise le réseau du pont Libvirt

Ce problème me rend fou. Je lance une nouvelle installation d'Ubuntu 18.04, avec:

  • ufw pour gérer le pare-feu
  • un pont br0
  • lxd et libvirt (KVM)

J'ai essayé le package stock docker.io et les packages du propre dépôt deb de docker.

Je veux pouvoir déployer des conteneurs Docker en choisissant l'IP pour lier son port (par exemple. -P 10.58.26.6:98800:98800), puis ouvrir le port avec UFW.

Mais docker semble créer des règles iptables qui pertubent le pont br0 (par exemple, l'hôte ne peut pas envoyer de ping aux invités libvirt)

J'ai regardé tout autour et je ne trouve pas de bonne solution sensible à la sécurité.

Faire manuellement iptables -I FORWARD -i br0 -o br0 -j ACCEPT semble faire tout fonctionner.

Définissant également "iptables": false pour le démon docker permet au pont de se comporter normalement, mais rompt le réseau de sortie des conteneurs du docker.

J'ai trouvé cette solution qui semblait simple, en modifiant un seul fichier UFW https://stackoverflow.com/a/51741599/1091772 , mais cela ne fonctionne pas du tout.

Quelle serait la meilleure pratique et un moyen sûr de résoudre ce problème de manière permanente, en survivant aux redémarrages?

EDIT: J'ai fini par ajouter -A ufw-before-forward -i br0 -o br0 -j ACCEPT au bout du /etc/ufw/before.rules devant le COMMIT. Puis-je considérer cela comme un correctif ou ne pose-t-il pas des problèmes?

7
Laurent

Le problème, en fait une fonctionnalité: br_netfilter

D'après la description, je crois que la seule explication logique est que le bridge netfilter code est activé: destiné entre autres utilisations pour le pare-feu de pont avec état ou pour exploiter iptables 'correspond et cibles du chemin du pont sans avoir à (ou pouvoir) les dupliquer toutes dans ebtables. Sans tenir compte de la superposition du réseau, le code de pont Ethernet, au niveau de la couche réseau 2, fait maintenant des appels vers iptables fonctionnant au niveau IP, c'est-à-dire la couche réseau 3. Il ne peut encore être activé que globalement: soit pour l'hôte et tous les conteneurs, ou pour aucun. Une fois compris ce qui se passe et savoir quoi chercher, des choix adaptés peuvent être faits.

Le projet netfilter décrit les diverses interactions ebtables/iptables lorsque br_netfilter est activé. Particulièrement intéressant est le section 7 expliquant pourquoi certaines règles sans effet apparent sont parfois nécessaires pour éviter les effets involontaires du chemin du pont, comme en utilisant:

iptables -t nat -A POSTROUTING -s 172.16.1.0/24 -d 172.16.1.0/24 -j ACCEPT
iptables -t nat -A POSTROUTING -s 172.16.1.0/24 -j MASQUERADE

pour éviter que deux systèmes sur le même LAN soient NATés par ... le pont (voir l'exemple ci-dessous).

Vous avez quelques choix pour éviter votre problème, mais le choix que vous avez fait est probablement le meilleur si vous ne voulez pas connaître tous les détails ni vérifier si certaines règles iptables (parfois cachées dans d'autres espaces de noms) seraient perturbées:

  • empêcher définitivement le module br_netfilter d'être chargé. En général, blacklist ne suffit pas, install doit être utilisé. C'est un choix sujet à des problèmes pour les applications qui dépendent de br_netfilter: évidemment Docker, Kubernetes, ...

    echo install br_netfilter /bin/true > /etc/modprobe.d/disable-br-netfilter.conf
    
  • Faites charger le module, mais désactivez ses effets. Pour les effets iptables c'est-à-dire:

    sysctl -w net.bridge.bridge-nf-call-iptables=0
    

    Si vous mettez ceci au démarrage, le module doit être chargé en premier ou cette bascule n'existera pas encore.

Ces deux choix précédents perturberont à coup sûr iptables correspond à -m physdev : Le module xt_physdev lorsqu'il est lui-même chargé, charge automatiquement le module br_netfilter (cela se produirait même si une règle ajoutée à partir d'un conteneur se déclenchait) le chargement). Maintenant br_netfilter ne sera pas chargé, -m physdev ne correspondra probablement jamais.

  • Contournez l'effet de br_netfilter en cas de besoin, comme OP: ajoutez ces règles sans opération apparentes dans diverses chaînes (PREROUTING, FORWARD, POSTROUTING) comme décrit dans section 7 . Par exemple:

    iptables -t nat -A POSTROUTING -s 172.18.0.0/16 -d 172.18.0.0/16 -j ACCEPT
    
    iptables -A FORWARD -i br0 -o br0 -j ACCEPT
    

    Ces règles ne doivent jamais correspondre car le trafic sur le même réseau local IP n'est pas acheminé, à l'exception de certaines configurations DNAT rares. Mais grâce à br_netfilter ils correspondent, car ils sont d'abord appelés pour commuté trames ("mises à jour" en paquets IP) traversant le pont . Ensuite, ils sont appelés à nouveau pour routé paquets traversant le routeur vers une interface indépendante (mais ne correspondra pas alors).

  • Ne mettez pas d'adresse IP sur le pont: mettez cette adresse IP à une extrémité d'une interface veth avec son autre extrémité sur le pont: cela devrait garantir que le pont n'interagira pas avec le routage, mais ce n'est pas ce que font la plupart des produits communs de conteneurs/VM.

  • Vous pouvez même masquer le pont dans son propre espace de noms de réseau isolé (cela ne serait utile que si vous souhaitez vous isoler des autres règles ebtables cette fois).

  • Basculez tout sur nftables lequel parmi les objectifs énoncés évitera ces problèmes d'interaction de pont . Pour l'instant, le pare-feu du pont n'a pas de support avec état disponible, il est toujours ESSUYEZ mais il est promis d'être plus propre lorsqu'il sera disponible, car il n'y aura pas de "upcall" .

Vous devez rechercher ce qui déclenche le chargement de br_netfilter (par exemple: -m physdev) et voyez si vous pouvez l'éviter ou non, pour choisir comment procéder.


Exemple avec des espaces de noms réseau

Reproduisons quelques effets à l'aide d'un espace de noms réseau. Notez que nulle part aucune règle ebtables ne sera utilisée. Notez également que cet exemple repose sur l'héritage habituel iptables , et non iptables sur nftables comme activé par défaut sur Debian buster.

Reproduisons un cas simple similaire à de nombreuses utilisations de conteneurs: un routeur 192.168.0.1/192.0.2.100 faisant NAT avec deux hôtes derrière: 192.168.0.101 et 192.168.0.102, lié avec un pont sur le Les deux hôtes peuvent communiquer directement sur le même LAN, via le pont.

#!/bin/sh

for ns in Host1 Host2 router; do
    ip netns del $ns 2>/dev/null || :
    ip netns add $ns
    ip -n $ns link set lo up
done

ip netns exec router sysctl -q -w net.ipv4.conf.default.forwarding=1

ip -n router link add bridge0 type bridge
ip -n router link set bridge0 up
ip -n router address add 192.168.0.1/24 dev bridge0

for i in 1 2; do
    ip -n Host$i link add eth0 type veth peer netns router port$i
    ip -n Host$i link set eth0 up
    ip -n Host$i address add 192.168.0.10$i/24 dev eth0
    ip -n Host$i route add default via 192.168.0.1
    ip -n router link set port$i up master bridge0
done

#to mimic a standard NAT router, iptables rule voluntarily made as it is to show the last "effect"
ip -n router link add name eth0 type dummy
ip -n router link set eth0 up
ip -n router address add 192.0.2.100/24 dev eth0
ip -n router route add default via 192.0.2.1
ip netns exec router iptables -t nat -A POSTROUTING -s 192.168.0.0/24 -j MASQUERADE

Chargeons le module du noyau br_netfilter (pour être sûr qu'il ne sera pas plus tard) et désactivons ses effets avec la bascule (pas par espace de noms) bridge-nf-call- iptables, disponible uniquement dans l'espace de noms initial:

modprobe br_netfilter
sysctl -w net.bridge.bridge-nf-call-iptables=0

Attention: encore une fois, cela peut perturber iptables des règles comme -m physdev n'importe où sur l'hôte ou dans des conteneurs qui dépendent de br_netfilter chargé et activé.

Ajoutons quelques compteurs de trafic ping icmp.

ip netns exec router iptables -A FORWARD -p icmp --icmp-type echo-request
ip netns exec router iptables -A FORWARD -p icmp --icmp-type echo-reply

Voyons ping:

# ip netns exec Host1 ping -n -c2 192.168.0.102
PING 192.168.0.102 (192.168.0.102) 56(84) bytes of data.
64 bytes from 192.168.0.102: icmp_seq=1 ttl=64 time=0.047 ms
64 bytes from 192.168.0.102: icmp_seq=2 ttl=64 time=0.058 ms

--- 192.168.0.102 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1017ms
rtt min/avg/max/mdev = 0.047/0.052/0.058/0.009 ms

Les compteurs ne correspondront pas:

# ip netns exec router iptables -v -S FORWARD
-P FORWARD ACCEPT -c 0 0
-A FORWARD -p icmp -m icmp --icmp-type 8 -c 0 0
-A FORWARD -p icmp -m icmp --icmp-type 0 -c 0 0

Activons bridge-nf-call-iptables et ping à nouveau:

# sysctl -w net.bridge.bridge-nf-call-iptables=1
net.bridge.bridge-nf-call-iptables = 1
# ip netns exec Host1 ping -n -c2 192.168.0.102
PING 192.168.0.102 (192.168.0.102) 56(84) bytes of data.
64 bytes from 192.168.0.102: icmp_seq=1 ttl=64 time=0.094 ms
64 bytes from 192.168.0.102: icmp_seq=2 ttl=64 time=0.163 ms

--- 192.168.0.102 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1006ms
rtt min/avg/max/mdev = 0.094/0.128/0.163/0.036 ms

Cette fois, les paquets commutés ont obtenu une correspondance dans le filtre/la chaîne FORWARD d'iptables:

# ip netns exec router iptables -v -S FORWARD
-P FORWARD ACCEPT -c 4 336
-A FORWARD -p icmp -m icmp --icmp-type 8 -c 2 168
-A FORWARD -p icmp -m icmp --icmp-type 0 -c 2 168

Mettons une politique DROP (qui met à zéro les compteurs par défaut) et réessayons:

# ip netns exec Host1 ping -n -c2 192.168.0.102
PING 192.168.0.102 (192.168.0.102) 56(84) bytes of data.

--- 192.168.0.102 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 1008ms

# ip netns exec router iptables -v -S FORWARD
-P FORWARD DROP -c 2 168
-A FORWARD -p icmp -m icmp --icmp-type 8 -c 4 336
-A FORWARD -p icmp -m icmp --icmp-type 0 -c 2 168

Le code de pont a filtré les trames/paquets commutés via iptables. Ajoutons la règle de contournement (qui remettra à zéro les compteurs par défaut) comme dans OP et réessayons:

# ip netns exec router iptables -A FORWARD -i bridge0 -o bridge0 -j ACCEPT
# ip netns exec Host1 ping -n -c2 192.168.0.102
PING 192.168.0.102 (192.168.0.102) 56(84) bytes of data.
64 bytes from 192.168.0.102: icmp_seq=1 ttl=64 time=0.132 ms
64 bytes from 192.168.0.102: icmp_seq=2 ttl=64 time=0.123 ms

--- 192.168.0.102 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1024ms
rtt min/avg/max/mdev = 0.123/0.127/0.132/0.012 ms

# ip netns exec router iptables -v -S FORWARD
-P FORWARD DROP -c 0 0
-A FORWARD -p icmp -m icmp --icmp-type 8 -c 6 504
-A FORWARD -p icmp -m icmp --icmp-type 0 -c 4 336
-A FORWARD -i bridge0 -o bridge0 -c 4 336 -j ACCEPT

Voyons ce qui est actuellement réellement reçu sur Host2 lors d'un ping de Host1:

# ip netns exec Host2 tcpdump -l -n -s0 -i eth0 -p icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
02:16:11.068795 IP 192.168.0.1 > 192.168.0.102: ICMP echo request, id 9496, seq 1, length 64
02:16:11.068817 IP 192.168.0.102 > 192.168.0.1: ICMP echo reply, id 9496, seq 1, length 64
02:16:12.088002 IP 192.168.0.1 > 192.168.0.102: ICMP echo request, id 9496, seq 2, length 64
02:16:12.088063 IP 192.168.0.102 > 192.168.0.1: ICMP echo reply, id 9496, seq 2, length 64

... au lieu de la source 192.168.0.101. La règle MASQUERADE a également été appelée depuis le chemin du pont. Pour éviter cela, ajoutez (comme expliqué dans section 7 l'exemple de) = une règle d'exception avant, ou indiquez une interface sortante non pontée, si possible (maintenant elle est disponible, vous pouvez même utiliser -m physdev s'il doit s'agir d'un pont ...).


Liés au hasard:

LKML/netfilter-dev: br_netfilter: enable dans les netns non initiaux : cela aiderait à activer cette fonctionnalité par espace de nom plutôt que globalement, limitant ainsi les interactions entre les hôtes et les conteneurs.

netfilter-dev: netfilter: physdev: relâche la dépendance br_netfilter : une simple tentative de suppression d'une règle - physdev non existante pourrait créer des problèmes.

netfilter-dev: prise en charge du suivi des connexions pour le pont : code netfilter du pont WIP pour préparer un pare-feu de pont avec état à l'aide de nftables, cette fois plus élégamment. Je pense que l'une des dernières étapes pour se débarrasser d'iptables (l'API côté noyau de).

7
A.B

Si les menaces ci-dessus ne résolvent pas votre problème, voici comment j'ai résolu le problème sur mon Debian Stretch.

  • Tout d'abord, enregistrez vos iptables actuels

    iptables-save > your-current-iptables.rules
    
  • 2e, supprimez TOUS les règles créées par Docker

    iptables -D <DOCKER-CHAIN-RULES> <target-line-number>
    
  • 3ème, ajoutez des règles itpables pour accepter tout trafic vers INPUT, FORWARD et OUTPUT

    iptables -I INPUT -j ACCEPT
    iptables -I FORWARD -j ACCEPT
    iptables -I OUTPUT -j ACCEPT
    
  • 4ème, redémarrez votre Docker

    service docker restart
    

Une fois l'étape 3 terminée, vous pouvez envoyer une requête ping à votre libvert bloqué KVM Host à partir d'un autre PC, vous verrez les réponses ICMP.

Le redémarrage de Docker ajoutera également ses règles iptables requises à votre machine, mais il ne bloquera plus vos hôtes pontés KVM).

Si la solution ci-dessus ne fonctionne pas pour vous, vous pouvez restaurer les iptables à l'aide de la commande suivante:

  • Restaurer iptables

    iptables-restore < your-current-iptables.rules
    
0
Vincent P