Network Docker Cross-Network Container Communication

在 Docker 的網路規則中,由於 DOCKER-ISOLATION-STAGE-1DOCKER-ISOLATION-STAGE-2 兩個 iptables chain 的關係,因此不同 Docker network 連結的 container 是無法互相溝通的。不過,我們可以透過建立 router 的手段來規避掉 chain 的限制,讓 container 收到其他 container 來的packages。此方式涉及到 Linux Bridge 處理封包的方式和基本網路知識,希望透過此介紹,來更了解 network stack 與 packages 處理流程。

Docker Container Network

我們知道,Docker 的網路實現是基於 Linux network namespace,Linux network bridge,與 virtual ethernet device 所架構而成的(可參閱 Linux Network Fundamentals)。

假設我們建立兩個 container,一個連結自定義網路(User-Defined Bridge Networks) host1 與連結預設網路的 host2:

1sudo docker network create h1r1br
2sudo docker run -d -it --network h1r1br --rm --name h1 ubuntu:16.04
3sudo docker run -d -it --rm --name h2 ubuntu:16.04

其結構圖示如下:

hugo

我們可以使用 brctl 來查看 bridge 和 veth 關係

1yu-shuan@:~$ brctl show
2bridge name	bridge id		STP enabled	interfaces
3br-0e510c1eeb44	 8000.02429a48a474	no		veth8b7aaa8
4docker0		         8000.024288b35644	no		vethfa6f0c4

也可以用 ifconfig 來查看 bridge 與 veth 的資訊

1yu-shuan@:~$ ifconfig br-0e510c1eeb44
2br-0e510c1eeb44: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
3        inet 172.22.0.1  netmask 255.255.0.0  broadcast 172.22.255.255
4        inet6 fe80::42:9aff:fe48:a474  prefixlen 64  scopeid 0x20<link>
5        ether 02:42:9a:48:a4:74  txqueuelen 0  (Ethernet)

我們進入 host1 container,來看 routing 設定:

1root@fb1431179a56:/# ip route
2default via 172.22.0.1 dev eth0
3172.22.0.0/16 dev eth0  proto kernel  scope link  src 172.22.0.2

由於 host1 只有一個 interface,因此無論 Dst IP 是什麼,都會從 eth0 出去,而 host2 也同理。

Connection Test

Case 1: host1 connects to the internet

接著我們看一下從 host1 container 是如何與外部 internet 聯繫的。我們從 host1 container ping google server (169.254.169.254)。

hugo

從 host1 發送 icmp 封包,由於沒有 169.254.169.254 的 MAC 資訊,因此將封包轉送到 default gateway(dst_Mac 改成 gatyeway 的 mac address),也就是 bridge br-0e510c1eeb44(h1r1br),從 eth0 出去。

當封包抵達 bridge br-0e510c1eeb44 後,就是在本機端處理此封包,因此會使用本機端的 iptable 設定。同樣地,由於沒有 169.254.169.254 的資訊,因此會轉發到本機設定的 default gateway,並檢查 filter table 中的對應 rules,最後發送到外部網路。

Case 2: host1(172.22.0.2) ping host2(172.17.0.2)

再來討論 host1 ping host2 的場景~我們第一直覺就能夠判斷一定不能通。那為什麼不能通呢?

封包處理流程跟上面第一個例子相似, icmp 封包會被轉發到本機端,其中最大的差別在於:此 icmp 封包會被 filter.FORWARD table Drop 掉,因此就無法將封包發送到目標 host2 container 中。

透過 iptables command-lin tool 來查看 table 設定:

 1yu-shuan@:~$ sudo iptables -nL --line-numbers -v -t filter
 2Chain INPUT (policy ACCEPT 1921 packets, 227K bytes)
 3num   pkts bytes target     prot opt in     out     source               destination
 41     199K 1215M sshguard   all  --  *      *       0.0.0.0/0            0.0.0.0/0
 5
 6Chain FORWARD (policy DROP 0 packets, 0 bytes)
 7num   pkts bytes target     prot opt in     out     source               destination
 81    49568   61M DOCKER-USER  all  --  *      *       0.0.0.0/0            0.0.0.0/0
 92    49568   61M DOCKER-ISOLATION-STAGE-1  all  --  *      *       0.0.0.0/0            0.0.0.0/0
