はじめに

こんにちは。澤井です!
Dockerを触る機会が多くなりました。

本記事はDockerのブリッジネットワークにおけるコンテナ間通信について記載しています。
具体的にイメージできるようにネットワークインターフェースを流れるパケットも観察したいと思います。

ネットワークインターフェースは、ネットワークインターフェースカード(NIC)やネットワークアダプタと呼ぶこともあります。

以降、本記事ではネットワークインターフェースを単にインターフェースと記載します。

目次

検証環境

本記事は以下の環境で確認しました。

  • Amazon EC2 Ubuntu 20.04.4 LTS (Focal Fossa)

準備

本記事を読むにあたり、知っておくと良い内容を簡単にまとめます。

  • 本記事ではネットワークドライバがbridgeのDockerネットワークをブリッジネットワークと記載します
  • Dockerでドライバを指定せずにネットワークを作成(docker network create)するとブリッジネットワークが作成されます
  • ブリッジネットワークは仮想ブリッジを使用します(ブリッジはOSI参照モデルのデータリンク層における通信を制御します)
  • Dockerは仮想イーサネット(veth)を使用します(仮想イーサネットは両端にインターフェースを持ちます)
  • ユーザーが作成したブリッジネットワークをユーザー定義ブリッジネットワークと呼びます

本記事はユーザー定義ブリッジネットワークに配置したコンテナ間の通信について記載します。

ユーザー定義ブリッジネットワークを作成

ユーザー定義ブリッジネットワークを作成する前の状態

まずはユーザー定義ブリッジネットワークを作成する前のホストのインターフェースを確認します。
以降、コマンド発行結果で本記事に直接関係のない内容は省略しています。

// ホストのインターフェースを確認
host$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    inet 127.0.0.1/8 scope host lo
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc fq_codel state UP group default qlen 1000
    inet 10.0.0.2/24 brd 10.0.0.255 scope global dynamic eth0
4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0

