Thématiques principales

samedi 23 novembre 2019

Reseau : Iptables et réseaux docker

Maintenant que nous avons vu le principe de réécriture d’adresse [1] nous allons maintenant nous intéresser à la vraie finalité de cette suite d’article sur Iptables: comment docker réalise ses réseaux virtuels?

À coup sûr vous l’aurez compris, Iptables va avoir un rôle. Mais avant d’en arriver à cela, reprenons la construction d’un nouveau réseau virtuel docker.

Par défaut, les réseaux docker sont au nombre de 3 [2]:

$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
ff147d84a29e        bridge              bridge              local
8932db1d0751        host                host                local
9a54dab98d45        none                null                local  

Ces trois types de réseau sont gérés par trois drivers spécifiques:
  • le driver null permettant d’isoler complètement les conteneurs qui serait associé à ce réseau en ne lui ne leur associant aucune adresse ip.
  • le drivers host qui permet de partager l’interface réseau de la machine hôte avec les conteneurs associé à ce réseau.
  • le driver bridge qui permet ici de construire une nouvelle interface réseau sur la machine hôte (un bridge entre autre! [3])
C’est ce dernier driver qui va nous intéresser. En effet en construisant un bridge, docker va construire une interface réseau dédiée au réseau construit spécifiquement pour l’ensemble des conteneurs qui seront déployés dans celui ci. Et afin de permettre à ces conteneurs de communiquer avec le monde extérieur, alors le bridge va servir de gateway et ce grâce …. à IpTable!



Essayons [docker-networking]:


$ docker network create net-test
d4aaba91e629e5b7f1cf14f81a2f8ce279307579430280c67fe081ebecef0c39

$ docker network inspect net-test
[
    {
        "Name": "net-test",
        "Id": "d4aaba91e629e5b7f1cf14f81a2f8ce279307579430280c67fe081ebecef0c39",
        "Created": "2019-11-23T17:18:36.467168945+01:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.20.0.0/16",
                    "Gateway": "172.20.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {},
        "Options": {},
        "Labels": {}
    }
]

Ainsi avec docker inspect on constate que la gateway qui à ete construit est en 172.20.0.1
Si on regarde du coté de la conf réseau de la machine hote:


$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group 
[...]
17: br-d4aaba91e629: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
    link/ether 02:42:a6:9d:11:87 brd ff:ff:ff:ff:ff:ff
    inet 172.20.0.1/16 brd 172.20.255.255 scope global br-d4aaba91e629
       valid_lft forever preferred_lft forever

Effectivement maintenant on à un bridge mais côté iptables il n’y pas grand chose à voir en dehors de la conf de base docker car nous n’avons aucun conteneur dans ce reseau.

Pour y voir plus clair considérons le docker-compose suivant:


version: "3.2"
services:
  tc-registry:
    image: registry:2.7.1
    ports:
      - 5000:5000

Le lancement de ce containeur va alors constuire par defaut un reseau dedié avec un bridge (ici nommé tc-infra-base_default), et une adresse reseau dedié au conteneur. Voyons cela:


$ docker-compose up -d tc-registry
Creating network "tc-infra-base_default" with the default driver
Creating tc-infra-base_tc-registry_1 ... done

Si on inspect ce reseau qu’avons nous:


$ docker network inspect tc-infra-base_default
[
    {
        "Name": "tc-infra-base_default",
        "Id": "c43bef5f1b354c45bc32179a2184002ec91fa21a2686199deae24eacccdbcdff",
        "Created": "2019-11-23T17:32:01.141824783+01:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.21.0.0/16",
                    "Gateway": "172.21.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": true,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "68d130ea5de4fab1dedf61a8c026bf19e8b90f9d28cdd16268e874a9f46558c5": {
                "Name": "tc-infra-base_tc-registry_1",
                "EndpointID": "c4133255d2a0d9a546d54bf3d8491dcc62990aca5d2ceeb2b32c0a051dba2f3a",
                "MacAddress": "02:42:ac:15:00:02",
                "IPv4Address": "172.21.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {
            "com.docker.compose.network": "default",
            "com.docker.compose.project": "tc-infra-base"
        }
    }
]