10
11Chain DOCKER-ISOLATION-STAGE-1 (1 references)
12num   pkts bytes target     prot opt in     out     source               destination
131     9119  499K DOCKER-ISOLATION-STAGE-2  all  --  br-0e510c1eeb44 !br-0e510c1eeb44  0.0.0.0/0            0.0.0.0/0
142     4729  252K DOCKER-ISOLATION-STAGE-2  all  --  br-5004af5170da !br-5004af5170da  0.0.0.0/0            0.0.0.0/0
153       78 18422 DOCKER-ISOLATION-STAGE-2  all  --  br-a0789b166030 !br-a0789b166030  0.0.0.0/0            0.0.0.0/0
164      901 48486 DOCKER-ISOLATION-STAGE-2  all  --  br-e7d606a081d9 !br-e7d606a081d9  0.0.0.0/0            0.0.0.0/0
175        0     0 DOCKER-ISOLATION-STAGE-2  all  --  br-e0e7a31ff7d4 !br-e0e7a31ff7d4  0.0.0.0/0            0.0.0.0/0
186    70549 8287K DOCKER-ISOLATION-STAGE-2  all  --  docker0 !docker0  0.0.0.0/0            0.0.0.0/0
197    1161K  805M RETURN     all  --  *      *       0.0.0.0/0            0.0.0.0/0
20
21Chain DOCKER-ISOLATION-STAGE-2 (6 references)
22num   pkts bytes target     prot opt in     out     source               destination
231        0     0 DROP       all  --  *      br-0e510c1eeb44  0.0.0.0/0            0.0.0.0/0
242        0     0 DROP       all  --  *      br-5004af5170da  0.0.0.0/0            0.0.0.0/0
253        0     0 DROP       all  --  *      br-a0789b166030  0.0.0.0/0            0.0.0.0/0
264       31 15348 DROP       all  --  *      br-e7d606a081d9  0.0.0.0/0            0.0.0.0/0
275        6   504 DROP       all  --  *      br-e0e7a31ff7d4  0.0.0.0/0            0.0.0.0/0
286        2   168 DROP       all  --  *      docker0  0.0.0.0/0            0.0.0.0/0
297    85337 9089K RETURN     all  --  *      *       0.0.0.0/0            0.0.0.0/0

封包會進入 DOCKER-ISOLATION-STAGE-1,接著 match 到其中 number 1 的 rule

1Chain DOCKER-ISOLATION-STAGE-1 (1 references)
2num   pkts bytes target     prot opt in     out     source               destination
31     9119  499K DOCKER-ISOLATION-STAGE-2  all  --  br-0e510c1eeb44 !br-0e510c1eeb44  0.0.0.0/0            0.0.0.0/0

最後進入 DOCKER-ISOLATION-STAGE-2,match 到 number 6 的 rule,封包被 Drop 掉。

1Chain DOCKER-ISOLATION-STAGE-2 (6 references)
2num   pkts bytes target     prot opt in     out     source               destination
36        2   168 DROP       all  --  *      docker0  0.0.0.0/0            0.0.0.0/0

Cross-Network Container Communication via a Router

如果要讓兩個分別在不同 bridge 的 container 互相溝通,最重要的是如何符合 docker 本身在 iptables 所設定的規則。而在實體網路中,我們會使用 router 來連結兩個網段的網路,相同的概念也可以應用在 docker container 上,以實現跨 network bridge 溝通的目的。

首先,我們先起一個 router container,並且使用不同的 network 把 host1、host2、與 router 連結在一起。

hugo