Dockerをインストールした直後のホストのインターフェースは3つです。

  • 1: lo: ループバックのインターフェース(172.0.0.1
  • 2: eth0ホストのデフォルトのインターフェース(10.0.0.2
    • デフォルトゲートウェイ(10.0.0.1)と接続されたインターフェース
  • 4: docker0:Dockerインストール時に自動で作成されるデフォルトのブリッジネットワークのインターフェース(172.17.0.1

ではユーザー定義ブリッジネットワークを作成します。

最終的なネットワーク図

本記事で作成するユーザー定義ブリッジネットワークは以下のようになります。

最終的なネットワーク(完全版)

ブリッジネットワークを作成

sampleという名前でブリッジネットワークを作成します。

host$ sudo docker network create sample
29faca36a6b4...

sampleネットワークが作成されました(ネットワークアドレスは172.18.0.0/16)。

host$ sudo docker network inspect sample
[
    {
        "Name": "sample",
        "Id": "29faca36a6b4...",
        "Driver": "bridge",
        "IPAM": {
            "Config": [
                {
                    "Subnet": "172.18.0.0/16",
                    "Gateway": "172.18.0.1"
                }
            ]
        },
    }
]

sampleネットワークの作成に伴って仮想ブリッジが作成されます。

  • ホストに仮想ブリッジ(br-29faca36a6b4)が作成されます
  • ホストに仮想ブリッジのインターフェース(br-29faca36a6b4)が作成されます

※ 例ではブリッジ名とブリッジのインターフェース名が同じです。

上記を図にすると以下のようになります。

ブリッジネットワークを作成

ブリッジを確認します。

// brctlはブリッジを表示する
host$ sudo brctl show
bridge name bridge id STP enabled interfaces
br-29faca36a6b4 8000.0242846801c8 no

br-29faca36a6b4というブリッジが作成されています。
※ この時点ではinterfacesnoになっています(理由は後述します)。

ホストのインターフェースを確認します。

host$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    inet 127.0.0.1/8 scope host lo
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc fq_codel state UP group default qlen 1000
    inet 10.0.0.2/24 brd 10.0.0.255 scope global dynamic eth0
4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
5: br-29faca36a6b4: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
    inet 172.18.0.1/16 brd 172.18.255.255 scope global br-29faca36a6b4

br-29faca36a6b4インターフェース(172.18.0.1)が追加されています。

1つめのコンテナを追加

sampleネットワークにコンテナを追加します。 コンテナ名はcontainer1にします。

host$ sudo docker run -itd --name container1 --net sample  ubuntu:latest

ここまでを図にすると以下のようになります。

1つめのコンテナを追加

ホストのインターフェースを確認します。

host$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    inet 127.0.0.1/8 scope host lo
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc fq_codel state UP group default qlen 1000
    inet 10.0.0.2/24 brd 10.0.0.255 scope global dynamic eth0
4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
5: br-29faca36a6b4: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
    inet 172.18.0.1/16 brd 172.18.255.255 scope global br-29faca36a6b4
7: veth7a0c3b1@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-29faca36a6b4 state UP group default
    inet6 fe80::10b9:a1ff:feec:976/64 scope link

ホストveth7a0c3b1@if6インターフェースが追加されています。
このインターフェースは、ブリッジ(br-29faca36a6b4)とコンテナ(container1)を接続する仮想イーサネットのブリッジ側インターフェースです。

次にコンテナ内のインターフェースを確認します。

host$ sudo docker exec -it container1 /bin/bash
container1# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    inet 127.0.0.1/8 scope host lo
6: eth0@if7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    inet 172.18.0.2/16 brd 172.18.255.255 scope global eth0

eth0@if7インターフェースが作成されています。 eth0@if7はブリッジ側のveth7a0c3b1@if6と接続されています。

eth0@if7@if7はホストの7のインターフェースであるveth7a0c3b1と接続しているという意味です。
逆にveth7a0c3b1@if6はコンテナの6のインターフェースeth0と接続しているという意味です。

コンテナのルートテーブルも確認します。
ゲートウェイに172.18.0.1が設定されています(172.18.0.1は、ブリッジのbr-29faca36a6b4インターフェースに付与されています)。

container1# route -ne

// FlagsのGがゲートウェイを表す
Kernel IP routing table
Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface
0.0.0.0         172.18.0.1      0.0.0.0         UG        0 0          0 eth0
172.18.0.0      0.0.0.0         255.255.0.0     U         0 0          0 eth0

2つめのコンテナを追加

コンテナを追加します。
コンテナ名はcontainer2にします。

host$ sudo docker run -itd --name container2 --net sample  ubuntu:latest

ここまでを図にすると以下のようになります。

2つめのコンテナを追加

ホストのインターフェースを確認します。

host$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    inet 127.0.0.1/8 scope host lo
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc fq_codel state UP group default qlen 1000
    inet 10.0.0.2/24 brd 10.0.0.255 scope global dynamic eth0
4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
5: br-29faca36a6b4: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
    inet 172.18.0.1/16 brd 172.18.255.255 scope global br-29faca36a6b4
7: veth7a0c3b1@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-29faca36a6b4 state UP group default
    inet6 fe80::10b9:a1ff:feec:976/64 scope link
9: veth2a5f404@if8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-29faca36a6b4 state UP group default
    inet6 fe80::38aa:3bff:fee3:ed9b/64 scope link

ホストveth2a5f404@if8インターフェースが追加されています。
これはブリッジとコンテナ(container2)を接続する仮想イーサネットのブリッジ側インターフェースです。

次にコンテナ内のインターフェースを確認します。

host$ sudo docker exec -it container2 /bin/bash
container2# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    inet 127.0.0.1/8 scope host lo
8: eth0@if9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    inet 172.18.0.3/16 brd 172.18.255.255 scope global eth0

コンテナにeth0@if9インターフェースが作成されています。
eth0@if9はブリッジ側のveth2a5f404@if8と接続されています

コンテナのルートテーブルも確認します。
ゲートウェイに172.18.0.1が設定されています(172.18.0.1は、ブリッジのbr-29faca36a6b4インターフェースに付与されています)。

container2# route -ne
Kernel IP routing table
Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface
0.0.0.0         172.18.0.1      0.0.0.0         UG        0 0          0 eth0
172.18.0.0      0.0.0.0         255.255.0.0     U         0 0          0 eth0

コンテナ追加後のブリッジを確認

再掲になりますが、以下のようなユーザー定義ブリッジネットワークが作成されました。

最終的なネットワーク(完全版)

コンテナを2つ追加した後のブリッジbr-29faca36a6b4の状態を確認してみます。

host$ brctl show br-29faca36a6b4
bridge name	bridge id STP enabled interfaces
br-29faca36a6b4	8000.0242c107547f no veth2a5f404
                                     veth7a0c3b1

コンテナを2つ追加したのでブリッジとコンテナを繋ぐ2つの仮想イーサネットが作成されました。
interfaces列に仮想イーサネットのブリッジ側インターフェースが追加されています(veth2a5f404veth7a0c3b1)。

ここまでで、container1とcontainer2が通信できるようになります。
実際にインターフェースの通信をキャプチャしてデータの流れを確認します。

コンテナ間の通信を確認

コンテナ間の通信を確認するためにcontainer1(172.18.0.2)からcontainer2(172.18.0.3)にpingを発行してパケットの流れをみます。

今回は分かりやすさのためにIPアドレスを使用しますが、ユーザー定義ブリッジネットワークはDockerデーモンによって内蔵DNSサーバ機能が提供されています。
ですのでコンテナ名でも通信可能です。

pingによって発行されたICMP echo requestパッケットは以下経路でcontainer2に到達します。

  1. container1のeth0@if7インターフェース
  2. ブリッジ側のcontainer1に対応するveth7a0c3b1@if6インターフェース
  3. ブリッジのbr-29faca36a6b4インターフェース
  4. ブリッジ側のcontainer2に対応するveth2a5f404@if8インターフェース
  5. container2のeth0@if9インターフェース

ICMP echo replyは逆の流れでcontainer2からcontainer1に到達します。

コンテナ間通信

1. container1のeth0@if7インターフェースをキャプチャ

container1# tcpdump -tnl -i eth0@if7
IP 172.18.0.2 > 172.18.0.3: ICMP echo request, id 1, seq 1, length 64
IP 172.18.0.3 > 172.18.0.2: ICMP echo reply, id 1, seq 1, length 64

2. ブリッジ側のveth7a0c3b1インターフェースをキャプチャ

host$ sudo tcpdump -tnl -i veth7a0c3b1 icmp
IP 172.18.0.2 > 172.18.0.3: ICMP echo request, id 1, seq 1, length 64
IP 172.18.0.3 > 172.18.0.2: ICMP echo reply, id 1, seq 1, length 64

3. ブリッジのbr-29faca36a6b4インターフェースをキャプチャ

host$ sudo tcpdump -tnl -i br-29faca36a6b4 icmp
IP 172.18.0.2 > 172.18.0.3: ICMP echo request, id 2, seq 1, length 64
IP 172.18.0.3 > 172.18.0.2: ICMP echo reply, id 2, seq 1, length 64

4. ブリッジ側のveth2a5f404インターフェースをキャプチャ

host$ sudo tcpdump -tnl -i veth2a5f404 icmp
IP 172.18.0.2 > 172.18.0.3: ICMP echo request, id 1, seq 1, length 64
IP 172.18.0.3 > 172.18.0.2: ICMP echo reply, id 1, seq 1, length 64

5. container2のeth0@if9インターフェースをキャプチャ

container2# tcpdump -tnl -i eth0@if9 icmp
IP 172.18.0.2 > 172.18.0.3: ICMP echo request, id 1, seq 1, length 64
IP 172.18.0.3 > 172.18.0.2: ICMP echo reply, id 1, seq 1, length 64

ブリッジを経由したcontainer1(172.18.0.2)とcontainer2(172.18.0.3)の通信を確認できました。

コンテナと外部の通信を確認

最後にブリッジネットワークと外部の通信について確認します。

外部との通信はホストが提供するiptablesのIPマスカレード(NAT)を使用して実現しています(IPマスカレードはNATのLinuxの実装です)。

コンテナから外部に送信される際にIPマスカレードによって、ホストのIPアドレスに変換されてeth0から送信されます。
IPマスカレードはDockerが自動でiptablesに設定します。

iptablesに設定されたIPマスカレードを確認します(必要な部分のみ掲載しています)。

host$ sudo iptables -nL -t nat

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination
MASQUERADE  all  --  172.18.0.0/16        0.0.0.0/0

上記のとおり172.18.0.0/16から外部への通信にはMASQUERADEが設定されています。

外部との通信を確認

container1から93.184.216.34(example.org)にpingを発行してsampleネットワークと外部との通信を確認します。 パケットは以下の経路を通ります

  1. container1のeth0@if7インターフェース
  2. ブリッジ側のcontainer1に対応するveth7a0c3b1@if6インターフェース
  3. ブリッジのbr-29faca36a6b4インターフェース
  4. ホストのeth0インターフェース

ICMP echo replyは逆の順序でcontainer1のeth0@if7インターフェースに届きます。

tcpdumpでそれぞれのインターフェースの通信をキャプチャします。

1. container1のeth0@if7インターフェースをキャプチャ

container1# tcpdump -tnl -i eth0 icmp

IP 172.18.0.2 > 93.184.216.34: ICMP echo request, id 5, seq 1, length 64
IP 93.184.216.34 > 172.18.0.2: ICMP echo reply, id 5, seq 1, length 64

2. ブリッジ側のcontainer1に対応するveth7a0c3b1@if6インターフェースをキャプチャ

host$ sudo tcpdump -tnl -i veth7a0c3b1 icmp

IP 172.18.0.2 > 93.184.216.34: ICMP echo request, id 5, seq 1, length 64
IP 93.184.216.34 > 172.18.0.2: ICMP echo reply, id 5, seq 1, length 64

3. ブリッジのbr-29faca36a6b4インターフェースをキャプチャ

host$ sudo tcpdump -tnl -i br-29faca36a6b4 icmp

IP 172.18.0.2 > 93.184.216.34: ICMP echo request, id 8, seq 1, length 64
IP 93.184.216.34 > 172.18.0.2: ICMP echo reply, id 8, seq 1, length 64

4. ホストのeth0インターフェースをキャプチャ

IPマスカレードによってIPが10.0.0.2に書き換えられて送信されています。

host$ sudo tcpdump -tnl -i eth0 icmp

IP 10.0.0.2 > 93.184.216.34: ICMP echo request, id 9, seq 1, length 64
IP 93.184.216.34 > 10.0.0.2: ICMP echo reply, id 9, seq 1, length 64

上記のとおりホストのeth0インターフェースから、ホストのIPアドレス(10.0.0.2)に変換されて送受信されています。 (今回はAmazon EC2で確認しているために、IPマスカレードによって変換されたアドレスが10.0.0.2といプライベートアIPドレスになっていますが、 環境によっては、この時点でグローバルIPになっている場合もあります。)

以上、ざっとブリッジネットワークの通信を観察してきました。

まとめ

実際に作成されたリソースを確認することでネットワークに関する理解が深まりました。 参考になれば幸いです。