Linux Bridgeを介した通信ができない

## はじめに
タイトルについてのトラブルシュートに際して調査を行ったところ、案外本件にクリティカルに言及する記事が少なかったため記事にさせていただきました。

## 事象
CentOS7にLinux Bridgeを作成し、配下にdockerコンテナをアサインして疎通確認したところ疎通を行うことができなかった。
<img width="804" alt="qiita-bridge-1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246372/b26f2d21-4f3c-151e-1691-69b5ea8cd18f.png">

## 結論
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
~~~

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

<img width="747" alt="qiita-bridge-2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246372/f1e4fccd-43ed-28d1-1e31-05592947cbc2.png">

### 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
~~~

ここまでの操作を行うことで、以下の絵のような状態になります。

<img width="747" alt="qiita-bridge-3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246372/db6fa890-a8f6-4774-0d2a-476e073bec14.png">

### 新規作成したBridgeを介して疎通確認
新規作成したnew-bridge1を介して、先ほどのdocker0と同じようにcent1からcent２へ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までは届いていない

<img width="804" alt="qiita-bridge-4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246372/1caf36a1-e10d-8fcb-e44c-2a01e1c828ca.png">

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 = １とすることが指定されています。
自身は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仮想マシンをアサインして同様の問題が発生した際も、この例で解決できるのではないかと想定しています。（検証はしていません）

## お礼
本調査に当たっては周囲の方々に様々な調査協力をいただいたり、ご相談をさせていただきました。この場合を借りてお礼申し上げます。

#### 参考
KVMブリッジネットワークの設定
https://qiita.com/TsutomuNakamura/items/e15d2c8c02586a7ae572#bridge-%E3%83%88%E3%83%A9%E3%83%95%E3%82%A3%E3%83%83%E3%82%AF%E3%81%AEnetfilter-%E3%82%92%E7%84%A1%E5%8A%B9%E5%8C%96%E3%81%99%E3%82%8B

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