如此一來,host1 的封包可以透過 h1r1br Bridge 傳送到 router1 ,接著再經過 docker0 bridge 將封包從 router1 傳送到 host2。

當然,只是將 router 1 連結 host1、host2 還不夠,我們還要設定 host1、host2 的 default gateway,這樣才能確保封包能正確地走到 router1 再傳出去。

Host1

1ip route del default
2ip route add default via 172.22.0.3

Host2

1ip route del default
2ip route add default via 172.17.0.3

經過這樣的調整後,封包處理流程會變成:

hugo

  1. host1 ping 172.17.0.2 (host2)
  2. host1 routing table 沒有 172.17.0.0/16 的資訊,走 default gatway
  3. default gatway 被設定為 172.22.0.3 因此 dst_mac 會變成 router1 eth0 的 mac address(如果 host1 的 arp table 沒有紀錄 172.22.0.3 mac address,則 host1 會發送 ARP request 去詢問,而因為 172.22.0.3 在同一個 bridge 上,因此可以得到 ARP response)
  4. 封包傳送至 h1r1br Bridge,而由於 dst_mac 不是 h1r1br Bridge 的 mac address,且 dst_mac src_mac interface 皆在同一個 bridge 上,因此符合 docker 網路規範,封包不會被 drop 掉。

The importance of bridge’s decision

這裡有一個非常重要的概念,就是封包的 dst_mac mac address 會影響 Linux Bridge 處理封包的流程。根據 ebtables/iptables interaction on a Linux-based bridge 中有提到:

The bridge’s decision for a frame can be one of these:

  • bridge it, if the destination MAC address is on another side of the bridge;
  • flood it over all the forwarding bridge ports, if the position of the box with the destination MAC is unknown to the bridge;
  • pass it to the higher protocol code (the IP code), if the destination MAC address is that of the bridge or of one of its ports;
  • ignore it, if the destination MAC address is located on the same side of the bridge.

如果我們沒有設定 host1 的 default gateway 是 router1 的 eth0,那麼 default gateway 的 mac address 就會變成 h1r1br Bridge 的 mac address,並且根據上面 bridge’s decision 所描述,會將封包 pass 到 IP Network Layer, 接著根據 routing table 來決定 output interface,此時 dst_mac mac address 就會變成 docker0,最後就會在 filter.FORWARD 階段被 Drop 掉。

etable/iptable Logs

我們從 log 來確認剛剛的敘述是否正確。

  • Default Gateway is router1 eth0
