はじめに
タイトルについてのトラブルシュートに際して調査を行ったところ、案外本件にクリティカルに言及する記事が少なかったため記事にさせていただきました。
事象
CentOS7にLinux Bridgeを作成し、配下にdockerコンテナをアサインして疎通確認したところ疎通を行うことができなかった。
結論
Linux Bridgeはbridgeというカーネルモジュールを使って動作しているが、そののセキュリティはbr_netfilter(bridgeと依存関係)というカーネルモジュールで管理されており、br_netfilterはiptalesの設定を見て通信を制御している。そのため以下いずれかを行うことで疎通が行えるようになる。
①Bridge Netfilterを無効化する
②iptablesに許可設定を行う
検証環境
OS:CentOS 7.5
Kernel Ver:3.10.0-862.14.4.el7.x86_64
docker Ver:18.06.1-ce
通常のdocker0での検証
まずは通常dockerコンテナをデプロイした際にコンテナがアサインされるdocker0を介して、コンテナ同士の疎通が行えることを確認します。
dockerコンテナをデプロイ
docker runを実行し、コンテナを2つデプロイします。
# docker run -d --name cent1 centos/tools:latest /sbin/init
# docker run -d --name cent2 centos/tools:latest /sbin/init
docker psコマンドでコンテナが正常に起動したことを確認します。
# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
8126f9f72ee2 centos/tools:latest "/sbin/init" 6 seconds ago Up 3 seconds cent2
a957a097b6a5 centos/tools:latest "/sbin/init" About a minute ago Up About a minute cent1
docker0へのアサイン状況確認
まずはデプロイしたコンテナのNICとdockerホストのNICの紐付けを確認します。
dockerコンテナのそれぞれのNICを確認すると以下の通りです。
cent1のeth0はdockerホストのindex9
cent2のeth0はdockerホストのindex11
に紐づいていることがわかります。
# docker exec cent1 ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
8: eth0@if9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
# docker exec cent2 ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
10: eth0@if11: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
dockerホストのNICを確認すると以下の通りです。
# ip l
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether 00:0c:29:b3:b5:18 brd ff:ff:ff:ff:ff:ff
3: ens192: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether 00:0c:29:b3:b5:22 brd ff:ff:ff:ff:ff:ff
4: vlan10@ens192: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
link/ether 00:0c:29:b3:b5:22 brd ff:ff:ff:ff:ff:ff
5: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
link/ether 02:42:1c:c2:6d:d0 brd ff:ff:ff:ff:ff:ff
9: vethc59a2d1@if8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default
link/ether f6:1a:1b:00:b9:b5 brd ff:ff:ff:ff:ff:ff link-netnsid 0
11: vethfee6857@if10: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default
link/ether 86:45:ea:11:db:35 brd ff:ff:ff:ff:ff:ff link-netnsid 1
さらにLinux Bridgeの情報を確認してみると以下のように、docker0にcent1,2のホスト側のvethがアサインされていることがわかります。
# brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.02421cc26dd0 no vethc59a2d1
vethfee6857
上記をまとめると以下の絵のようになります。
docker0を介した疎通確認
docker0を介してcent1からcent2にpingを飛ばして疎通確認すると以下のように正常に疎通を行うことができます。
# docker exec cent2 ping -c 3 172.17.0.3
PING 172.17.0.3 (172.17.0.3) 56(84) bytes of data.
64 bytes from 172.17.0.3: icmp_seq=1 ttl=64 time=10.2 ms
64 bytes from 172.17.0.3: icmp_seq=2 ttl=64 time=0.048 ms
64 bytes from 172.17.0.3: icmp_seq=3 ttl=64 time=0.045 ms
--- 172.17.0.3 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2001ms
rtt min/avg/max/mdev = 0.045/3.448/10.252/4.811 ms
新規に作成したBridgeでの検証
ここからが本題です。
続いて新規でLinux Bridgeを作成し、dockerコンテナをアサインした時、docker0と同じく疎通ができるか確認します。
新規Bridgeの作成
新しいbridgeとしてnew-bridge1という名前のBridgeを作成します。
# brctl addbr new-bridge1
# brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.02421cc26dd0 no vethc59a2d1
vethfee6857
new-bridge1 8000.000000000000 no
作成したら以下のようにBridgeを起動しておきます。
# ip l set dev new-bridge1 up
# ip l
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether 00:0c:29:b3:b5:18 brd ff:ff:ff:ff:ff:ff
3: ens192: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether 00:0c:29:b3:b5:22 brd ff:ff:ff:ff:ff:ff
4: vlan10@ens192: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
link/ether 00:0c:29:b3:b5:22 brd ff:ff:ff:ff:ff:ff
5: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default
link/ether 02:42:1c:c2:6d:d0 brd ff:ff:ff:ff:ff:ff
9: vethc59a2d1@if8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master new-bridge1 state UP mode DEFAULT group default
link/ether f6:1a:1b:00:b9:b5 brd ff:ff:ff:ff:ff:ff link-netnsid 0
11: vethfee6857@if10: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master new-bridge1 state UP mode DEFAULT group default
link/ether 86:45:ea:11:db:35 brd ff:ff:ff:ff:ff:ff link-netnsid 1
12: new-bridge1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
link/ether 86:45:ea:11:db:35 brd ff:ff:ff:ff:ff:ff
docker0からコンテナNICを除外する
dockerでデプロイしたコンテナのNIC(正確にはdockerホスト側のコンテナNICに対応するveth)はdocker0にアサインされた状態にあります。
検証のため、これらのコンテナNICをdocker0から除外します。
# brctl delif docker0 vethc59a2d1
# brctl delif docker0 vethfee6857
# brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.02421cc26dd0 no
new-bridge1 8000.000000000000 no
作成したBridgeにコンテナNICをアサインする
コンテナNICを新規で作成したnew-bridge1にアサインします。
# brctl addif new-bridge1 vethc59a2d1
# brctl addif new-bridge1 vethfee6857
# brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.02421cc26dd0 no
new-bridge1 8000.8645ea11db35 no vethc59a2d1
vethfee6857
ここまでの操作を行うことで、以下の絵のような状態になります。
新規作成したBridgeを介して疎通確認
新規作成したnew-bridge1を介して、先ほどのdocker0と同じようにcent1からcent2へpingを飛ばして疎通確認してみます。
# docker exec cent1 ping -c 3 172.17.0.3
PING 172.17.0.3 (172.17.0.3) 56(84) bytes of data.
--- 172.17.0.3 ping statistics ---
3 packets transmitted, 0 received, 100% packet loss, time 1999ms
すると先ほどのdocker0を介した場合と異なり、cent1とcent2の間で疎通できていないことがわかります。
事象調査
パケットキャプチャ
まずは各NICにてtcpdumpを取得してみます。
以下の結果から2つのことがわかります。
①ARPリクエストは正常にcent1からcent2に届いており、cent1はそのレスポンスを受け取れている
②pingはLinux Bridge(new-bridge1)までは届いているがcent2までは届いていない
cent1のNIC
# tcpdump -i vethc59a2d1
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on vethc59a2d1, link-type EN10MB (Ethernet), capture size 262144 bytes
23:20:39.379638 IP 172.17.0.2 > 172.17.0.3: ICMP echo request, id 45, seq 1, length 64
23:20:40.378780 IP 172.17.0.2 > 172.17.0.3: ICMP echo request, id 45, seq 2, length 64
23:20:41.378785 IP 172.17.0.2 > 172.17.0.3: ICMP echo request, id 45, seq 3, length 64
23:20:44.383711 ARP, Request who-has 172.17.0.3 tell 172.17.0.2, length 28
23:20:44.383744 ARP, Reply 172.17.0.3 is-at 02:42:ac:11:00:03 (oui Unknown), length 28
cent2のNIC
# tcpdump -i vethfee6857
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on vethfee6857, link-type EN10MB (Ethernet), capture size 262144 bytes
23:20:44.383726 ARP, Request who-has 172.17.0.3 tell 172.17.0.2, length 28
23:20:44.383741 ARP, Reply 172.17.0.3 is-at 02:42:ac:11:00:03 (oui Unknown), length 28
new-bridge1
# tcpdump -i new-bridge1
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on new-bridge1, link-type EN10MB (Ethernet), capture size 262144 bytes
23:20:39.379638 IP 172.17.0.2 > 172.17.0.3: ICMP echo request, id 45, seq 1, length 64
23:20:40.378780 IP 172.17.0.2 > 172.17.0.3: ICMP echo request, id 45, seq 2, length 64
23:20:41.378785 IP 172.17.0.2 > 172.17.0.3: ICMP echo request, id 45, seq 3, length 64
23:20:44.383711 ARP, Request who-has 172.17.0.3 tell 172.17.0.2, length 28
23:20:44.383741 ARP, Reply 172.17.0.3 is-at 02:42:ac:11:00:03 (oui Unknown), length 28
①については念のため各コンテナ内でARPキャッシュを確認してみますが、
それぞれ正常に通信相手のMACアドレスが書き込まれていることがわかります。
# docker exec cent1 arp -e
Address HWtype HWaddress Flags Mask Iface
172.17.0.3 ether 02:42:ac:11:00:03 C eth0
gateway (incomplete) eth0
# docker exec cent2 arp -e
Address HWtype HWaddress Flags Mask Iface
172.17.0.2 ether 02:42:ac:11:00:02 C eth0
gateway (incomplete) eth0
なぜこんなことが起こるのか
Linux Bridgeはbridgeというカーネルモジュールを使って動作していますが、そののセキュリティはbr_netfilter(bridgeと依存関係)というカーネルモジュールで管理されており、br_netfilterはiptablesの設定を見て通信を制御しているようです。
そのためデフォルトではBridgeを介した通信を許可しておらず、今回のようなことが起こります。
$ lsmod | grep br_netfilter
br_netfilter 24576 0
bridge 155648 1 br_netfilter
解決方法
以下いずれかの対応により、コンテナ間の疎通が行えるようになります。
その1 Bridge Netfilterを無効化する
通常Linux Bridgeの通信を制御しているNetfilterは有効な状態となっていますが、
これを意図的に無効化してやれば疎通を行うことができます。
なお、Bridge Netfilterの有効無効はカーネルパラメータnet.bridge.bridge-nf-call-iptables
により設定できます。
Bridge Netfilterの状態確認
現在は1が設定されており有効な状態です。
# sysctl net.bridge.bridge-nf-call-iptables
net.bridge.bridge-nf-call-iptables = 1
設定変更
/etc/sysctl.confにてnet.bridge.bridge-nf-call-iptables = 0を設定し、
設定を反映させます。
# cat /etc/sysctl.conf
# sysctl settings are defined through files in
# /usr/lib/sysctl.d/, /run/sysctl.d/, and /etc/sysctl.d/.
#
# Vendors settings live in /usr/lib/sysctl.d/.
# To override a whole file, create a new file with the same in
# /etc/sysctl.d/ and put new settings there. To override
# only specific settings, add a file with a lexically later
# name in /etc/sysctl.d/ and put new settings there.
#
# For more information, see sysctl.conf(5) and sysctl.d(5).
net.bridge.bridge-nf-call-iptables = 0
# sysctl -p
net.bridge.bridge-nf-call-iptables = 0
# sysctl net.bridge.bridge-nf-call-iptables
net.bridge.bridge-nf-call-iptables = 0
その2 iptablesに許可設定を行う
Bridge Netfilterはiptablesを参照して通信の制御を行っています。
そのため以下のようにiptablesに許可ルールを追記することで
Linux Bridgeを介した通信を行うことができるようになります。
なおここではiptablesコマンドでルールを追加する際、-mでphysdevというブリッジの入出力を管理するパケットマッチングモジュールを指定することで、Brige内を経由する全ての通信を許可する設定としています。
iptables - システム管理コマンドの説明 - Linux コマンド集 一覧表
https://kazmax.zpp.jp/cmd/i/iptables.8.html
※従来iptablesはCenOSにおいて通信制御の機能を担っていましたが、
CentOS7では通信制御の機能としてiptablesに代わりfirewalldが使用されています。
しかしながらiptablesの設定自体は残されており、今回のようにNetfilterで利用されているようです。
# iptables -I FORWARD -m physdev --physdev-is-bridged -j ACCEPT
# iptables -nvL --line-number
Chain INPUT (policy ACCEPT 52 packets, 3250 bytes)
num pkts bytes target prot opt in out source destination
Chain FORWARD (policy DROP 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
1 0 0 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 PHYSDEV match --physdev-is-bridged
2 2006 2508K DOCKER-USER all -- * * 0.0.0.0/0 0.0.0.0/0
3 2006 2508K DOCKER-ISOLATION-STAGE-1 all -- * * 0.0.0.0/0 0.0.0.0/0
4 1126 2451K ACCEPT all -- * docker0 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
5 46 5840 DOCKER all -- * docker0 0.0.0.0/0 0.0.0.0/0
6 834 51247 ACCEPT all -- docker0 !docker0 0.0.0.0/0 0.0.0.0/0
7 46 5840 ACCEPT all -- docker0 docker0 0.0.0.0/0 0.0.0.0/0
(略)
kubernetesではnet.bridge.bridge-nf-call-iptables = 1とすることが指定されています。
自身はkubernetesで以下の検証を行っている際にこの問題に直面したため、
iptablesのルール追加にて対応しました。
Multusで遊ぶ
https://rheb.hatenablog.com/entry/multus_introduction
docker0を介した通信はなぜ行うことができるのか
ここで1つ「なぜdocker0も実態は同じLinux Bridgeなのに疎通できるのか」疑問が湧きます。
その答えはiptablesの設定にあります。
dockerはインストール時およびdockerネットワーク作成時に必要なルールをiptablesに記載してくれているようです。
iptablesの設定情報を確認すると、FRWARD ChainのNo.5,6にてdocker0から外部宛ての通信およびdocker0を介した通信がACCEPTされているのがわかります。dockerではその他にもiptablesにNAT設定などを行っています。
# iptables -nvL --line-number
Chain INPUT (policy ACCEPT 228K packets, 579M bytes)
num pkts bytes target prot opt in out source destination
Chain FORWARD (policy DROP 12 packets, 1008 bytes)
num pkts bytes target prot opt in out source destination
1 9003 12M DOCKER-USER all -- * * 0.0.0.0/0 0.0.0.0/0
2 9003 12M DOCKER-ISOLATION-STAGE-1 all -- * * 0.0.0.0/0 0.0.0.0/0
3 5650 12M ACCEPT all -- * docker0 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
4 0 0 DOCKER all -- * docker0 0.0.0.0/0 0.0.0.0/0
5 3341 191K ACCEPT all -- docker0 !docker0 0.0.0.0/0 0.0.0.0/0
6 0 0 ACCEPT all -- docker0 docker0 0.0.0.0/0 0.0.0.0/0
Chain OUTPUT (policy ACCEPT 130K packets, 7700K bytes)
num pkts bytes target prot opt in out source destination
Chain DOCKER (1 references)
num pkts bytes target prot opt in out source destination
Chain DOCKER-ISOLATION-STAGE-1 (1 references)
num pkts bytes target prot opt in out source destination
1 3341 191K DOCKER-ISOLATION-STAGE-2 all -- docker0 !docker0 0.0.0.0/0 0.0.0.0/0
2 9003 12M RETURN all -- * * 0.0.0.0/0 0.0.0.0/0
Chain DOCKER-ISOLATION-STAGE-2 (1 references)
num pkts bytes target prot opt in out source destination
1 0 0 DROP all -- * docker0 0.0.0.0/0 0.0.0.0/0
2 3341 191K RETURN all -- * * 0.0.0.0/0 0.0.0.0/0
Chain DOCKER-USER (1 references)
num pkts bytes target prot opt in out source destination
1 9003 12M RETURN all -- * * 0.0.0.0/0 0.0.0.0/0
まとめ
Linux Bridgeを使う上でL2レイヤのみの動作なはずだから何も気にしなくても疎通できるはずと思っていましたが誤りでした。
Linux BridgeはIP付与できるのも含め、L3的な動きもするのですね。
今回はコンテナで実験しましたが、KVM仮想マシンをアサインして同様の問題が発生した際も、この例で解決できるのではないかと想定しています。(検証はしていません)
お礼
本調査に当たっては周囲の方々に様々な調査協力をいただいたり、ご相談をさせていただきました。この場合を借りてお礼申し上げます。
参考
11.2. libvirt を使用したブリッジネットワーク
https://docs.fedoraproject.org/ja-JP/Fedora/13/html/Virtualization_Guide/sect-Virtualization-Network_Configuration-Bridged_networking_with_libvirt.html
宣伝
弱小Twitterやってます。良かったら少し付き合ってください。
https://twitter.com/mochizuki875