On retrouve ici une gateway en 172.21.0.1 et on constate egelement que la conteneur de la registry à elle aussi une adresse ip dans le reseau 172.21.0.0, c’est à dire 172.21.0.2

Maintenant que nous avons un conteneur dans le reseau, regardons du coté de iptables ce que cela implique.

Du coté de la table nat:


$ sudo iptables -t nat -L
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
DOCKER     all  --  anywhere             anywhere             ADDRTYPE match dst-type LOCAL

Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
DOCKER     all  --  anywhere            !localhost/8          ADDRTYPE match dst-type LOCAL

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination
MASQUERADE  all  --  172.21.0.0/16        anywhere
MASQUERADE  all  --  172.17.0.0/16        anywhere
MASQUERADE  tcp  --  172.21.0.2           172.21.0.2           tcp dpt:5000

Chain DOCKER (2 references)
target     prot opt source               destination
RETURN     all  --  anywhere             anywhere
RETURN     all  --  anywhere             anywhere
DNAT       tcp  --  anywhere             anywhere             tcp dpt:5000 to:172.21.0.2:5000

On voit que notre réseau existe ici POSTROUTING à côté du réseau docker par défaut (le bridge par défaut sur le réseau en 172.17.0.0). Également une règle DNAT est appliqué afin de router les trames vers le conteneur.

Du côté de la table filter:


$ sudo iptables -t filter -L
Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain FORWARD (policy DROP)
target     prot opt source               destination
DOCKER-USER  all  --  anywhere             anywhere
DOCKER-ISOLATION-STAGE-1  all  --  anywhere             anywhere
ACCEPT     all  --  anywhere             anywhere             ctstate RELATED,ESTABLISHED
DOCKER     all  --  anywhere             anywhere
ACCEPT     all  --  anywhere             anywhere
ACCEPT     all  --  anywhere             anywhere
ACCEPT     all  --  anywhere             anywhere             ctstate RELATED,ESTABLISHED
DOCKER     all  --  anywhere             anywhere
ACCEPT     all  --  anywhere             anywhere
ACCEPT     all  --  anywhere             anywhere

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

Chain DOCKER (2 references)
target     prot opt source               destination
ACCEPT     tcp  --  anywhere             172.21.0.2           tcp dpt:5000

Chain DOCKER-ISOLATION-STAGE-1 (1 references)
target     prot opt source               destination
DOCKER-ISOLATION-STAGE-2  all  --  anywhere             anywhere
DOCKER-ISOLATION-STAGE-2  all  --  anywhere             anywhere
RETURN     all  --  anywhere             anywhere

Chain DOCKER-ISOLATION-STAGE-2 (2 references)
target     prot opt source               destination
DROP       all  --  anywhere             anywhere
DROP       all  --  anywhere             anywhere
RETURN     all  --  anywhere             anywhere

Chain DOCKER-USER (1 references)
target     prot opt source               destination
RETURN     all  --  anywhere             anywhere

Dans cette table on constate la présence des règles d’isolation permettant de consolider le sous réseau. De même il y à une règle permettant d’autoriser (ACCEPT dans la chain DOCKER) les trames à être transmisent au conteneur.

Conclusions

La gestion des réseaux dans docker est un incontournable pour bénéficier de toute la puissance cet outil. A l’inverse, celle de iptable ne l’est heureusement pas.

Pourtant pour une utilisation avancé de docker, il est important de mesurer au combien docker masque énormément d'élément de complexité.

Avec cet article, le but était d’explorer (et encore plutôt en surface) ce point toujours assez mystérieux de la gestion réseau dans docker.

Références:

[1] https://un-est-tout-et-tout-est-un.blogspot.com/2019/10/reseau-iptables-routeur-logiciel.html
[2] https://stackoverflow.com/questions/41083328/what-is-the-use-of-host-and-none-network-in-docker
[3] https://fr.wikipedia.org/wiki/Pont_(r%C3%A9seau)
[docker-networking] https://success.docker.com/article/networking
[iptables-docker] https://docs.docker.com/network/iptables/

Aucun commentaire:

Enregistrer un commentaire