1Jul  6 06:56:45 sdn-experi-project kernel: [313896.817044] [raw.PREROUTING]IN=br-0e510c1eeb44 OUT= PHYSIN=veth8b7aaa8 MAC=02:42:ac:16:00:03:02:42:ac:16:00:02:08:00 SRC=172.22.0.2 DST=172.21.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=64082 DF PROTO=ICMP TYPE=8 CODE=0 ID=1164 SEQ=1
2Jul  6 06:56:45 sdn-experi-project kernel: [313896.817061] TRACE: eb:filter:FORWARD IN=veth8b7aaa8 OUT=veth6d5ff81 MAC source = 02:42:ac:16:00:02 MAC dest = 02:42:ac:16:00:03 proto = 0x0800 IP SRC=172.22.0.2 IP DST=172.21.0.2, IP tos=0x00, IP proto=1
3Jul  6 06:56:45 sdn-experi-project kernel: [313896.817070] [filter.FORWARD]IN=br-0e510c1eeb44 OUT=br-0e510c1eeb44 PHYSIN=veth8b7aaa8 PHYSOUT=veth6d5ff81 MAC=02:42:ac:16:00:03:02:42:ac:16:00:02:08:00 SRC=172.22.0.2 DST=172.21.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=64082 DF PROTO=ICMP TYPE=8 CODE=0 ID=1164 SEQ=1
4Jul  6 06:56:45 sdn-experi-project kernel: [313896.817130] [raw.PREROUTING]IN=br-5004af5170da OUT= PHYSIN=veth83ddfd8 MAC=02:42:ac:15:00:02:02:42:ac:15:00:03:08:00 SRC=172.22.0.2 DST=172.21.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=63 ID=64082 DF PROTO=ICMP TYPE=8 CODE=0 ID=1164 SEQ=1
5Jul  6 06:56:45 sdn-experi-project kernel: [313896.817134] TRACE: eb:filter:FORWARD IN=veth83ddfd8 OUT=veth56a6af1 MAC source = 02:42:ac:15:00:03 MAC dest = 02:42:ac:15:00:02 proto = 0x0800 IP SRC=172.22.0.2 IP DST=172.21.0.2, IP tos=0x00, IP proto=1
6Jul  6 06:56:45 sdn-experi-project kernel: [313896.817141] [filter.FORWARD]IN=br-5004af5170da OUT=br-5004af5170da PHYSIN=veth83ddfd8 PHYSOUT=veth56a6af1 MAC=02:42:ac:15:00:02:02:42:ac:15:00:03:08:00 SRC=172.22.0.2 DST=172.21.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=63 ID=64082 DF PROTO=ICMP TYPE=8 CODE=0 ID=1164 SEQ=1
7Jul  6 06:56:45 sdn-experi-project kernel: [313896.817209] TRACE: eb:filter:FORWARD IN=veth56a6af1 OUT=veth83ddfd8 MAC source = 02:42:ac:15:00:02 MAC dest = 02:42:ac:15:00:03 proto = 0x0800 IP SRC=172.21.0.2 IP DST=172.22.0.2, IP tos=0x00, IP proto=1
8Jul  6 06:56:45 sdn-experi-project kernel: [313896.817220] TRACE: eb:filter:FORWARD IN=veth6d5ff81 OUT=veth8b7aaa8 MAC source = 02:42:ac:16:00:03 MAC dest = 02:42:ac:16:00:02 proto = 0x0800 IP SRC=172.21.0.2 IP DST=172.22.0.2, IP tos=0x00, IP proto=1
  • Default Gateway is h1r1br Bridge
1Jul  6 07:06:23 sdn-experi-project kernel: [314474.517447] [raw.PREROUTING]IN=br-0e510c1eeb44 OUT= PHYSIN=veth8b7aaa8 MAC=02:42:9a:48:a4:74:02:42:ac:16:00:02:08:00 SRC=172.22.0.2 DST=172.21.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=52580 DF PROTO=ICMP TYPE=8 CODE=0 ID=1180 SEQ=1
2Jul  6 07:06:23 sdn-experi-project kernel: [314474.517594] TRACE: eb:filter:INPUT IN=veth8b7aaa8 OUT= MAC source = 02:42:ac:16:00:02 MAC dest = 02:42:9a:48:a4:74 proto = 0x0800 IP SRC=172.22.0.2 IP DST=172.21.0.2, IP tos=0x00, IP proto=1
3Jul  6 07:06:23 sdn-experi-project kernel: [314474.517622] [filter.FORWARD]IN=br-0e510c1eeb44 OUT=br-5004af5170da PHYSIN=veth8b7aaa8 MAC=02:42:9a:48:a4:74:02:42:ac:16:00:02:08:00 SRC=172.22.0.2 DST=172.21.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=63 ID=52580 DF PROTO=ICMP TYPE=8 CODE=0 ID=1180 SEQ=1

比對 netfilter package flow:

可以看到最重要的差異在於 default gateway 是 router1 eth0 的時候,會走 bridge-level filter.FORWARD,反之如果是 h1r1br Bridge, 則會走 bridge-level filter.INPUT 並經過 routing decision,而造成此差異的原因就在於 bridge’s decision

結論

我們使用 router 的方式來實現 cross-network container communication,由於 Docker container network 結合 linux bridge 來實作網路模型,因此上述透過實驗來驗證封包在 bridge 的流向和處理方式,了解這樣的流程,對於未來在 debug 相關議題上都非常有幫助。