<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Tony Bai &#187; iptables</title>
	<atom:link href="http://tonybai.com/tag/iptables/feed/" rel="self" type="application/rss+xml" />
	<link>https://tonybai.com</link>
	<description>一个程序员的心路历程</description>
	<lastBuildDate>Sun, 12 Apr 2026 22:30:28 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.2.1</generator>
		<item>
		<title>Hello，WireGuard</title>
		<link>https://tonybai.com/2020/03/29/hello-wireguard/</link>
		<comments>https://tonybai.com/2020/03/29/hello-wireguard/#comments</comments>
		<pubDate>Sun, 29 Mar 2020 08:29:01 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Apt-get]]></category>
		<category><![CDATA[BLAKE2]]></category>
		<category><![CDATA[brew]]></category>
		<category><![CDATA[ChaCha20]]></category>
		<category><![CDATA[cni]]></category>
		<category><![CDATA[curve25519]]></category>
		<category><![CDATA[DKMS]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[ethr]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[ipsec]]></category>
		<category><![CDATA[iptables]]></category>
		<category><![CDATA[ip_forward]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[l2tp]]></category>
		<category><![CDATA[Linus]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[macos]]></category>
		<category><![CDATA[netfilter]]></category>
		<category><![CDATA[netns]]></category>
		<category><![CDATA[noise-framework]]></category>
		<category><![CDATA[openvpn]]></category>
		<category><![CDATA[overlay]]></category>
		<category><![CDATA[overlaynetwork]]></category>
		<category><![CDATA[Poly1305]]></category>
		<category><![CDATA[pptp]]></category>
		<category><![CDATA[SipHash24]]></category>
		<category><![CDATA[SSH]]></category>
		<category><![CDATA[sysctl]]></category>
		<category><![CDATA[tailscale]]></category>
		<category><![CDATA[TCP]]></category>
		<category><![CDATA[Tcpdump]]></category>
		<category><![CDATA[tunnel]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[UDP]]></category>
		<category><![CDATA[virtual-private-network]]></category>
		<category><![CDATA[vpn]]></category>
		<category><![CDATA[vps]]></category>
		<category><![CDATA[wg]]></category>
		<category><![CDATA[wg-quick]]></category>
		<category><![CDATA[wg0]]></category>
		<category><![CDATA[WireGuard]]></category>
		<category><![CDATA[wireguard-go]]></category>
		<category><![CDATA[wormhole]]></category>
		<category><![CDATA[公钥]]></category>
		<category><![CDATA[内核]]></category>
		<category><![CDATA[动态内核模块技术]]></category>
		<category><![CDATA[容器]]></category>
		<category><![CDATA[密码学]]></category>
		<category><![CDATA[带宽]]></category>
		<category><![CDATA[控制平面]]></category>
		<category><![CDATA[数据平面]]></category>
		<category><![CDATA[私钥]]></category>
		<category><![CDATA[隧道]]></category>

		<guid isPermaLink="false">https://tonybai.com/?p=2879</guid>
		<description><![CDATA[2020年1月28日，Linux之父Linus Torvalds正式将WireGuard merge到Linux 5.6版本内核主线： 图：WireGuard被加入linux kernel 5.6主线的commit log 这意味着在Linux 5.6内核发布时，linux在内核层面将原生支持一个新的VPN协议栈：WireGuard。 图：WireGuard Logo 一. VPN与WireGuard的创新 VPN，全称Virtual Private Network（虚拟专用网络）。提起VPN，大陆的朋友想到的第一件事就是fan qiang。其实fan qiang只是VPN的一个“小众”应用罢了^_^，企业网络才是VPN真正施展才能的地方。VPN支持在不安全的公网上建立一条加密的、安全的到企业内部网络的通道（隧道tunnel），这就好比专门架设了一个专用网络那样。在WireGuard出现之前，VPN的隧道协议主要有PPTP、L2TP和IPSec等，其中PPTP和L2TP协议工作在OSI模型的第二层，又称为二层隧道协议；IPSec是第三层隧道协议。 既然已经有了这么多的VPN协议，那么Why WireGuard？ WireGuard的作者Jason A. Donenfeld在WireGuard官网给出了很明确地理由： 简单、易用、无连接、无状态：号称目前最易用和最简单的VPN解决方案 WireGuard可以像SSH一样易于配置和部署。只需交换非常简单的公钥就可以建立VPN连接，就像交换SSH密钥一样，其余所有由WireGuard透明处理。并且WireGuard建立的VPN连接是基于UDP的，无需建立和管理连接，无需关心和管理状态的。 先进加密协议 WireGuard充分利用安全领域和密码学在这些年的最新成果，使用noise framework，Curve25519，ChaCha20，Poly1305，BLAKE2，SipHash24等构建WireGuard的安全方案。 最小的攻击面(最少代码实现) WireGuard的内核模块c代码仅不足5k行，便于代码安全评审。也使得WireGuard的实现更不容易被攻击（代码量少，理论上漏洞相对于庞大的代码集合而言也会少许多）。 高性能 密码学最新成果带来的高速机密原语和WireGuard的内核驻留机制，使其相较于之前的VPN方案更具性能优势。 以上这些理由，同时也是WireGuard这个协议栈的特性。 这么说依然很抽象，我们来实操一下，体验一下WireGuard的简洁、易用、安全、高效。 二. WireGuard安装和使用 WireGuard将在linux 5.6内核中提供原生支持，也就是说在那之前，我们还无法直接使用WireGuard，安装还是不可避免的。在我的实验环境中有两台Linux VPS主机，都是ubuntu 18.04，内核都是4.15.0。因此我们需要首先添加WireGuard的ppa仓库： sudo add-apt-repository ppa:wireguard/wireguard 更新源后，即可通过下面命令安装WireGuard： sudo apt-get update sudo apt-get install wireguard 安装的WireGuard分为两部分： WireGuard内核模块(wireguard.ko)，这部分通过动态内核模块技术DKMS安装到ubuntu的内核模块文件目录下： $ [...]]]></description>
			<content:encoded><![CDATA[<p>2020年1月28日，Linux之父<a href="https://github.com/torvalds">Linus Torvalds</a>正式将<a href="https://www.wireguard.com/">WireGuard</a> merge<a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=bd2463ac7d7ec51d432f23bf0e893fb371a908cd">到Linux 5.6版本内核主线</a>：</p>
<p><img src="https://tonybai.com/wp-content/uploads/hello-wireguard/hello-wireguard-add-to-linux-kernel-5.6-next.png" alt="img{512x368}" /></p>
<p><center>图：WireGuard被加入linux kernel 5.6主线的commit log</center></p>
<p>这意味着在Linux 5.6内核发布时，linux在内核层面将<strong>原生</strong>支持一个新的VPN协议栈：<a href="https://git.zx2c4.com/wireguard">WireGuard</a>。</p>
<p><img src="https://tonybai.com/wp-content/uploads/hello-wireguard/hello-wireguard-wireguard-logo.png" alt="img{512x368}" /></p>
<p><center>图：WireGuard Logo</center></p>
<h2>一. VPN与WireGuard的创新</h2>
<p>VPN，全称Virtual Private Network（虚拟专用网络）。提起VPN，大陆的朋友想到的第一件事就是fan qiang。其实fan qiang只是VPN的一个“小众”应用罢了^_^，企业网络才是VPN真正施展才能的地方。VPN支持在不安全的公网上建立一条加密的、安全的到企业内部网络的通道（隧道tunnel），这就好比专门架设了一个专用网络那样。在WireGuard出现之前，VPN的隧道协议主要有<a href="https://tools.ietf.org/html/rfc2637">PPTP</a>、<a href="https://tools.ietf.org/html/rfc2661">L2TP</a>和<a href="https://tools.ietf.org/html/rfc4301">IPSec</a>等，其中PPTP和L2TP协议工作在OSI模型的第二层，又称为二层隧道协议；IPSec是第三层隧道协议。</p>
<p>既然已经有了这么多的VPN协议，那么<strong>Why WireGuard？</strong></p>
<p>WireGuard的作者<a href="https://github.com/zx2c4">Jason A. Donenfeld</a>在<a href="https://www.wireguard.com/">WireGuard官网</a>给出了很明确地理由：</p>
<ul>
<li>简单、易用、无连接、无状态：号称目前最易用和最简单的VPN解决方案</li>
</ul>
<p>WireGuard可以像SSH一样易于配置和部署。只需交换非常简单的公钥就可以建立VPN连接，就像交换SSH密钥一样，其余所有由WireGuard透明处理。并且WireGuard建立的VPN连接是基于UDP的，无需建立和管理连接，无需关心和管理状态的。</p>
<ul>
<li>先进加密协议</li>
</ul>
<p>WireGuard充分利用安全领域和密码学在这些年的最新成果，使用<a href="http://www.noiseprotocol.org/">noise framework</a>，<a href="http://cr.yp.to/ecdh.html">Curve25519</a>，<a href="http://cr.yp.to/chacha.html">ChaCha20</a>，<a href="http://cr.yp.to/mac.html">Poly1305</a>，<a href="https://blake2.net/">BLAKE2</a>，<a href="https://131002.net/siphash/">SipHash24</a>等构建WireGuard的安全方案。</p>
<ul>
<li>最小的攻击面(最少代码实现)</li>
</ul>
<p>WireGuard的内核模块c代码仅不足5k行，便于代码安全评审。也使得WireGuard的实现更不容易被攻击（代码量少，理论上漏洞相对于庞大的代码集合而言也会少许多）。</p>
<ul>
<li>高性能</li>
</ul>
<p>密码学最新成果带来的高速机密原语和WireGuard的内核驻留机制，使其相较于之前的VPN方案更具性能优势。</p>
<p>以上这些理由，同时也是WireGuard这个协议栈的特性。</p>
<p>这么说依然很抽象，我们来实操一下，体验一下WireGuard的简洁、易用、安全、高效。</p>
<h2>二. WireGuard安装和使用</h2>
<p>WireGuard将在linux 5.6内核中提供原生支持，也就是说在那之前，我们还无法直接使用WireGuard，安装还是不可避免的。在我的实验环境中有两台Linux VPS主机，都是<a href="https://tonybai.com/tag/ubuntu">ubuntu 18.04</a>，内核都是4.15.0。因此我们需要首先添加WireGuard的ppa仓库：</p>
<pre><code>sudo add-apt-repository ppa:wireguard/wireguard

</code></pre>
<p>更新源后，即可通过下面命令安装WireGuard：</p>
<pre><code>sudo apt-get update

sudo apt-get install wireguard

</code></pre>
<p>安装的WireGuard分为两部分：</p>
<ul>
<li>WireGuard内核模块(wireguard.ko)，这部分通过动态内核模块技术<a href="https://baike.baidu.com/item/DKMS/9743354">DKMS</a>安装到ubuntu的内核模块文件目录下：</li>
</ul>
<pre><code>$ ls /lib/modules/4.15.0-29-generic/updates/dkms/
wireguard.ko

</code></pre>
<ul>
<li>用户层的命令行工具</li>
</ul>
<p>类似于内核netfilter和命令行工具iptables之间关系，wireguard.ko对应的用户层命令行工具wireguard-tools：<code>wg、wg-quick</code>被安装到/usr/bin下面了：</p>
<pre><code>$ ls -t /usr/bin|grep wg|head -n 2
wg
wg-quick

</code></pre>
<h3>1. peer to peer vpn</h3>
<p>在两个linux Vps上都安装完WireGuard后，我们就可以在两个节点(peer)建立虚拟专用网络(VPN)了。我们分为称两个linux节点为peer1和peer2：</p>
<p><img src="https://tonybai.com/wp-content/uploads/hello-wireguard/hello-wireguard-peer-to-peer-1.png" alt="img{512x368}" /></p>
<p><center>图：点对点wireguard通信图</center></p>
<p>就像上图那样，我们只分别需要在peer1和peer2建立<code>/etc/wireguard/wg0.conf</code>。</p>
<p>peer1的<code>/etc/wireguard/wg0.conf</code>：</p>
<pre><code>[Interface]
PrivateKey = {peer1's privatekey}
Address = 10.0.0.1
ListenPort = 51820

[Peer]
PublicKey = {peer2's publickey}
EndPoint = {peer2's ip}:51820
AllowedIPs = 10.0.0.2/32

</code></pre>
<p>peer2的<code>/etc/wireguard/wg0.conf</code>：</p>
<pre><code>[Interface]
PrivateKey = {peer2's privatekey}
Address = 10.0.0.2
ListenPort = 51820

[Peer]
PublicKey = {peer1's publickey}
EndPoint = {peer1's ip}:51820
AllowedIPs = 10.0.0.1/32

</code></pre>
<p>我们看到每个peer上WireGuard所需的配置文件wg0.conf包含两大部分：</p>
<ul>
<li>
<p><code>[Interface]部分</code></p>
<ul>
<li>
<p>PrivateKey &#8211;  peer自身的privatekey</p>
</li>
<li>
<p>Address &#8211; peer的wg0接口在vpn网络中绑定的路由ip范围，在上述例子中仅绑定了一个ip地址</p>
</li>
<li>
<p>ListenPort &#8211; wg网络协议栈监听UDP端口</p>
</li>
</ul>
</li>
<li>
<p><code>[Peer]部分</code>（描述vpn网中其他peer信息，一个wg0配置文件中显然可以配置多个Peer部分）</p>
<ul>
<li>
<p>PublicKey &#8211; 该peer的publickey</p>
</li>
<li>
<p>EndPoint &#8211; 该peer的wg网路协议栈地址(ip+port)</p>
</li>
<li>
<p>AllowedIPs &#8211; 允许该peer发送过来的wireguard载荷中的源地址范围。同时本机而言，这个字段也会作为本机路由表中wg0绑定的ip范围。</p>
</li>
</ul>
</li>
</ul>
<p>每个Peer自身的privatekey和publickey可以通过WireGuard提供的命令行工具生成：</p>
<pre><code>$ wg genkey | tee privatekey | wg pubkey &gt; publickey
$ ls
privatekey  publickey
</code></pre>
<blockquote>
<p>注：这两个文件可以生成在任意路径下，我们要的是两个文件中内容。</p>
</blockquote>
<p>在两个peer上配置完<code>/etc/wireguard/wg0.conf</code>配置文件后，我们就可以使用下面命令<strong>在peer1和peer2之间建立一条双向加密VPN隧道</strong>了：</p>
<pre><code>peer1:

$ sudo wg-quick up wg0
[#] ip link add wg0 type wireguard
[#] wg setconf wg0 /dev/fd/63
[#] ip -4 address add 10.0.0.1 dev wg0
[#] ip link set mtu 1420 up dev wg0
[#] ip -4 route add 10.0.0.2/32 dev wg0

peer2:

$ sudo wg-quick up wg0
[#] ip link add wg0 type wireguard
[#] wg setconf wg0 /dev/fd/63
[#] ip -4 address add 10.0.0.2 dev wg0
[#] ip link set mtu 1420 up dev wg0
[#] ip -4 route add 10.0.0.1/32 dev wg0

</code></pre>
<p>执行上述命令，每个peer会增加一个network interface dev: <strong>wg0</strong>，并在系统路由表中增加一条路由，以peer1为例：</p>
<pre><code>$ ip a

... ...

4: wg0: &lt;POINTOPOINT,NOARP,UP,LOWER_UP&gt; mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
    link/none
    inet 10.0.0.1/32 scope global wg0
       valid_lft forever preferred_lft forever

$ ip route
default via 172.21.0.1 dev eth0 proto dhcp metric 100
10.0.0.2 dev wg0 scope link
... ...

</code></pre>
<p>现在我们来测试两个Peer之间的连通性。<strong>WireGuard的peer之间是对等的</strong>，谁发起的请求谁就是client端。我们在peer1上ping peer2，在peer2上我们用tcpdump抓wg0设备的包：</p>
<pre><code>Peer1:

$ ping -c 3 10.0.0.2
PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data.
64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=34.9 ms
64 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=34.7 ms
64 bytes from 10.0.0.2: icmp_seq=3 ttl=64 time=34.6 ms

--- 10.0.0.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2002ms
rtt min/avg/max/mdev = 34.621/34.781/34.982/0.262 ms

Peer2:

# tcpdump -i wg0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on wg0, link-type RAW (Raw IP), capture size 262144 bytes
13:29:52.659550 IP 10.0.0.1 &gt; instance-cspzrq3u: ICMP echo request, id 20580, seq 1, length 64
13:29:52.659603 IP instance-cspzrq3u &gt; 10.0.0.1: ICMP echo reply, id 20580, seq 1, length 64
13:29:53.660463 IP 10.0.0.1 &gt; instance-cspzrq3u: ICMP echo request, id 20580, seq 2, length 64
13:29:53.660495 IP instance-cspzrq3u &gt; 10.0.0.1: ICMP echo reply, id 20580, seq 2, length 64
13:29:54.662201 IP 10.0.0.1 &gt; instance-cspzrq3u: ICMP echo request, id 20580, seq 3, length 64
13:29:54.662234 IP instance-cspzrq3u &gt; 10.0.0.1: ICMP echo reply, id 20580, seq 3, length 64

</code></pre>
<p>我们看到peer1和peer2经由WireGuard建立的vpn实现了连通：在peer2上ping peer1(10.0.0.1)亦得到相同结果。</p>
<p>这时如果我们如果在peer2(vpn ip: 10.0.0.2)上启动一个http server(监听0.0.0.0:9090):</p>
<pre><code>//httpserver.go
package main

import "net/http"

func index(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("hello, wireguard\n"))
}

func main() {
    http.Handle("/", http.HandlerFunc(index))
    http.ListenAndServe(":9090", nil)
}
</code></pre>
<p>那么我们在peer1(vpn ip:10.0.0.1)去访问这个server：</p>
<pre><code>$ curl http://10.0.0.2:9090
hello, wireguard

</code></pre>
<p>在peer2(instance-cspzrq3u)上的tcpdump显示(tcp握手+数据通信+tcp拆除)：</p>
<pre><code>14:15:05.233794 IP 10.0.0.1.43922 &gt; instance-cspzrq3u.9090: Flags [S], seq 1116349511, win 27600, options [mss 1380,sackOK,TS val 3539789774 ecr 0,nop,wscale 7], length 0
14:15:05.233854 IP instance-cspzrq3u.9090 &gt; 10.0.0.1.43922: Flags [S.], seq 3504538202, ack 1116349512, win 27360, options [mss 1380,sackOK,TS val 2842719516 ecr 3539789774,nop,wscale 7], length 0
14:15:05.268792 IP 10.0.0.1.43922 &gt; instance-cspzrq3u.9090: Flags [.], ack 1, win 216, options [nop,nop,TS val 3539789809 ecr 2842719516], length 0
14:15:05.268882 IP 10.0.0.1.43922 &gt; instance-cspzrq3u.9090: Flags [P.], seq 1:78, ack 1, win 216, options [nop,nop,TS val 3539789809 ecr 2842719516], length 77
14:15:05.268907 IP instance-cspzrq3u.9090 &gt; 10.0.0.1.43922: Flags [.], ack 78, win 214, options [nop,nop,TS val 2842719551 ecr 3539789809], length 0
14:15:05.269514 IP instance-cspzrq3u.9090 &gt; 10.0.0.1.43922: Flags [P.], seq 1:134, ack 78, win 214, options [nop,nop,TS val 2842719552 ecr 3539789809], length 133
14:15:05.304147 IP 10.0.0.1.43922 &gt; instance-cspzrq3u.9090: Flags [.], ack 134, win 224, options [nop,nop,TS val 3539789845 ecr 2842719552], length 0
14:15:05.304194 IP 10.0.0.1.43922 &gt; instance-cspzrq3u.9090: Flags [F.], seq 78, ack 134, win 224, options [nop,nop,TS val 3539789845 ecr 2842719552], length 0
14:15:05.304317 IP instance-cspzrq3u.9090 &gt; 10.0.0.1.43922: Flags [F.], seq 134, ack 79, win 214, options [nop,nop,TS val 2842719586 ecr 3539789845], length 0
14:15:05.339035 IP 10.0.0.1.43922 &gt; instance-cspzrq3u.9090: Flags [.], ack 135, win 224, options [nop,nop,TS val 3539789880 ecr 2842719586], length 0

</code></pre>
<p>如果要拆除这个vpn，只需在每个peer上分别执行如下命令：</p>
<pre><code>$ sudo wg-quick down wg0
[#] ip link delete dev wg0

</code></pre>
<h3>2. peer to the local network of other peer</h3>
<p>上面两个peer虽然实现了点对点的连通，但是如果我们想从peer1访问peer2所在的局域网中的另外一台机器（这显然是vpn最常用的应用场景），如下面示意图：</p>
<p><img src="https://tonybai.com/wp-content/uploads/hello-wireguard/hello-wireguard-peer-to-the-network-of-other-peer-1.png" alt="img{512x368}" /></p>
<p><center>图：从一个peer到另外一个peer所在局域网的节点的通信图</center></p>
<p>基于目前的配置是否能实现呢？我们来试试。首先我们在peer1上要将<code>192.168.1.0/24</code>网段的路由指到wg0上，这样我们在peer1上ping或curl 192.168.1.123:9090，数据才能被交给wg0处理并通过vpn网络送出，修改peer1上的wg0.conf：</p>
<pre><code>// peer1's /etc/wireguard/wg0.conf

... ...
[Peer]
PublicKey = {peer2's publickey}
EndPoint = peer2's ip:51820
AllowedIPs = 10.0.0.2/32,192.168.1.0/24

</code></pre>
<p>重启peer1上的wg0使上述配置生效。然后我们尝试在peer1上ping 192.168.1.123：</p>
<pre><code>$ ping -c 3 192.168.1.123
PING 192.168.1.123 (192.168.1.123) 56(84) bytes of data.

--- 192.168.1.123 ping statistics ---
3 packets transmitted, 0 received, 100% packet loss, time 2038ms

</code></pre>
<p>我们在peer2上的tcpdump显示：</p>
<pre><code># tcpdump -i wg0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on wg0, link-type RAW (Raw IP), capture size 262144 bytes
14:33:38.393520 IP 10.0.0.1 &gt; 192.168.1.123: ICMP echo request, id 30426, seq 1, length 64
14:33:39.408083 IP 10.0.0.1 &gt; 192.168.1.123: ICMP echo request, id 30426, seq 2, length 64
14:33:40.432079 IP 10.0.0.1 &gt; 192.168.1.123: ICMP echo request, id 30426, seq 3, length 64

</code></pre>
<p>我们看到peer2收到来自10.0.0.1的到192.168.1.123的ping包都<strong>没有对应的回包</strong>，通信失败。Why？我们分析一下。</p>
<p>peer2在51820端口收到WireGuard包后，去除wireguard包的包裹，露出真实数据包。真实数据包的目的ip地址为192.168.1.123，该地址并非peer2自身地址(其自身局域网地址为192.168.1.10)。既然不是自身地址，就不能送到上层协议栈(tcp)处理，那么另外一条路是forward(转发)出去。但是是否允许转发么？显然从结果来看，从wg0收到的消息无权转发，于是消息丢弃，这就是没有回包和通信失败的原因。</p>
<p>为了支持转发（这是vpn常用场景的功能哦），我们需要为peer2的wg0.conf增加些转发配置：</p>
<pre><code>// peer2's  wg0.conf

[Interface]

... ...
PostUp   = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUT  ING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUT  ING -o eth0 -j MASQUERADE

... ...

</code></pre>
<p>重启peer2的wg0。在peer2的内核层我们也要开启转发开关：</p>
<pre><code>// /etc/sysctl.conf

net.ipv4.ip_forward=1

net.ipv6.conf.all.forwarding=1

</code></pre>
<p>执行下面命令临时生效：</p>
<pre><code># sysctl -p
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1

</code></pre>
<p>接下来，我们再来测试一下连通性。我们在peer1上再次尝试<code>ping 192.168.1.123</code>：</p>
<pre><code>$ ping -c 3 192.168.1.123
PING 192.168.1.123 (192.168.1.123) 56(84) bytes of data.
64 bytes from 192.168.1.123: icmp_seq=1 ttl=46 time=200 ms
64 bytes from 192.168.1.123: icmp_seq=2 ttl=46 time=200 ms
64 bytes from 192.168.1.123: icmp_seq=3 ttl=46 time=200 ms

--- 192.168.1.123 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2002ms
rtt min/avg/max/mdev = 200.095/200.239/200.396/0.531 ms

</code></pre>
<p>这回通了！peer2上的Tcpdump输出中也看到了回包：</p>
<pre><code>14:49:58.808467 IP 10.0.0.1 &gt; 192.168.1.123: ICMP echo request, id 402, seq 1, length 64
14:49:58.974035 IP 192.168.1.123 &gt; 10.0.0.1: ICMP echo reply, id 402, seq 1, length 64
14:49:59.809747 IP 10.0.0.1 &gt; 192.168.1.123: ICMP echo request, id 402, seq 2, length 64
14:49:59.975240 IP 192.168.1.123 &gt; 10.0.0.1: ICMP echo reply, id 402, seq 2, length 64
14:50:00.810802 IP 10.0.0.1 &gt; 192.168.1.123: ICMP echo request, id 402, seq 3, length 64
14:50:00.976202 IP 192.168.1.123 &gt; 10.0.0.1: ICMP echo reply, id 402, seq 3, length 64
</code></pre>
<p>我们在192.168.1.123上运行上面的那个httpserver程序，再在peer1上用curl访问这个程序：</p>
<pre><code>$ curl 192.168.1.123:9090
hello, wireguard
</code></pre>
<p>我们看到httpserver的应答成功返回。peer2上的tcpdump也抓到了整个通信过程：</p>
<pre><code>14:50:36.437259 IP 10.0.0.1.47918 &gt; 192.168.1.123.9090: Flags [S], seq 3235649864, win 27600, options [mss 1380,sackOK,TS val 101915019 ecr 0,nop,wscale 7], length 0
14:50:36.593554 IP 192.168.1.123.9090 &gt; 10.0.0.1.47918: Flags [S.], seq 2420552016, ack 3235649865, win 28960, options [mss 1460,sackOK,TS val 2323314775 ecr 101915019,nop,wscale 7], length 0
14:50:36.628315 IP 10.0.0.1.47918 &gt; 192.168.1.123.9090: Flags [.], ack 1, win 216, options [nop,nop,TS val 101915210 ecr 2323314775], length 0
14:50:36.628379 IP 10.0.0.1.47918 &gt; 192.168.1.123.9090: Flags [P.], seq 1:84, ack 1, win 216, options [nop,nop,TS val 101915210 ecr 2323314775], length 83
14:50:36.784550 IP 192.168.1.123.9090 &gt; 10.0.0.1.47918: Flags [.], ack 84, win 227, options [nop,nop,TS val 2323314822 ecr 101915210], length 0
14:50:36.784710 IP 192.168.1.123.9090 &gt; 10.0.0.1.47918: Flags [P.], seq 1:134, ack 84, win 227, options [nop,nop,TS val 2323314822 ecr 101915210], length 133
14:50:36.820339 IP 10.0.0.1.47918 &gt; 192.168.1.123.9090: Flags [.], ack 134, win 224, options [nop,nop,TS val 101915401 ecr 2323314822], length 0
14:50:36.820383 IP 10.0.0.1.47918 &gt; 192.168.1.123.9090: Flags [F.], seq 84, ack 134, win 224, options [nop,nop,TS val 101915401 ecr 2323314822], length 0
14:50:36.977226 IP 192.168.1.123.9090 &gt; 10.0.0.1.47918: Flags [F.], seq 134, ack 85, win 227, options [nop,nop,TS val 2323314870 ecr 101915401], length 0
14:50:37.011927 IP 10.0.0.1.47918 &gt; 192.168.1.123.9090: Flags [.], ack 135, win 224, options [nop,nop,TS val 101915594 ecr 2323314870], length 0

</code></pre>
<h3>3. WireGuard的用户层实现</h3>
<p>在linux上，我们务必使用WireGuard的内核模式，这显然是最高效的。在macOS、Windows上，WireGuard无法以内核模块驻留模式运行，但WireGuard项目提供了WireGuard的用户层实现。其作者<a href="https://github.com/zx2c4">Jason A. Donenfeld</a>亲自实现了<a href="https://tonybai.com/tag/go">Go语言</a>版本的<a href="https://git.zx2c4.com/wireguard-go">wireguard-go</a>。macOS上使用的就是wireguard的Go实现。我们可以使用brew在macOS上按照WireGuard：</p>
<pre><code>$brew install wireguard-tools

</code></pre>
<p>配置好<code>/etc/wireguard/wg0.conf</code>后(和linux上的配置方式一致)，同样可以通过wg-quick命令启动wireguard：</p>
<pre><code>$sudo wg-quick up wg0

</code></pre>
<p>wg-quick实际上会通过<code>wireguard-go</code>来实现linux wireguard在内核中完成的功能：</p>
<pre><code>$ps -ef|grep wireguard

    0 57783     1   0  3:18下午 ttys002    0:00.01 wireguard-go utun

</code></pre>
<h2>三. WireGuard性能如何</h2>
<p>关于WireGuard性能如何，官方给出了一个性能基准测试的对比数据（相较于其他vpn网络栈）：</p>
<p><img src="https://tonybai.com/wp-content/uploads/hello-wireguard/hello-wireguard-performance-1.png" alt="img{512x368}" /></p>
<p><center>图：WireGuard性能与其他vpn网络栈的对比（来自官方截图）</center></p>
<p>我们看到和IPSec、OpenVPN相比，无论从吞吐还是延迟，WireGuard都领先不少。</p>
<p>我们这里用<a href="https://github.com/Microsoft/ethr">microsoft开源的带宽测试工具ethr</a>来直观看一下走物理网络和走WireGuard VPN的带宽差别。</p>
<p>在peer2上运行：</p>
<pre><code>$ ethr -s

</code></pre>
<p>然后在peer1上分别通过物理网络和VPN网络向peer2发起请求：</p>
<ul>
<li>peer1 -> peer2 (物理网络)</li>
</ul>
<pre><code>$ ethr -c  peer2's ip
Connecting to host [peer2 ip], port 9999
[  6] local 172.21.0.5 port 46108 connected to  peer2 ip port 9999
- - - - - - - - - - - - - - - - - - - - - - -
[ ID]   Protocol    Interval      Bits/s
[  6]     TCP      000-001 sec     1.54M
[  6]     TCP      001-002 sec     1.54M
[  6]     TCP      002-003 sec     1.54M
[  6]     TCP      003-004 sec     1.54M
[  6]     TCP      004-005 sec     1.54M

.... ...

</code></pre>
<ul>
<li>peer1 -> peer2 (vpn网络)</li>
</ul>
<pre><code>$ ethr -c 10.0.0.2
Connecting to host [10.0.0.2], port 9999
[  6] local 10.0.0.1 port 36010 connected to 10.0.0.2 port 9999
- - - - - - - - - - - - - - - - - - - - - - -
[ ID]   Protocol    Interval      Bits/s
[  6]     TCP      000-001 sec     1.79M
[  6]     TCP      001-002 sec      640K
[  6]     TCP      002-003 sec     1.15M
[  6]     TCP      003-004 sec      512K
[  6]     TCP      004-005 sec     1.02M
[  6]     TCP      005-006 sec     1.02M
[  6]     TCP      006-007 sec     1.02M

</code></pre>
<p>我们看到走vpn的带宽相当于走物理网络的<strong>66%</strong>(1.02/1.54)左右。这里peer1(腾讯云)、peer2(百度云)之间走的是互联网，而在局域网测试的效果可能更好（留给大家^_^）。</p>
<h2>四. 小结</h2>
<p>经过上面的实验，我们看到了WireGuard的配置的确十分简单，这也是我目前使用过的配置过程最为简单的vpn。随着linux kernel 5.6内置对WireGuard的原生支持，WireGuard在vpn领域势必会有更为广泛的应用。</p>
<p>在容器网络方面，目前WireGuard已经给出了<a href="https://www.wireguard.com/netns/">跨容器的网络通信方案</a>，基于wireguard的k8s cni网络插件<a href="https://github.com/gravitational/wormhole">wormhole</a>可以让pod之间通过wireguard实现的overlay网络通信。</p>
<p>国外的tailscale公司正在实现<a href="https://tailscale.com/blog/how-tailscale-works/">一种基于Wireguard的mesh vpn网络</a>，该网络以WireGuard为数据平面的承载体，该公司主要实现控制平面。该公司目前聚集了一些Go核心开发人员，这里就包括著名的go核心开发团队成员、net/http包的最初作者和当前维护者的Brad Fitzpatrick。</p>
<h2>五. 参考资料</h2>
<ul>
<li>
<p><a href="https://zhuanlan.zhihu.com/p/91383212">WireGuard，简约之美</a> &#8211; https://zhuanlan.zhihu.com/p/91383212 原理说明，墙裂推荐！</p>
</li>
<li>
<p><a href="https://baike.baidu.com/item/虚拟专用网络/8747869">虚拟专用网络</a> &#8211; https://baike.baidu.com/item/虚拟专用网络/8747869</p>
</li>
<li>
<p><a href="https://www.wireguard.com/">WireGuard官网资料</a> &#8211; https://www.wireguard.com/</p>
</li>
<li>
<p><a href="https://github.com/pirate/wireguard-docs">非官方WireGuard文档</a> &#8211; https://github.com/pirate/wireguard-docs</p>
</li>
<li>
<p><a href="https://www.stavros.io/posts/how-to-configure-wireguard/">How to easily configure WireGuard</a> &#8211; https://www.stavros.io/posts/how-to-configure-wireguard/</p>
</li>
<li>
<p><a href="https://www.ericlight.com/wireguard-part-one-installation.html">WireGuard series</a> &#8211; https://www.ericlight.com/wireguard-part-one-installation.html</p>
</li>
<li>
<p><a href="https://www.lixh.cn/archives/2165.html">MacOS下WireGuard客户端的安装和配置</a></p>
</li>
</ul>
<hr />
<p>我的网课“<a href="https://coding.imooc.com/class/284.html">Kubernetes实战：高可用集群搭建、配置、运维与应用</a>”在慕课网上线了，感谢小伙伴们学习支持！</p>
<p><a href="https://tonybai.com/">我爱发短信</a>：企业级短信平台定制开发专家 https://tonybai.com/<br />
smspush : 可部署在企业内部的定制化短信平台，三网覆盖，不惧大并发接入，可定制扩展； 短信内容你来定，不再受约束, 接口丰富，支持长短信，签名可选。</p>
<p>著名云主机服务厂商DigitalOcean发布最新的主机计划，入门级Droplet配置升级为：1 core CPU、1G内存、25G高速SSD，价格5$/月。有使用DigitalOcean需求的朋友，可以打开这个<a href="https://m.do.co/c/bff6eed92687">链接地址</a>：https://m.do.co/c/bff6eed92687 开启你的DO主机之路。</p>
<p>Gopher Daily(Gopher每日新闻)归档仓库 &#8211; https://github.com/bigwhite/gopherdaily</p>
<p>我的联系方式：</p>
<p>微博：https://weibo.com/bigwhite20xx<br />
微信公众号：iamtonybai<br />
博客：tonybai.com<br />
github: https://github.com/bigwhite</p>
<p>微信赞赏：<br />
<img src="https://tonybai.com/wp-content/uploads/wechat-zanshang-code-512x512.jpg" alt="img{512x368}" /></p>
<p>商务合作方式：撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。</p>
<p style='text-align:left'>&copy; 2020, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2020/03/29/hello-wireguard/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>再谈Docker容器单机网络：利用iptables trace和ebtables log</title>
		<link>https://tonybai.com/2017/11/06/explain-docker-single-host-network-using-iptables-trace-and-ebtables-log/</link>
		<comments>https://tonybai.com/2017/11/06/explain-docker-single-host-network-using-iptables-trace-and-ebtables-log/#comments</comments>
		<pubDate>Sun, 05 Nov 2017 16:09:11 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[bridge]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[Debug]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[ebtables]]></category>
		<category><![CDATA[filter]]></category>
		<category><![CDATA[FORWARD]]></category>
		<category><![CDATA[iptables]]></category>
		<category><![CDATA[ipvs]]></category>
		<category><![CDATA[kube-proxy]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[loadbalance]]></category>
		<category><![CDATA[MAC]]></category>
		<category><![CDATA[mangle]]></category>
		<category><![CDATA[MASQUERADE]]></category>
		<category><![CDATA[NAT]]></category>
		<category><![CDATA[network]]></category>
		<category><![CDATA[ping]]></category>
		<category><![CDATA[POSTROUTING]]></category>
		<category><![CDATA[PREROUTING]]></category>
		<category><![CDATA[raw]]></category>
		<category><![CDATA[Trace]]></category>
		<category><![CDATA[veth]]></category>
		<category><![CDATA[交换机]]></category>
		<category><![CDATA[容器]]></category>
		<category><![CDATA[数据链路层]]></category>
		<category><![CDATA[桥设备]]></category>
		<category><![CDATA[网络层]]></category>
		<category><![CDATA[负载均衡]]></category>
		<category><![CDATA[路由]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=2446</guid>
		<description><![CDATA[这大半年一直在搞Kubernetes。每次搭建Kubernetes集群，或多或少都会被Kubernetes的“网络插件们”折腾折腾。因此，要说目前Kubernetes中最难搞的是什么？个人觉得莫过于其Pod网络了，至少也是最难搞的之一。除此之外，以Service和Pod为中心的Kubernetes架构还大量利用iptables规则来实现Service的反向代理和负载均衡，这又与Docker原生容器单机网络实现所基于的linux bridge和iptables规则糅合在一起，让troubleshooting时的难度又增加了一些。 去年曾经花过一段研究Docker网络，但现在看来当时在某些关键环节的理解上还有些模糊，于是花了周末的闲暇时间对Docker容器单机网络做了一次再理解。这次重新认识利用上了iptables的Trace功能以及数据链路层的ebtables，让我可以更清晰地看到单机容器网络的网络数据流流向。同时，有了容器网络理解这个基础，对后续解决K8s Pod网络问题也是大有裨益的。 本文从某个角度来说也可以理解为自我答疑，我不会从最最基础的Docker网络结构说起，对Docker容器单机网络结构不了解的童鞋，可以先看看我之前写的《理解Docker单机容器网络》和《理解Docker容器网络之Linux Network Namespace》两篇文章。 一、实验环境 1、主机环境和工具版本 Docker的默认单机容器网络从最初的版本开始就几乎没有变过，因此理论上下面的分析适用于Docker的大部分版本。我的实验环境如下： Ubuntu 16.04.3 LTS (GNU/Linux 4.4.0-63-generic x86_64) # docker version Client: Version: 17.09.0-ce API version: 1.32 Go version: go1.8.3 Git commit: afdb6d4 Built: Tue Sep 26 22:42:18 2017 OS/Arch: linux/amd64 Server: Version: 17.09.0-ce API version: 1.32 (minimum version 1.12) Go version: go1.8.3 Git commit: afdb6d4 Built: [...]]]></description>
			<content:encoded><![CDATA[<p>这大半年一直在搞<a href="http://tonybai.com/tag/kubernetes">Kubernetes</a>。每次<a href="http://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm/">搭建Kubernetes集群</a>，或多或少都会被Kubernetes的“<a href="https://kubernetes.io/docs/concepts/cluster-administration/network-plugins/">网络插件们</a>”折腾折腾。因此，要说目前Kubernetes中最难搞的是什么？个人觉得莫过于其<a href="http://tonybai.com/2017/01/17/understanding-flannel-network-for-kubernetes/">Pod网络</a>了，至少也是最难搞的之一。除此之外，以<a href="http://tonybai.com/2017/02/09/rolling-update-for-services-in-kubernetes-cluster/">Service</a>和Pod为中心的Kubernetes架构还大量利用<a href="http://www.iptables.info/en/print/structure-of-iptables.html">iptables规则</a>来实现Service的反向代理和负载均衡，这又与<a href="http://tonybai.com/2016/01/15/understanding-container-networking-on-single-host/">Docker原生容器单机网络</a>实现所基于的<a href="https://wiki.linuxfoundation.org/networking/bridge">linux bridge</a>和<a href="http://tonybai.com/tag/iptables">iptables规则</a>糅合在一起，让troubleshooting时的难度又增加了一些。</p>
<p>去年曾经花过一段研究<a href="http://tonybai.com/2016/01/15/understanding-container-networking-on-single-host/">Docker网络</a>，但现在看来当时在某些关键环节的理解上还有些模糊，于是花了周末的闲暇时间对Docker容器单机网络做了一次再理解。这次重新认识利用上了iptables的Trace功能以及数据链路层的ebtables，让我可以更清晰地看到单机容器网络的网络数据流流向。同时，有了容器网络理解这个基础，对后续解决K8s Pod网络问题也是大有裨益的。</p>
<p>本文从某个角度来说也可以理解为自我答疑，我不会从最最基础的<a href="http://tonybai.com/tag/docker">Docker</a>网络结构说起，对Docker容器单机网络结构不了解的童鞋，可以先看看我之前写的《<a href="http://tonybai.com/2016/01/15/understanding-container-networking-on-single-host/">理解Docker单机容器网络</a>》和《<a href="http://tonybai.com/2017/01/11/understanding-linux-network-namespace-for-docker-network/">理解Docker容器网络之Linux Network Namespace</a>》两篇文章。</p>
<h2>一、实验环境</h2>
<h3>1、主机环境和工具版本</h3>
<p>Docker的默认单机容器网络从最初的版本开始就几乎没有变过，因此理论上下面的分析适用于Docker的大部分版本。我的实验环境如下：</p>
<pre><code>Ubuntu 16.04.3 LTS (GNU/Linux 4.4.0-63-generic x86_64)

# docker version
Client:
 Version:      17.09.0-ce
 API version:  1.32
 Go version:   go1.8.3
 Git commit:   afdb6d4
 Built:        Tue Sep 26 22:42:18 2017
 OS/Arch:      linux/amd64

Server:
 Version:      17.09.0-ce
 API version:  1.32 (minimum version 1.12)
 Go version:   go1.8.3
 Git commit:   afdb6d4
 Built:        Tue Sep 26 22:40:56 2017
 OS/Arch:      linux/amd64
 Experimental: false

# iptables --version
iptables v1.6.0
# ebtables --version
ebtables v2.0.10-4 (December 2011)

</code></pre>
<h3>2、容器网络及拓扑</h3>
<p>我们需要制作一个用于实验的容器镜像。因为这里仅用ping包进行测试，这里我们仅基于ubuntu:14.04 base image制作一个简单的安装有必要网络工具的image：</p>
<pre><code>//Dockerfile

From ubuntu:14.04
RUN apt-get update &amp;&amp; apt-get install -y curl iptables
ENTRYPOINT ["tail", "-f", "/var/log/bootstrap.log"]

// 制作镜像：

# docker build -t foo:latest ./
</code></pre>
<p>启动两个容器：</p>
<pre><code># docker run --name c1 -d --cap-add=NET_ADMIN foo:latest
7a01a19d9328b39f094c9a9c76340d179baaf93afb52189816bcc79f8319cb64
# docker run --name c2 -d --cap-add=NET_ADMIN foo:latest
94a2f1841f6d95fd0682299b17c0aedb60c1047786c8e75b0f1ab7316a995409
</code></pre>
<p>容器启动后的网络信息汇总如下：</p>
<pre><code># ifconfig -a
docker0   Link encap:Ethernet  HWaddr 02:42:ff:27:17:4d
          inet addr:192.168.0.1  Bcast:0.0.0.0  Mask:255.255.240.0
          ... ...

eth0      Link encap:Ethernet  HWaddr 00:16:3e:06:3a:3a
          inet addr:10.171.77.0  Bcast:10.171.79.255  Mask:255.255.248.0
          ... ...

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          ... ...

veth0594f4b Link encap:Ethernet  HWaddr 96:5b:d4:80:73:5f
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          ... ...

veth57a3dec Link encap:Ethernet  HWaddr 02:52:e9:60:ea:b1
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          ... ...
</code></pre>
<p>为了方便大家理解，这里附上一幅简易的容器网络拓扑：</p>
<p><img src="http://tonybai.com/wp-content/uploads/docker-bridge-network-demo.png" alt="img{512x368}" /></p>
<h2>二、调试工具配置</h2>
<p>Docker单机容器网络默认使用的是桥接网络，所有启动的容器均桥接在Docker引擎创建的docker0 linux bridge上，因此内核对Linux bridge的处理逻辑是理解Docker容器网络的关键。</p>
<p>与硬件网桥/交换机不同的是，Linux Bridge还具备三层网络，即IP层的功能，也就是docker0既是一个网桥也是一个具备三层转发功能的网卡设备。传统意义上，按照iso网络七层规范，iptables工作在三层，而网桥是一个二层(数据链路层)设备，但Linux协议栈针对网桥设备的实现却在网络层的规则链(ebtables)中串接了iptables的规则链处理，即在二层也可以处理ip包，这是为了实现桥接透明防火墙的需要。但实现也会保证每个packet数据包仅会走一次iptable的某个chain，要么在linker layer走，要么在network layer走，不会出现在linker layer走一次，又在network layer重复走一次的情况。关于这种基于linux bridge的ebtables和iptables的交互规则，在netfilter官网的一篇名为《<a href="http://ebtables.netfilter.org/br_fw_ia/br_fw_ia.html">ebtables/iptables interaction on a Linux-based bridge</a>》文档中有详细说明，这篇文章也是后续分析的一个重要参考。下面这幅图也是文章中提到的那幅netfilter数据流全图，后续在分析时会反复回到这幅图（后续简称为：<strong>全图</strong>）：</p>
<p><img src="http://tonybai.com/wp-content/uploads/nf-packet-flow.png" alt="img{512x368}" /><br />
建议：右键在新标签中打开图片看大图</p>
<p>关于数据包在iptables的各条chain的流经图可以参见下面：</p>
<p><img src="http://tonybai.com/wp-content/uploads/iptables-traverse.jpg" alt="img{512x368}" /></p>
<h3>1、iptables TRACE target的设置</h3>
<p>在本次实验中，我们主要需要查看数据包的流转路径，因此我们需要针对iptables的data flow进行跟踪。之前，我曾使用过iptables提供的LOG target或mark set&amp;match方式来跟踪iptables中的数据流，但这两种方式都不理想，需要针对特定流程插入LOG target或match在入口包设定好的mark，对iptables规则的侵入较大，调试和观察也较为复杂；iptables自身提供了TRACE功能，一旦设定，当数据包匹配到任意chain上任意table的处理规则时，iptables会在系统日志(/var/log/syslog)中自动输出此时的数据包状态日志。</p>
<p>我们来为iptables规则添加TRACE，TRACE target只能在iptables的raw表中添加，raw表中有两条iptables built-in chain: PREROUTING和OUTPUT，分别代表网卡数据入口和本地进程下推数据的出口。TRACE target就添加在这两条chain上，步骤如下：</p>
<pre><code># iptables -t raw -A OUTPUT -p icmp -j TRACE
# iptables -t raw -A PREROUTING -p icmp -j TRACE
</code></pre>
<p>注意：我们采用icmp协议(ping协议)进行测试，因此我们只TRACE icmp协议的请求和应答包。</p>
<h3>2、ebtables的调试设置</h3>
<p>我们的重点在iptables，为ebtables只是辅助，帮助我们看清数据包到底是在哪一层被hook进iptables的规则链中进行处理的。因此我们在<strong>全图</strong>中的每个ebtables的built-in chain上都加上LOG（ebtables目前还不支持TRACE）：</p>
<pre><code># ebtables -t broute -A BROUTING -p ipv4 --ip-proto 1 --log-level 6 --log-ip --log-prefix "TRACE: eb:broute:BROUTING" -j ACCEPT
# ebtables -t nat -A OUTPUT -p ipv4 --ip-proto 1 --log-level 6 --log-ip --log-prefix "TRACE: eb:nat:OUTPUT"  -j ACCEPT
# ebtables -t nat -A PREROUTING -p ipv4 --ip-proto 1 --log-level 6 --log-ip --log-prefix "TRACE: eb:nat:PREROUTING" -j ACCEPT
# ebtables -t filter -A INPUT -p ipv4 --ip-proto 1 --log-level 6 --log-ip --log-prefix "TRACE: eb:filter:INPUT" -j ACCEPT
# ebtables -t filter -A FORWARD -p ipv4 --ip-proto 1 --log-level 6 --log-ip --log-prefix "TRACE: eb:filter:FORWARD" -j ACCEPT
# ebtables -t filter -A OUTPUT -p ipv4 --ip-proto 1 --log-level 6 --log-ip --log-prefix "TRACE: eb:filter:OUTPUT" -j ACCEPT
# ebtables -t nat -A POSTROUTING -p ipv4 --ip-proto 1 --log-level 6 --log-ip --log-prefix "TRACE: eb:nat:POSTROUTING" -j ACCEPT

注意：这里--ip-proto 1 表示仅match icmp packet。
</code></pre>
<h3>3、iptables和ebtables规则全文</h3>
<p>启动两个容器并添加上述规则后，当前的的iptables规则如下：(通过iptables-save输出的按table组织的rules)</p>
<pre><code># iptables-save
# Generated by iptables-save v1.6.0 on Sun Nov  5 14:50:46 2017
*raw

: PREROUTING ACCEPT [1564539:108837380]
:OUTPUT ACCEPT [1504962:130805835]
-A PREROUTING -p icmp -j TRACE
-A OUTPUT -p icmp -j TRACE
COMMIT
# Completed on Sun Nov  5 14:50:46 2017
# Generated by iptables-save v1.6.0 on Sun Nov  5 14:50:46 2017
*filter
:INPUT ACCEPT [1564535:108837044]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [1504968:130806627]

: DOCKER - [0:0]

: DOCKER-ISOLATION - [0:0]

: DOCKER-USER - [0:0]

-A FORWARD -j DOCKER-USER
-A FORWARD -j DOCKER-ISOLATION
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
-A FORWARD -i docker0 -o docker0 -j ACCEPT
-A DOCKER-ISOLATION -j RETURN
-A DOCKER-USER -j RETURN
COMMIT
# Completed on Sun Nov  5 14:50:46 2017
# Generated by iptables-save v1.6.0 on Sun Nov  5 14:50:46 2017
*nat

: PREROUTING ACCEPT [280:14819]
:INPUT ACCEPT [278:14651]
:OUTPUT ACCEPT [639340:38370263]

: POSTROUTING ACCEPT [639342:38370431]

: DOCKER - [0:0]

-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 192.168.0.0/20 ! -o docker0 -j MASQUERADE
-A DOCKER -i docker0 -j RETURN
COMMIT
# Completed on Sun Nov  5 14:50:46 2017
</code></pre>
<p>而ebtables的规则如下：</p>
<pre><code># ebtables-save
# Generated by ebtables-save v1.0 on Sun Nov  5 16:51:50 CST 2017
*nat
: PREROUTING ACCEPT
:OUTPUT ACCEPT
: POSTROUTING ACCEPT
-A PREROUTING -p IPv4 --ip-proto icmp --log-level info --log-prefix "TRACE: eb:nat:PREROUTING" --log-ip -j ACCEPT
-A OUTPUT -p IPv4 --ip-proto icmp --log-level info --log-prefix "TRACE: eb:nat:OUTPUT" --log-ip -j ACCEPT
-A POSTROUTING -p IPv4 --ip-proto icmp --log-level info --log-prefix "TRACE: eb:nat:POSTROUTING" --log-ip -j ACCEPT

*broute
:BROUTING ACCEPT
-A BROUTING -p IPv4 --ip-proto icmp --log-level info --log-prefix "TRACE: eb:broute:BROUTING" --log-ip -j ACCEPT

*filter
:INPUT ACCEPT
:FORWARD ACCEPT
:OUTPUT ACCEPT
-A INPUT -p IPv4 --ip-proto icmp --log-level info --log-prefix "TRACE: eb:filter:INPUT" --log-ip -j ACCEPT
-A FORWARD -p IPv4 --ip-proto icmp --log-level info --log-prefix "TRACE: eb:filter:FORWARD" --log-ip -j ACCEPT
-A OUTPUT -p IPv4 --ip-proto icmp --log-level info --log-prefix "TRACE: eb:filter:OUTPUT" --log-ip -j ACCEPT

</code></pre>
<p>对于iptables，我们还可以通过iptables命令输出另外一种组织形式的规则列表，我们这里列出filter和nat这两个重要的table的规则(输出规则number，便于后续match分析时查看)：</p>
<pre><code># iptables -nL --line-numbers -v -t filter
Chain INPUT (policy ACCEPT 2558K packets, 178M 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       10   840 DOCKER-USER  all  --  *      *       0.0.0.0/0            0.0.0.0/0
2       10   840 DOCKER-ISOLATION  all  --  *      *       0.0.0.0/0            0.0.0.0/0
3        7   588 ACCEPT     all  --  *      docker0  0.0.0.0/0            0.0.0.0/0            ctstate RELATED,ESTABLISHED
4        3   252 DOCKER     all  --  *      docker0  0.0.0.0/0            0.0.0.0/0
5        0     0 ACCEPT     all  --  docker0 !docker0  0.0.0.0/0            0.0.0.0/0
6        3   252 ACCEPT     all  --  docker0 docker0  0.0.0.0/0            0.0.0.0/0

Chain OUTPUT (policy ACCEPT 2460K packets, 214M 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 (1 references)
num   pkts bytes target     prot opt in     out     source               destination
1       10   840 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       10   840 RETURN     all  --  *      *       0.0.0.0/0            0.0.0.0/0

# iptables -nL --line-numbers -v -t nat
Chain PREROUTING (policy ACCEPT 884 packets, 46522 bytes)
num   pkts bytes target     prot opt in     out     source               destination
1      881 46270 DOCKER     all  --  *      *       0.0.0.0/0            0.0.0.0/0            ADDRTYPE match dst-type LOCAL

Chain INPUT (policy ACCEPT 881 packets, 46270 bytes)
num   pkts bytes target     prot opt in     out     source               destination

Chain OUTPUT (policy ACCEPT 1048K packets, 63M bytes)
num   pkts bytes target     prot opt in     out     source               destination
1        0     0 DOCKER     all  --  *      *       0.0.0.0/0           !127.0.0.0/8          ADDRTYPE match dst-type LOCAL

Chain POSTROUTING (policy ACCEPT 1048K packets, 63M bytes)
num   pkts bytes target     prot opt in     out     source               destination
1        0     0 MASQUERADE  all  --  *      !docker0  192.168.0.0/20       0.0.0.0/0

Chain DOCKER (2 references)
num   pkts bytes target     prot opt in     out     source               destination
1        0     0 RETURN     all  --  docker0 *       0.0.0.0/0            0.0.0.0/0

</code></pre>
<h2>三、Container to Container</h2>
<p>下面，我们分三种情况来看看容器网络的数据包是如何流动的，首先是Container to Container。</p>
<p><img src="http://tonybai.com/wp-content/uploads/docker-bridge-network-demo-container-to-container.png" alt="img{512x368}" /></p>
<p>我们在容器C1中执行ping 3次 C2的命令：</p>
<pre><code># docker exec c1 ping -c 3 192.168.0.3
PING 192.168.0.3 (192.168.0.3) 56(84) bytes of data.
64 bytes from 192.168.0.3: icmp_seq=1 ttl=64 time=0.226 ms
64 bytes from 192.168.0.3: icmp_seq=2 ttl=64 time=0.159 ms
64 bytes from 192.168.0.3: icmp_seq=3 ttl=64 time=0.185 ms

--- 192.168.0.3 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 0.159/0.190/0.226/0.027 ms
</code></pre>
<p>在容器c1(192.168.0.2)中，icmp request由ping程序(c1 namespace中的local process)发出。c1 network namespace中的路由表如下：</p>
<pre><code># docker exec c1 netstat -rn
Kernel IP routing table
Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface
0.0.0.0         192.168.0.1     0.0.0.0         UG        0 0          0 eth0
192.168.0.0     0.0.0.0         255.255.240.0   U         0 0          0 eth0
</code></pre>
<p>由于目标容器地址为192.168.0.3，在容器c1的直连网络上，走第二条直连路由（非默认路由），数据包通过eth0发出。</p>
<p>由于c1 namespace中的eth0通过veth机制连接在host namespace的docker0 bridge的一个Slave port上，因此上述数据包通过docker0 bridge的slave port: veth0594f4b流入docker0 bridge。</p>
<p>这里再强调一下linux bridge设备。Linux下的Bridge是一种虚拟设备，它依赖于一个或多个<strong>从设备</strong>。它不是内核虚拟出的和<strong>从设备</strong>同一层次的镜像设备，而是内核虚拟出的一个高一层次的设备，并把<strong>从设备</strong>虚拟化为端口port，同时处理各个<strong>从设备</strong>的数据收发及转发。bridge设备是建立在从设备之上的（这些从设备可以是实际设备，也可以是vlan设备等），并且我们可以为bridge准备一个IP（bridge设备的MAC地址是它所有从设备中最小的MAC地址），这样该主机就可以通过这个bridge设备与网络中的其它主机通信了。另外一旦某个网络设备被“插到”linux bridge上，这个网络设备将会变为bridge的<strong>从设备</strong>，被虚拟化为端口port，<strong>从设备</strong>的IP及MAC都不再可用，好似被bridge剥夺了被内核网络栈处理的资格；它们被设置为接收任何包，对其流入的数据包的处理交由bridge完成，并最终由bridge设备来决定数据包的去向：接收到本机、转发或丢弃。</p>
<p>因此，位于host namespace的docker0 bridge从slave port: veth0594f4b收到icmp request后，我们不会看到veth0594f4b这一netdev被内核网络栈程序单独处理(比如：单独走一遍ebtables和iptables chains)，而是进入bridge处理逻辑（此时可以回顾一下上面的全图）。由于数据包已经进入到了<strong>host namespace</strong>，因此我们可以通过ebtables和iptables输出的Trace和log来跟踪数据包流转的路径了：</p>
<h3>1、start -> bridgecheck -> linker layer</h3>
<pre><code>TRACE: eb:broute:BROUTING IN=veth0594f4b OUT= MAC source = 02:42:c0:a8:00:02 MAC dest = 02:42:c0:a8:00:03 proto = 0x0800 IP SRC=192.168.0.2 IP DST=192.168.0.3, IP tos=0x00, IP proto=1
TRACE: eb:nat:PREROUTING IN=veth0594f4b OUT= MAC source = 02:42:c0:a8:00:02 MAC dest = 02:42:c0:a8:00:03 proto = 0x0800 IP SRC=192.168.0.2 IP DST=192.168.0.3, IP tos=0x00, IP proto=1
</code></pre>
<p>从最初的trace log来看，在bridge check之后(发现it is a linux bridge)，数据包进入到linker layer中；并且在linker layer的BROUTING built-in chain之后，数据包没有被转移到上面的network layer，而是<strong>继续linker layer的行程</strong>：进入linker layer的nat:PREROUTING中。</p>
<h3>2、call iptables chain rules in linker layer</h3>
<p>结合<strong>全图</strong>中的图示和日志输出，在linker layer的nat:PREROUTING之后，linker layer调用了上层iptables的处理规则：raw:PREROUTING和nat:PREROUTING：</p>
<pre><code>TRACE: raw:PREROUTING:policy:2 IN=docker0 OUT= PHYSIN=veth0594f4b MAC=02:42:c0:a8:00:03:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=47066 DF PROTO=ICMP TYPE=8 CODE=0 ID=90 SEQ=1
TRACE: nat:PREROUTING:policy:2 IN=docker0 OUT= PHYSIN=veth0594f4b MAC=02:42:c0:a8:00:03:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=47066 DF PROTO=ICMP TYPE=8 CODE=0 ID=90 SEQ=1
</code></pre>
<p>Trace target在数据包match table、chains的policy或rules时会输出日志，日志格式：”TRACE:tablename:chainname:type:rulenum”。当匹配到的是普通rules时，type=”rule”;当碰到一个user-defined chain的return target时，type=”return”；当匹配到built-in chain(比如：PREROUTING、INPUT、OUTPUT、FORWARD和POSTROUTING)的default policy时，type=”policy”。</p>
<p>从上面的日志输出来看，似乎PREROUTING chain的raw table中的Trace target不能被trace自身match，因此trace log输出的是匹配raw table built-in chain: PREROUTING的default policy: ACCEPT，num=2(policy和rules整体排序后的序号)；在PREROUTING chain的nat表中匹配时，Trace也仅匹配到了default policy，rule 1（target: Docker）没有匹配上；</p>
<p>这里有一点奇怪的是mangle table没有任何输出，即便是default policy的也没有，原因暂不明。</p>
<h3>3、bridge decision</h3>
<p>根据<strong>全图</strong>和后续的日志，我们得到了<strong>bridge decision</strong>的结果：继续在linker layer上处理数据包，一路向右。不过在处理的路径上依旧调用了iptables的rules：</p>
<pre><code>TRACE: eb:filter:FORWARD IN=veth0594f4b OUT=veth57a3dec MAC source = 02:42:c0:a8:00:02 MAC dest = 02:42:c0:a8:00:03 proto = 0x0800 IP SRC=192.168.0.2 IP DST=192.168.0.3, IP tos=0x00, IP proto=1
TRACE: filter:FORWARD:rule:1 IN=docker0 OUT=docker0 PHYSIN=veth0594f4b PHYSOUT=veth57a3dec MAC=02:42:c0:a8:00:03:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=47066 DF PROTO=ICMP TYPE=8 CODE=0 ID=90 SEQ=1
TRACE: filter:DOCKER-USER:return:1 IN=docker0 OUT=docker0 PHYSIN=veth0594f4b PHYSOUT=veth57a3dec MAC=02:42:c0:a8:00:03:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=47066 DF PROTO=ICMP TYPE=8 CODE=0 ID=90 SEQ=1
TRACE: filter:FORWARD:rule:2 IN=docker0 OUT=docker0 PHYSIN=veth0594f4b PHYSOUT=veth57a3dec MAC=02:42:c0:a8:00:03:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=47066 DF PROTO=ICMP TYPE=8 CODE=0 ID=90 SEQ=1
TRACE: filter:DOCKER-ISOLATION:return:1 IN=docker0 OUT=docker0 PHYSIN=veth0594f4b PHYSOUT=veth57a3dec MAC=02:42:c0:a8:00:03:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=47066 DF PROTO=ICMP TYPE=8 CODE=0 ID=90 SEQ=1
TRACE: filter:FORWARD:rule:4 IN=docker0 OUT=docker0 PHYSIN=veth0594f4b PHYSOUT=veth57a3dec MAC=02:42:c0:a8:00:03:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=47066 DF PROTO=ICMP TYPE=8 CODE=0 ID=90 SEQ=1
TRACE: filter:DOCKER:return:1 IN=docker0 OUT=docker0 PHYSIN=veth0594f4b PHYSOUT=veth57a3dec MAC=02:42:c0:a8:00:03:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=47066 DF PROTO=ICMP TYPE=8 CODE=0 ID=90 SEQ=1
TRACE: filter:FORWARD:rule:6 IN=docker0 OUT=docker0 PHYSIN=veth0594f4b PHYSOUT=veth57a3dec MAC=02:42:c0:a8:00:03:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=47066 DF PROTO=ICMP TYPE=8 CODE=0 ID=90 SEQ=1

</code></pre>
<p><strong>bridge decision</strong>决定的依据或则规则是什么呢？《<a href="http://ebtables.netfilter.org/br_fw_ia/br_fw_ia.html">ebtables/iptables interaction on a Linux-based bridge</a>》一文给了我们一些答案：</p>
<pre><code>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.
</code></pre>
<p>不过即便按照这几条规则，我依然有一定困惑，那就是真实的处理是：依旧在linker layer，但掺杂了上层网络层的处理规则。</p>
<p>另外，你可能会发现iptables log里MAC值的格式很怪异(比如：<strong>MAC=02:42:c0:a8:00:03:02:42:c0:a8:00:02:08:00</strong>)，非常long。其实这个MAC值是一个组合：Souce MAC, Destination MAC和 frame type的组合。</p>
<pre><code>02:42:c0:a8:00:03: Destination MAC=00:60:dd:45:67:ea
02:42:c0:a8:00:02: Source MAC=00:60:dd:45:4c:92
08:00 : Type=08:00 (ethernet frame carried an IPv4 datagram)
</code></pre>
<h3>4、eb:nat:POSTROUTING -> nat:POSTROUTING -> egress(qdisc)</h3>
<p>最后packet进入linker layer的POSTROUTING built-in chain：</p>
<pre><code>TRACE: eb:nat:POSTROUTING IN= OUT=veth57a3dec MAC source = 02:42:c0:a8:00:02 MAC dest = 02:42:c0:a8:00:03 proto = 0x0800 IP SRC=192.168.0.2 IP DST=192.168.0.3, IP tos=0x00, IP proto=1
TRACE: nat:POSTROUTING:policy:2 IN= OUT=docker0 PHYSIN=veth0594f4b PHYSOUT=veth57a3dec SRC=192.168.0.2 DST=192.168.0.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=47066 DF PROTO=ICMP TYPE=8 CODE=0 ID=90 SEQ=1
</code></pre>
<p>iptables nat:POSTROUTING没有匹配上docker引擎增加的那条target为DOCKER的rule，于是输出了default policy的日志。</p>
<p>进入到egress(qdisc)后，相当于数据包到了bridge上的另一个slave port(veth57a3dec)上，此时数据包必须被送回网络上，于是进入到容器C2的eth0中。离开了host namespace，我们的日志便追踪不到了。</p>
<p>容器c2因为所在的network namespace是独立于host namespace的，因此有自己的iptables规则（如果未设置，均为默认accept），不受host namespace中的iptables的影响。</p>
<h3>5、”消失”的iptable的nat:PREROUTING和nat:POSTROUTING</h3>
<p>C2容器回复ping response的路径与request甚为相似，这里一次性将全部日志列出：</p>
<pre><code>TRACE: eb:broute:BROUTING IN=veth57a3dec OUT= MAC source = 02:42:c0:a8:00:03 MAC dest = 02:42:c0:a8:00:02 proto = 0x0800 IP SRC=192.168.0.3 IP DST=192.168.0.2, IP tos=0x00, IP proto=1
TRACE: eb:nat:PREROUTING IN=veth57a3dec OUT= MAC source = 02:42:c0:a8:00:03 MAC dest = 02:42:c0:a8:00:02 proto = 0x0800 IP SRC=192.168.0.3 IP DST=192.168.0.2, IP tos=0x00, IP proto=1
TRACE: raw:PREROUTING:policy:2 IN=docker0 OUT= PHYSIN=veth57a3dec MAC=02:42:c0:a8:00:02:02:42:c0:a8:00:03:08:00 SRC=192.168.0.3 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=5962 PROTO=ICMP TYPE=0 CODE=0 ID=90 SEQ=1

TRACE: eb:filter:FORWARD IN=veth57a3dec OUT=veth0594f4b MAC source = 02:42:c0:a8:00:03 MAC dest = 02:42:c0:a8:00:02 proto = 0x0800 IP SRC=192.168.0.3 IP DST=192.168.0.2, IP tos=0x00, IP proto=1
TRACE: filter:FORWARD:rule:1 IN=docker0 OUT=docker0 PHYSIN=veth57a3dec PHYSOUT=veth0594f4b MAC=02:42:c0:a8:00:02:02:42:c0:a8:00:03:08:00 SRC=192.168.0.3 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=5962 PROTO=ICMP TYPE=0 CODE=0 ID=90 SEQ=1
TRACE: filter:DOCKER-USER:return:1 IN=docker0 OUT=docker0 PHYSIN=veth57a3dec PHYSOUT=veth0594f4b MAC=02:42:c0:a8:00:02:02:42:c0:a8:00:03:08:00 SRC=192.168.0.3 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=5962 PROTO=ICMP TYPE=0 CODE=0 ID=90 SEQ=1
TRACE: filter:FORWARD:rule:2 IN=docker0 OUT=docker0 PHYSIN=veth57a3dec PHYSOUT=veth0594f4b MAC=02:42:c0:a8:00:02:02:42:c0:a8:00:03:08:00 SRC=192.168.0.3 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=5962 PROTO=ICMP TYPE=0 CODE=0 ID=90 SEQ=1
TRACE: filter:DOCKER-ISOLATION:return:1 IN=docker0 OUT=docker0 PHYSIN=veth57a3dec PHYSOUT=veth0594f4b MAC=02:42:c0:a8:00:02:02:42:c0:a8:00:03:08:00 SRC=192.168.0.3 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=5962 PROTO=ICMP TYPE=0 CODE=0 ID=90 SEQ=1
TRACE: filter:FORWARD:rule:3 IN=docker0 OUT=docker0 PHYSIN=veth57a3dec PHYSOUT=veth0594f4b MAC=02:42:c0:a8:00:02:02:42:c0:a8:00:03:08:00 SRC=192.168.0.3 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=5962 PROTO=ICMP TYPE=0 CODE=0 ID=90 SEQ=1

TRACE: eb:nat:POSTROUTING IN= OUT=veth0594f4b MAC source = 02:42:c0:a8:00:03 MAC dest = 02:42:c0:a8:00:02 proto = 0x0800 IP SRC=192.168.0.3 IP DST=192.168.0.2, IP tos=0x00, IP proto=1
</code></pre>
<p>仔细观察，我们发现虽然与request的路径类似，但依旧有不同：<strong>iptable的nat:PREROUTING和nat:POSTROUTING消失了</strong>。Why？iptables就是这么设计的。iptables会跟踪connection的state，当一个connection的首个包经过一次后，connection的state由NEW变成了ESTABLISHED；对于ESTABLISHED的connection的后续packets，内核会自动按照该connection的首个包在nat:PREROUTING和nat:POSTROUTING环节的处理方式进行处理，而不再流经这两个链中的nat表逻辑。而ebtables中似乎没有这个逻辑。</p>
<p>后续的ping的第二个、第三个流程也印证了上述设计，这里仅列出ping request packet 2：</p>
<pre><code>TRACE: eb:broute:BROUTING IN=veth0594f4b OUT= MAC source = 02:42:c0:a8:00:02 MAC dest = 02:42:c0:a8:00:03 proto = 0x0800 IP SRC=192.168.0.2 IP DST=192.168.0.3, IP tos=0x00, IP proto=1
TRACE: eb:nat:PREROUTING IN=veth0594f4b OUT= MAC source = 02:42:c0:a8:00:02 MAC dest = 02:42:c0:a8:00:03 proto = 0x0800 IP SRC=192.168.0.2 IP DST=192.168.0.3, IP tos=0x00, IP proto=1
TRACE: raw:PREROUTING:policy:2 IN=docker0 OUT= PHYSIN=veth0594f4b MAC=02:42:c0:a8:00:03:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=47310 DF PROTO=ICMP TYPE=8 CODE=0 ID=90 SEQ=2
TRACE: eb:filter:FORWARD IN=veth0594f4b OUT=veth57a3dec MAC source = 02:42:c0:a8:00:02 MAC dest = 02:42:c0:a8:00:03 proto = 0x0800 IP SRC=192.168.0.2 IP DST=192.168.0.3, IP tos=0x00, IP proto=1
TRACE: filter:FORWARD:rule:1 IN=docker0 OUT=docker0 PHYSIN=veth0594f4b PHYSOUT=veth57a3dec MAC=02:42:c0:a8:00:03:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=47310 DF PROTO=ICMP TYPE=8 CODE=0 ID=90 SEQ=2
TRACE: filter:DOCKER-USER:return:1 IN=docker0 OUT=docker0 PHYSIN=veth0594f4b PHYSOUT=veth57a3dec MAC=02:42:c0:a8:00:03:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=47310 DF PROTO=ICMP TYPE=8 CODE=0 ID=90 SEQ=2
TRACE: filter:FORWARD:rule:2 IN=docker0 OUT=docker0 PHYSIN=veth0594f4b PHYSOUT=veth57a3dec MAC=02:42:c0:a8:00:03:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=47310 DF PROTO=ICMP TYPE=8 CODE=0 ID=90 SEQ=2
TRACE: filter:DOCKER-ISOLATION:return:1 IN=docker0 OUT=docker0 PHYSIN=veth0594f4b PHYSOUT=veth57a3dec MAC=02:42:c0:a8:00:03:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=47310 DF PROTO=ICMP TYPE=8 CODE=0 ID=90 SEQ=2
TRACE: filter:FORWARD:rule:3 IN=docker0 OUT=docker0 PHYSIN=veth0594f4b PHYSOUT=veth57a3dec MAC=02:42:c0:a8:00:03:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.3 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=47310 DF PROTO=ICMP TYPE=8 CODE=0 ID=90 SEQ=2
TRACE: eb:nat:POSTROUTING IN= OUT=veth57a3dec MAC source = 02:42:c0:a8:00:02 MAC dest = 02:42:c0:a8:00:03 proto = 0x0800 IP SRC=192.168.0.2 IP DST=192.168.0.3, IP tos=0x00, IP proto=1

</code></pre>
<p>全部日志内容请参见：<a href="http://tonybai.com/wp-content/uploads/docker-bridge-network-demo-iptables-trace-log.txt">docker-bridge-network-demo-iptables-trace-log.txt文件</a>，这里不赘述。</p>
<h2>四、Local Process to Container</h2>
<p><img src="http://tonybai.com/wp-content/uploads/docker-bridge-network-demo-localprocess-to-container.png" alt="img{512x368}" /></p>
<p>很多”疑难”环节在上面的container to container数据流分析时已经做了解惑，因此后续local process to container和container to external流程将不会再细致描述，说明会略微泛泛一些，不那么细致。</p>
<p>我们在host上执行ping C1三次：</p>
<pre><code># ping -c 3 192.168.0.2
PING 192.168.0.2 (192.168.0.2) 56(84) bytes of data.
64 bytes from 192.168.0.2: icmp_seq=1 ttl=64 time=0.160 ms
64 bytes from 192.168.0.2: icmp_seq=2 ttl=64 time=0.105 ms
64 bytes from 192.168.0.2: icmp_seq=3 ttl=64 time=0.131 ms

--- 192.168.0.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2000ms
rtt min/avg/max/mdev = 0.105/0.132/0.160/0.022 ms
</code></pre>
<h3>1、local process -> routing decision -> iptables OUTPUT chain</h3>
<p>ping request数据包从本地的ping process发出，根据目的地址路由后，选择docker0作为OUT设备：</p>
<pre><code>TRACE: raw:OUTPUT:policy:2 IN= OUT=docker0 SRC=192.168.0.1 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=18692 DF PROTO=ICMP TYPE=8 CODE=0 ID=30245 SEQ=1 UID=0 GID=0
TRACE: mangle:OUTPUT:policy:1 IN= OUT=docker0 SRC=192.168.0.1 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=18692 DF PROTO=ICMP TYPE=8 CODE=0 ID=30245 SEQ=1 UID=0 GID=0
TRACE: nat:OUTPUT:policy:2 IN= OUT=docker0 SRC=192.168.0.1 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=18692 DF PROTO=ICMP TYPE=8 CODE=0 ID=30245 SEQ=1 UID=0 GID=0
TRACE: filter:OUTPUT:policy:1 IN= OUT=docker0 SRC=192.168.0.1 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=18692 DF PROTO=ICMP TYPE=8 CODE=0 ID=30245 SEQ=1 UID=0 GID=0

</code></pre>
<p>奇怪的是这次mangle chain居然有trace log输出:(。</p>
<h3>2、进入linker layer：iptables POSTROUTING -> ebtables OUTPUT -> ebtables POSTROUTING</h3>
<p>由于是OUT是bridge设备，因此要进入到ebtable中走一遭：</p>
<pre><code>TRACE: mangle:POSTROUTING:policy:1 IN= OUT=docker0 SRC=192.168.0.1 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=18692 DF PROTO=ICMP TYPE=8 CODE=0 ID=30245 SEQ=1 UID=0 GID=0
TRACE: nat:POSTROUTING:policy:2 IN= OUT=docker0 SRC=192.168.0.1 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=18692 DF PROTO=ICMP TYPE=8 CODE=0 ID=30245 SEQ=1 UID=0 GID=0
TRACE: eb:nat:OUTPUT IN= OUT=veth57a3dec MAC source = 02:42:ff:27:17:4d MAC dest = 02:42:c0:a8:00:02 proto = 0x0800 IP SRC=192.168.0.1 IP DST=192.168.0.2, IP tos=0x00, IP proto=1
TRACE: eb:filter:OUTPUT IN= OUT=veth57a3dec MAC source = 02:42:ff:27:17:4d MAC dest = 02:42:c0:a8:00:02 proto = 0x0800 IP SRC=192.168.0.1 IP DST=192.168.0.2, IP tos=0x00, IP proto=1
TRACE: eb:nat:POSTROUTING IN= OUT=veth57a3dec MAC source = 02:42:ff:27:17:4d MAC dest = 02:42:c0:a8:00:02 proto = 0x0800 IP SRC=192.168.0.1 IP DST=192.168.0.2, IP tos=0x00, IP proto=1
TRACE: eb:nat:OUTPUT IN= OUT=veth0594f4b MAC source = 02:42:ff:27:17:4d MAC dest = 02:42:c0:a8:00:02 proto = 0x0800 IP SRC=192.168.0.1 IP DST=192.168.0.2, IP tos=0x00, IP proto=1
TRACE: eb:filter:OUTPUT IN= OUT=veth0594f4b MAC source = 02:42:ff:27:17:4d MAC dest = 02:42:c0:a8:00:02 proto = 0x0800 IP SRC=192.168.0.1 IP DST=192.168.0.2, IP tos=0x00, IP proto=1
TRACE: eb:nat:POSTROUTING IN= OUT=veth0594f4b MAC source = 02:42:ff:27:17:4d MAC dest = 02:42:c0:a8:00:02 proto = 0x0800 IP SRC=192.168.0.1 IP DST=192.168.0.2, IP tos=0x00, IP proto=1

</code></pre>
<p>icmp的response和container to container类似，入口走的是linker layer(由于是桥设备)，在bridge decision后，走到INPUT chain：</p>
<pre><code>TRACE: eb:broute:BROUTING IN=veth0594f4b OUT= MAC source = 02:42:c0:a8:00:02 MAC dest = 02:42:ff:27:17:4d proto = 0x0800 IP SRC=192.168.0.2 IP DST=192.168.0.1, IP tos=0x00, IP proto=1
TRACE: eb:nat:PREROUTING IN=veth0594f4b OUT= MAC source = 02:42:c0:a8:00:02 MAC dest = 02:42:ff:27:17:4d proto = 0x0800 IP SRC=192.168.0.2 IP DST=192.168.0.1, IP tos=0x00, IP proto=1
TRACE: raw:PREROUTING:policy:2 IN=docker0 OUT= PHYSIN=veth0594f4b MAC=02:42:ff:27:17:4d:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.1 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=56535 PROTO=ICMP TYPE=0 CODE=0 ID=30245 SEQ=1
TRACE: mangle:PREROUTING:policy:1 IN=docker0 OUT= PHYSIN=veth0594f4b MAC=02:42:ff:27:17:4d:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.1 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=56535 PROTO=ICMP TYPE=0 CODE=0 ID=30245 SEQ=1
TRACE: eb:filter:INPUT IN=veth0594f4b OUT= MAC source = 02:42:c0:a8:00:02 MAC dest = 02:42:ff:27:17:4d proto = 0x0800 IP SRC=192.168.0.2 IP DST=192.168.0.1, IP tos=0x00, IP proto=1
TRACE: mangle:INPUT:policy:1 IN=docker0 OUT= PHYSIN=veth0594f4b MAC=02:42:ff:27:17:4d:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.1 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=56535 PROTO=ICMP TYPE=0 CODE=0 ID=30245 SEQ=1
TRACE: filter:INPUT:policy:1 IN=docker0 OUT= PHYSIN=veth0594f4b MAC=02:42:ff:27:17:4d:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=192.168.0.1 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=56535 PROTO=ICMP TYPE=0 CODE=0 ID=30245 SEQ=1

</code></pre>
<p>以上我们可以与到非桥设备的ping做比对，我们在host上ping 另外一个LAN中的host：</p>
<pre><code># ping -c 1 10.28.61.30
PING 10.28.61.30 (10.28.61.30) 56(84) bytes of data.
64 bytes from 10.28.61.30: icmp_seq=1 ttl=57 time=1.09 ms

--- 10.28.61.30 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 1.093/1.093/1.093/0.000 ms
</code></pre>
<p>得到的trace log如下：</p>
<pre><code>icmp request:

TRACE: raw:OUTPUT:policy:2 IN= OUT=eth0 SRC=10.171.77.0 DST=10.28.61.30 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=4494 DF PROTO=ICMP TYPE=8 CODE=0 ID=30426 SEQ=1 UID=0 GID=0
TRACE: mangle:OUTPUT:policy:1 IN= OUT=eth0 SRC=10.171.77.0 DST=10.28.61.30 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=4494 DF PROTO=ICMP TYPE=8 CODE=0 ID=30426 SEQ=1 UID=0 GID=0
TRACE: nat:OUTPUT:policy:2 IN= OUT=eth0 SRC=10.171.77.0 DST=10.28.61.30 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=4494 DF PROTO=ICMP TYPE=8 CODE=0 ID=30426 SEQ=1 UID=0 GID=0
TRACE: filter:OUTPUT:policy:1 IN= OUT=eth0 SRC=10.171.77.0 DST=10.28.61.30 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=4494 DF PROTO=ICMP TYPE=8 CODE=0 ID=30426 SEQ=1 UID=0 GID=0
TRACE: mangle:POSTROUTING:policy:1 IN= OUT=eth0 SRC=10.171.77.0 DST=10.28.61.30 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=4494 DF PROTO=ICMP TYPE=8 CODE=0 ID=30426 SEQ=1 UID=0 GID=0
TRACE: nat:POSTROUTING:policy:2 IN= OUT=eth0 SRC=10.171.77.0 DST=10.28.61.30 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=4494 DF PROTO=ICMP TYPE=8 CODE=0 ID=30426 SEQ=1 UID=0 GID=0

icmp response:

TRACE: raw:PREROUTING:policy:2 IN=eth0 OUT= MAC=00:16:3e:06:3a:3a:00:2a:6a:aa:12:7c:08:00 SRC=10.28.61.30 DST=10.171.77.0 LEN=84 TOS=0x00 PREC=0x00 TTL=57 ID=61118 PROTO=ICMP TYPE=0 CODE=0 ID=30426 SEQ=1
TRACE: mangle:PREROUTING:policy:1 IN=eth0 OUT= MAC=00:16:3e:06:3a:3a:00:2a:6a:aa:12:7c:08:00 SRC=10.28.61.30 DST=10.171.77.0 LEN=84 TOS=0x00 PREC=0x00 TTL=57 ID=61118 PROTO=ICMP TYPE=0 CODE=0 ID=30426 SEQ=1
TRACE: mangle:INPUT:policy:1 IN=eth0 OUT= MAC=00:16:3e:06:3a:3a:00:2a:6a:aa:12:7c:08:00 SRC=10.28.61.30 DST=10.171.77.0 LEN=84 TOS=0x00 PREC=0x00 TTL=57 ID=61118 PROTO=ICMP TYPE=0 CODE=0 ID=30426 SEQ=1
TRACE: filter:INPUT:policy:1 IN=eth0 OUT= MAC=00:16:3e:06:3a:3a:00:2a:6a:aa:12:7c:08:00 SRC=10.28.61.30 DST=10.171.77.0 LEN=84 TOS=0x00 PREC=0x00 TTL=57 ID=61118 PROTO=ICMP TYPE=0 CODE=0 ID=30426 SEQ=1
</code></pre>
<p>可以对照着全图看出在request出去时，发现OUT设备不是bridge，直接走network layer的iptables rules，并从xfrm lookup出去，走到egress(qdisc); response回来时，进行bridge check后，发现IN设备eth0不是bridge，因此直接上到network layer，走iptable chain rules到local process。ebtable的log一行也没有输出。</p>
<p>后续的两个icmp request&amp;response大致相同，并且依旧不走nat PREROUTING和nat POSTROUTING，因为不再是NEW connection。</p>
<h2>五、Container to External</h2>
<p><img src="http://tonybai.com/wp-content/uploads/docker-bridge-network-demo-container-to-external.png" alt="img{512x368}" /></p>
<p>我们在c1 容器中ping 外部的一个节点三次：</p>
<pre><code># docker exec c1 ping -c 3 10.28.61.30
PING 10.28.61.30 (10.28.61.30) 56(84) bytes of data.
64 bytes from 10.28.61.30: icmp_seq=1 ttl=56 time=1.32 ms
64 bytes from 10.28.61.30: icmp_seq=2 ttl=56 time=1.30 ms
64 bytes from 10.28.61.30: icmp_seq=3 ttl=56 time=1.21 ms

--- 10.28.61.30 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2002ms
rtt min/avg/max/mdev = 1.219/1.280/1.323/0.060 ms
</code></pre>
<h3>1、start -> bridgecheck -> linker layer</h3>
<p>和Container to Container的开端很类似，在bridge check后，数据流进入linker layer(docker0 is a bridge)，并在该层进行iptables PREROUTING rules的处理，直到bridge decision之前：</p>
<pre><code>TRACE: eb:broute:BROUTING IN=veth0594f4b OUT= MAC source = 02:42:c0:a8:00:02 MAC dest = 02:42:ff:27:17:4d proto = 0x0800 IP SRC=192.168.0.2 IP DST=10.28.61.30, IP tos=0x00, IP proto=1
TRACE: eb:nat:PREROUTING IN=veth0594f4b OUT= MAC source = 02:42:c0:a8:00:02 MAC dest = 02:42:ff:27:17:4d proto = 0x0800 IP SRC=192.168.0.2 IP DST=10.28.61.30, IP tos=0x00, IP proto=1
TRACE: raw:PREROUTING:policy:2 IN=docker0 OUT= PHYSIN=veth0594f4b MAC=02:42:ff:27:17:4d:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=10.28.61.30 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=57351 DF PROTO=ICMP TYPE=8 CODE=0 ID=94 SEQ=1
TRACE: mangle:PREROUTING:policy:1 IN=docker0 OUT= PHYSIN=veth0594f4b MAC=02:42:ff:27:17:4d:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=10.28.61.30 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=57351 DF PROTO=ICMP TYPE=8 CODE=0 ID=94 SEQ=1
TRACE: nat:PREROUTING:policy:2 IN=docker0 OUT= PHYSIN=veth0594f4b MAC=02:42:ff:27:17:4d:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=10.28.61.30 LEN=84 TOS=0x00 PREC=0x00 TTL=64 ID=57351 DF PROTO=ICMP TYPE=8 CODE=0 ID=94 SEQ=1

</code></pre>
<h3>2、ebtable filter:INPUT -> routing decision -> iptables FORWARD</h3>
<p>目的地址为外部host ip，需要三层介入转发，于是数据包经由eb:filter:INPUT向上走到达network layer的routing decision，根据路由表，将包转发到eth0：</p>
<pre><code>TRACE: mangle:FORWARD:policy:1 IN=docker0 OUT=eth0 PHYSIN=veth0594f4b MAC=02:42:ff:27:17:4d:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=10.28.61.30 LEN=84 TOS=0x00 PREC=0x00 TTL=63 ID=57351 DF PROTO=ICMP TYPE=8 CODE=0 ID=94 SEQ=1
TRACE: filter:FORWARD:rule:1 IN=docker0 OUT=eth0 PHYSIN=veth0594f4b MAC=02:42:ff:27:17:4d:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=10.28.61.30 LEN=84 TOS=0x00 PREC=0x00 TTL=63 ID=57351 DF PROTO=ICMP TYPE=8 CODE=0 ID=94 SEQ=1
TRACE: filter:DOCKER-USER:return:1 IN=docker0 OUT=eth0 PHYSIN=veth0594f4b MAC=02:42:ff:27:17:4d:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=10.28.61.30 LEN=84 TOS=0x00 PREC=0x00 TTL=63 ID=57351 DF PROTO=ICMP TYPE=8 CODE=0 ID=94 SEQ=1
TRACE: filter:FORWARD:rule:2 IN=docker0 OUT=eth0 PHYSIN=veth0594f4b MAC=02:42:ff:27:17:4d:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=10.28.61.30 LEN=84 TOS=0x00 PREC=0x00 TTL=63 ID=57351 DF PROTO=ICMP TYPE=8 CODE=0 ID=94 SEQ=1
TRACE: filter:DOCKER-ISOLATION:return:1 IN=docker0 OUT=eth0 PHYSIN=veth0594f4b MAC=02:42:ff:27:17:4d:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=10.28.61.30 LEN=84 TOS=0x00 PREC=0x00 TTL=63 ID=57351 DF PROTO=ICMP TYPE=8 CODE=0 ID=94 SEQ=1
TRACE: filter:FORWARD:rule:5 IN=docker0 OUT=eth0 PHYSIN=veth0594f4b MAC=02:42:ff:27:17:4d:02:42:c0:a8:00:02:08:00 SRC=192.168.0.2 DST=10.28.61.30 LEN=84 TOS=0x00 PREC=0x00 TTL=63 ID=57351 DF PROTO=ICMP TYPE=8 CODE=0 ID=94 SEQ=1

</code></pre>
<h3>3、iptables nat:POSTROUTING match rule 1</h3>
<p>由于要流出到主机外，因此在最后iptables nat:POSTROUTING中，数据包匹配到rule 1，即做MASQUERADE，将数据包源地址更换为host ip：10.171.77.0。</p>
<pre><code>TRACE: mangle:POSTROUTING:policy:1 IN= OUT=eth0 PHYSIN=veth0594f4b SRC=192.168.0.2 DST=10.28.61.30 LEN=84 TOS=0x00 PREC=0x00 TTL=63 ID=57351 DF PROTO=ICMP TYPE=8 CODE=0 ID=94 SEQ=1
TRACE: nat:POSTROUTING:rule:1 IN= OUT=eth0 PHYSIN=veth0594f4b SRC=192.168.0.2 DST=10.28.61.30 LEN=84 TOS=0x00 PREC=0x00 TTL=63 ID=57351 DF PROTO=ICMP TYPE=8 CODE=0 ID=94 SEQ=1
</code></pre>
<h3>4、iptables prerouting、forward、postrouting -> ebtabls output、postrouting</h3>
<p>返回的应答由于IN设备为eth0，因此直接上到network layer进行iptable chain的处理。在路由后，OUT设备为docker0(bridge设备)，因此在最后的环节需要下降到linker layer做output和postrouting处理：</p>
<pre><code>TRACE: raw:PREROUTING:policy:2 IN=eth0 OUT= MAC=00:16:3e:06:3a:3a:00:2a:6a:aa:12:7c:08:00 SRC=10.28.61.30 DST=10.171.77.0 LEN=84 TOS=0x00 PREC=0x00 TTL=57 ID=58706 PROTO=ICMP TYPE=0 CODE=0 ID=94 SEQ=1
TRACE: mangle:PREROUTING:policy:1 IN=eth0 OUT= MAC=00:16:3e:06:3a:3a:00:2a:6a:aa:12:7c:08:00 SRC=10.28.61.30 DST=10.171.77.0 LEN=84 TOS=0x00 PREC=0x00 TTL=57 ID=58706 PROTO=ICMP TYPE=0 CODE=0 ID=94 SEQ=1
TRACE: mangle:FORWARD:policy:1 IN=eth0 OUT=docker0 MAC=00:16:3e:06:3a:3a:00:2a:6a:aa:12:7c:08:00 SRC=10.28.61.30 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=56 ID=58706 PROTO=ICMP TYPE=0 CODE=0 ID=94 SEQ=1
TRACE: filter:FORWARD:rule:1 IN=eth0 OUT=docker0 MAC=00:16:3e:06:3a:3a:00:2a:6a:aa:12:7c:08:00 SRC=10.28.61.30 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=56 ID=58706 PROTO=ICMP TYPE=0 CODE=0 ID=94 SEQ=1
TRACE: filter:DOCKER-USER:return:1 IN=eth0 OUT=docker0 MAC=00:16:3e:06:3a:3a:00:2a:6a:aa:12:7c:08:00 SRC=10.28.61.30 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=56 ID=58706 PROTO=ICMP TYPE=0 CODE=0 ID=94 SEQ=1
TRACE: filter:FORWARD:rule:2 IN=eth0 OUT=docker0 MAC=00:16:3e:06:3a:3a:00:2a:6a:aa:12:7c:08:00 SRC=10.28.61.30 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=56 ID=58706 PROTO=ICMP TYPE=0 CODE=0 ID=94 SEQ=1
TRACE: filter:DOCKER-ISOLATION:return:1 IN=eth0 OUT=docker0 MAC=00:16:3e:06:3a:3a:00:2a:6a:aa:12:7c:08:00 SRC=10.28.61.30 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=56 ID=58706 PROTO=ICMP TYPE=0 CODE=0 ID=94 SEQ=1
TRACE: filter:FORWARD:rule:3 IN=eth0 OUT=docker0 MAC=00:16:3e:06:3a:3a:00:2a:6a:aa:12:7c:08:00 SRC=10.28.61.30 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=56 ID=58706 PROTO=ICMP TYPE=0 CODE=0 ID=94 SEQ=1
TRACE: mangle:POSTROUTING:policy:1 IN= OUT=docker0 SRC=10.28.61.30 DST=192.168.0.2 LEN=84 TOS=0x00 PREC=0x00 TTL=56 ID=58706 PROTO=ICMP TYPE=0 CODE=0 ID=94 SEQ=1
TRACE: eb:nat:OUTPUT IN= OUT=veth0594f4b MAC source = 02:42:ff:27:17:4d MAC dest = 02:42:c0:a8:00:02 proto = 0x0800 IP SRC=10.28.61.30 IP DST=192.168.0.2, IP tos=0x00, IP proto=1
TRACE: eb:filter:OUTPUT IN= OUT=veth0594f4b MAC source = 02:42:ff:27:17:4d MAC dest = 02:42:c0:a8:00:02 proto = 0x0800 IP SRC=10.28.61.30 IP DST=192.168.0.2, IP tos=0x00, IP proto=1
TRACE: eb:nat:POSTROUTING IN= OUT=veth0594f4b MAC source = 02:42:ff:27:17:4d MAC dest = 02:42:c0:a8:00:02 proto = 0x0800 IP SRC=10.28.61.30 IP DST=192.168.0.2, IP tos=0x00, IP proto=1
</code></pre>
<p>后续的请求和应答基本类似，少的还是nat PREROUTING和nat POSTROUTING，因为不再是NEW connection。</p>
<h2>六、小结</h2>
<p>个人赶脚：iptables的规则还是太复杂了，再加上bridge的ebtable规则，让人有些眼花缭乱。尤其是kube-proxy的规则又与docker的规则鞣合在一起，iptables的rules列表就显得更为冗长和复杂了。但目前kube-proxy稳定版依然以iptables为主要实现机制，不过kube-proxy对ipvs的支持也已经在路上了(kubernetes 1.8中ipvs处于alpha阶段)，希望后续我们能有更多的选择。</p>
<blockquote>
<p>此次实验全部日志内容参见：<a href="http://tonybai.com/wp-content/uploads/docker-bridge-network-demo-iptables-trace-log.txt">docker-bridge-network-demo-iptables-trace-log.txt文件</a>。</p>
</blockquote>
<h2>七、参考资料</h2>
<ul>
<li>《<a href="http://backreference.org/2010/06/11/iptables-debugging/">iptables debugging</a>》</li>
<li>《<a href="http://ebtables.netfilter.org/br_fw_ia/br_fw_ia.html">ebtables/iptables interaction on a Linux-based bridge</a>》</li>
<li>《<a href="http://www.iptables.info/en/print/structure-of-iptables.html">Traversing of tables and chains</a>》</li>
<li>《<a href="https://goyalankit.com/blog/linux-bridge">Linux Bridge &#8211; how it works</a>》</li>
<li>“<a href="https://github.com/andl/docker-explain/blob/master/network/iptables.md">docker-explain network</a>“</li>
<li>《<a href="http://blog.csdn.net/jianchaolv/article/details/25777249">Linux下的虚拟Bridge实现</a>》</li>
</ul>
<hr />
<p>微博：<a href="http://weibo.com/bigwhite20xx">@tonybai_cn</a><br />
微信公众号：iamtonybai<br />
github.com: https://github.com/bigwhite</p>
<p style='text-align:left'>&copy; 2017, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2017/11/06/explain-docker-single-host-network-using-iptables-trace-and-ebtables-log/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>理解Kubernetes网络之Flannel网络</title>
		<link>https://tonybai.com/2017/01/17/understanding-flannel-network-for-kubernetes/</link>
		<comments>https://tonybai.com/2017/01/17/understanding-flannel-network-for-kubernetes/#comments</comments>
		<pubDate>Tue, 17 Jan 2017 07:40:58 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[aliyun]]></category>
		<category><![CDATA[arp]]></category>
		<category><![CDATA[brctl]]></category>
		<category><![CDATA[calico]]></category>
		<category><![CDATA[cluster]]></category>
		<category><![CDATA[cni]]></category>
		<category><![CDATA[CNM]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[coreos]]></category>
		<category><![CDATA[DNAT]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[ECS]]></category>
		<category><![CDATA[etcd]]></category>
		<category><![CDATA[etcdctl]]></category>
		<category><![CDATA[fdb]]></category>
		<category><![CDATA[flanned]]></category>
		<category><![CDATA[flannel]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[iproute2]]></category>
		<category><![CDATA[iptables]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[kube-up.sh]]></category>
		<category><![CDATA[kubeadm]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[MASQUERADE]]></category>
		<category><![CDATA[minion]]></category>
		<category><![CDATA[NAT]]></category>
		<category><![CDATA[overlaynetwork]]></category>
		<category><![CDATA[pod]]></category>
		<category><![CDATA[POSTROUTING]]></category>
		<category><![CDATA[PREROUTING]]></category>
		<category><![CDATA[Service]]></category>
		<category><![CDATA[SNAT]]></category>
		<category><![CDATA[tunnel]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[VTEP]]></category>
		<category><![CDATA[VXLAN]]></category>
		<category><![CDATA[weave]]></category>
		<category><![CDATA[内核]]></category>
		<category><![CDATA[容器]]></category>
		<category><![CDATA[阿里云]]></category>
		<category><![CDATA[隧道]]></category>
		<category><![CDATA[集群]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=2126</guid>
		<description><![CDATA[第一次采用kube-up.sh脚本方式安装的Kubernetes cluster目前运行良好，master node上的组件状态也始终是“没毛病”： # kubectl get cs NAME STATUS MESSAGE ERROR controller-manager Healthy ok scheduler Healthy ok etcd-0 Healthy {"health": "true"} 不过在第二次尝试用kubeadm安装和初始化Kubernetes cluster时遇到的各种网络问题还是让我“心有余悸”。于是趁上个周末，对Kubernetes的网络原理进行了一些针对性的学习。这里把对Kubernetes网络的理解记录一下和大家一起分享。 Kubernetes支持Flannel、Calico、Weave network等多种cni网络Drivers，但由于学习过程使用的是第一个cluster的Flannel网络，这里的网络原理只针对k8s+Flannel网络。 一、环境+提示 凡涉及到Docker、Kubernetes这类正在active dev的开源项目的文章，我都不得不提一嘴，那就是随着K8s以及flannel的演化，本文中的一些说法可能不再正确。提醒大家：阅读此类技术文章务必结合“环境”。 这里我们使用的环境就是我第一次建立k8s cluster的环境： # kube-apiserver --version Kubernetes v1.3.7 # /opt/bin/flanneld -version 0.5.5 # /opt/bin/etcd -version etcd Version: 3.0.12 Git SHA: 2d1e2e8 Go Version: go1.6.3 Go OS/Arch: linux/amd64 另外整个集群搭建在阿里云上，每个ECS上的OS及kernel版本：Ubuntu [...]]]></description>
			<content:encoded><![CDATA[<p>第一次<a href="http://tonybai.com/2016/10/18/learn-how-to-install-kubernetes-on-ubuntu">采用kube-up.sh脚本方式安装</a>的<a href="http://tonybai.com/tag/kubernetes">Kubernetes</a> cluster目前运行良好，master node上的组件状态也始终是“没毛病”：</p>
<pre><code># kubectl get cs
NAME                 STATUS    MESSAGE              ERROR
controller-manager   Healthy   ok
scheduler            Healthy   ok
etcd-0               Healthy   {"health": "true"}
</code></pre>
<p>不过在第二次尝试<a href="http://tonybai.com/2016/12/30/install-kubernetes-on-ubuntu-with-kubeadm">用kubeadm安装和初始化Kubernetes cluster</a>时遇到的各种网络问题还是让我“心有余悸”。于是趁上个周末，对Kubernetes的网络原理进行了一些针对性的学习。这里把对Kubernetes网络的理解记录一下和大家一起分享。</p>
<p>Kubernetes支持<a href="https://github.com/coreos/flannel">Flannel</a>、<a href="https://projectcalico.org/">Calico</a>、<a href="https://www.weave.works/">Weave network</a>等多种<a href="https://github.com/containernetworking/cni">cni网络</a>Drivers，但由于学习过程使用的是第一个cluster的Flannel网络，这里的网络原理只针对k8s+Flannel网络。</p>
<h3>一、环境+提示</h3>
<p>凡涉及到Docker、Kubernetes这类正在active dev的开源项目的文章，我都不得不提一嘴，那就是随着K8s以及flannel的演化，本文中的一些说法可能不再正确。提醒大家：阅读此类技术文章务必结合“环境”。</p>
<p>这里我们使用的环境就是我第一次建立k8s cluster的环境：</p>
<pre><code># kube-apiserver --version
Kubernetes v1.3.7

# /opt/bin/flanneld -version
0.5.5

# /opt/bin/etcd -version
etcd Version: 3.0.12
Git SHA: 2d1e2e8
Go Version: go1.6.3
Go OS/Arch: linux/amd64
</code></pre>
<p>另外整个集群搭建在<a href="https://www.aliyun.com/">阿里云</a>上，每个ECS上的OS及kernel版本：Ubuntu 14.04.4 LTS，3.19.0-70-generic。</p>
<p>在我的测试环境，有两个node：master node和一个minion node。master node参与workload的调度。所以你基本可以认为有两个minion node即可。</p>
<h3>二、Kubernetes Cluster中的几个“网络”</h3>
<p>之前的k8s cluster采用的是默认安装，即直接使用了配置脚本中(kubernetes/cluster/ubuntu/config-default.sh)自带的一些参数，比如：</p>
<pre><code>//摘自kubernetes/cluster/ubuntu/config-default.sh

export nodes=${nodes:-"root@master_node_ip root@minion_node_ip"}
export SERVICE_CLUSTER_IP_RANGE=${SERVICE_CLUSTER_IP_RANGE:-192.168.3.0/24}
export FLANNEL_NET=${FLANNEL_NET:-172.16.0.0/16}
</code></pre>
<p>从这里我们能够识别出三个“网络”：</p>
<ul>
<li>node network：承载kubernetes集群中各个“物理”Node(master和minion)通信的网络；</li>
<li>service network：由kubernetes集群中的Services所组成的“网络”；</li>
<li>flannel network： 即Pod网络，集群中承载各个Pod相互通信的网络。</li>
</ul>
<p>node network自不必多说，node间通过你的本地局域网（无论是物理的还是虚拟的）通信。</p>
<p>service network比较特殊，每个新创建的service会被分配一个service IP，在当前集群中，这个IP的分配范围是192.168.3.0/24。不过这个IP并不“真实”，更像一个“占位符”并且只有入口流量，所谓的“network”也是“名不符实”的，后续我们会详尽说明。</p>
<p>flannel network是我们要理解的重点，cluster中各个Pod要实现相互通信，必须走这个网络，无论是在同一node上的Pod还是跨node的Pod。我们的cluster中，flannel net的分配范围是：172.16.0.0/16。</p>
<p>在进一步挖掘“原理”之前，我们先来直观认知一下service network和flannel network：</p>
<p>Service network(看cluster-ip一列)：</p>
<pre><code># kubectl get services
NAME           CLUSTER-IP      EXTERNAL-IP   PORT(S)     AGE
index-api      192.168.3.168   &lt;none&gt;        30080/TCP   18d
kubernetes     192.168.3.1     &lt;none&gt;        443/TCP     94d
my-nginx       192.168.3.179   &lt;nodes&gt;       80/TCP      90d
nginx-kit      192.168.3.196   &lt;nodes&gt;       80/TCP      12d
rbd-rest-api   192.168.3.22    &lt;none&gt;        8080/TCP    60d
</code></pre>
<p>Flannel network（看IP那列）:</p>
<pre><code># kubectl get pod -o wide
NAME                           READY     STATUS    RESTARTS   AGE       IP            NODE
my-nginx-2395715568-gpljv      1/1       Running   6          91d       172.16.99.3   {master node ip}
nginx-kit-3872865736-rc8hr     2/2       Running   0          12d       172.16.57.7   {minion node ip}
... ...
</code></pre>
<h3>三、平坦的Flannel网络</h3>
<h4>1、Kubenetes安装后的网络状态</h4>
<p>首先让我们来看看：<a href="http://tonybai.com/2016/10/18/learn-how-to-install-kubernetes-on-ubuntu">kube-up.sh在安装k8s集群</a>时对各个K8s Node都动了什么手脚！</p>
<h5>a) 修改docker default配置</h5>
<p>在ubuntu 14.04下，<a href="http://tonybai.com/2016/12/27/when-docker-meets-systemd">docker的配置</a>都在/etc/default/docker文件中。如果你曾经修改过该文件，那么kube-up.sh脚本方式安装完kubernetes后，你会发现/etc/default/docker已经变样了，只剩下了一行：</p>
<pre><code>master node:
DOCKER_OPTS=" -H tcp://127.0.0.1:4243 -H unix:///var/run/docker.sock --bip=172.16.99.1/24 --mtu=1450"

minion node:
DOCKER_OPTS=" -H tcp://127.0.0.1:4243 -H unix:///var/run/docker.sock --bip=172.16.57.1/24 --mtu=1450"
</code></pre>
<p>可以看出kube-up.sh修改了Docker daemon的&#8211;bip选项，使得该node上docker daemon在该node的fannel subnet范围以内为启动的Docker container分配IP地址。</p>
<h5>b) 在etcd中初始化flannel网络数据</h5>
<p>多个node上的Flanneld依赖一个<a href="https://github.com/coreos/etcd/">etcd cluster</a>来做集中配置服务，etcd保证了所有node上flanned所看到的配置是一致的。同时每个node上的flanned监听etcd上的数据变化，实时感知集群中node的变化。</p>
<p>我们可以通过etcdctl查询到这些配置数据：</p>
<pre><code>master node:

//flannel network配置
# etcdctl --endpoints http://127.0.0.1:{etcd listen port} get  /coreos.com/network/config
{"Network":"172.16.0.0/16", "Backend": {"Type": "vxlan"}}

# etcdctl --endpoints http://127.0.0.1:{etcd listen port} ls  /coreos.com/network/subnets
/coreos.com/network/subnets/172.16.99.0-24
/coreos.com/network/subnets/172.16.57.0-24

//某一node上的flanne subnet和vtep配置
# etcdctl --endpoints http://127.0.0.1:{etcd listen port} get  /coreos.com/network/subnets/172.16.99.0-24
{"PublicIP":"{master node ip}","BackendType":"vxlan","BackendData":{"VtepMAC":"b6:bf:4c:81:cf:3b"}}

minion node:
# etcdctl --endpoints http://127.0.0.1:{etcd listen port} get  /coreos.com/network/subnets/172.16.57.0-24
{"PublicIP":"{minion node ip}","BackendType":"vxlan","BackendData":{"VtepMAC":"d6:51:2e:80:5c:69"}}
</code></pre>
<p>或用etcd 提供的rest api：</p>
<pre><code># curl -L http://127.0.0.1:{etcd listen port}/v2/keys/coreos.com/network/config
{"action":"get","node":{"key":"/coreos.com/network/config","value":"{\"Network\":\"172.16.0.0/16\", \"Backend\": {\"Type\": \"vxlan\"}}","modifiedIndex":5,"createdIndex":5}}
</code></pre>
<h5>c) 启动flanneld</h5>
<p>kube-up.sh在每个Kubernetes node上启动了一个flanneld的程序：</p>
<pre><code># ps -ef|grep flanneld

master node:
root      1151     1  0  2016 ?        00:02:34 /opt/bin/flanneld --etcd-endpoints=http://127.0.0.1:{etcd listen port} --ip-masq --iface={master node ip}

minion node:
root     11940     1  0  2016 ?        00:07:05 /opt/bin/flanneld --etcd-endpoints=http://{master node ip}:{etcd listen port} --ip-masq --iface={minion node ip}
</code></pre>
<p>一旦flanneld启动，它将从etcd中读取配置，并请求获取一个subnet lease(租约)，有效期目前是24hrs，并且监视etcd的数据更新。flanneld一旦获取subnet租约、配置完backend，它会将一些信息写入/run/flannel/subnet.env文件。</p>
<pre><code>master node：
# cat /run/flannel/subnet.env
FLANNEL_NETWORK=172.16.0.0/16
FLANNEL_SUBNET=172.16.99.1/24
FLANNEL_MTU=1450
FLANNEL_IPMASQ=true

minion node:
# cat /run/flannel/subnet.env
FLANNEL_NETWORK=172.16.0.0/16
FLANNEL_SUBNET=172.16.57.1/24
FLANNEL_MTU=1450
FLANNEL_IPMASQ=true
</code></pre>
<p>当然flanneld的最大意义在于根据etcd中存储的全cluster的subnet信息，跨node传输flannel network中的数据包，这个后面会详细说明。</p>
<h5>d) 创建flannel.1 网络设备、更新路由信息</h5>
<p>各个node上的网络设备列表新增一个名为flannel.1的类型为vxlan的网络设备：</p>
<pre><code>master node:

# ip -d link show
4: flannel.1: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1450 qdisc noqueue state UNKNOWN mode DEFAULT group default
    link/ether b6:bf:4c:81:cf:3b brd ff:ff:ff:ff:ff:ff promiscuity 0
    vxlan id 1 local {master node local ip} dev eth0 port 0 0 nolearning ageing 300

minion node:

349: flannel.1: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1450 qdisc noqueue state UNKNOWN mode DEFAULT group default
    link/ether d6:51:2e:80:5c:69 brd ff:ff:ff:ff:ff:ff promiscuity 0
    vxlan id 1 local  {minion node local ip} dev eth0 port 0 0 nolearning ageing 300

</code></pre>
<p>从flannel.1的设备信息来看，它似乎与eth0存在着某种bind关系。这是在其他bridge、veth设备描述信息中所没有的。</p>
<p>flannel.1设备的ip：</p>
<pre><code>master node:

flannel.1 Link encap:Ethernet  HWaddr b6:bf:4c:81:cf:3b
          inet addr:172.16.99.0  Bcast:0.0.0.0  Mask:255.255.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1450  Metric:1
          RX packets:5993274 errors:0 dropped:0 overruns:0 frame:0
          TX packets:5829044 errors:0 dropped:292 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:1689890445 (1.6 GB)  TX bytes:1144725704 (1.1 GB)

minion node:

flannel.1 Link encap:Ethernet  HWaddr d6:51:2e:80:5c:69
          inet addr:172.16.57.0  Bcast:0.0.0.0  Mask:255.255.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1450  Metric:1
          RX packets:6294640 errors:0 dropped:0 overruns:0 frame:0
          TX packets:5755599 errors:0 dropped:25 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:989362527 (989.3 MB)  TX bytes:1861492847 (1.8 GB)

</code></pre>
<p>可以看到两个node上的flannel.1的ip与k8s cluster为两个node上分配subnet的ip范围是对应的。</p>
<p>下面是两个node上的当前路由表：</p>
<pre><code>master node:

# ip route
... ...
172.16.0.0/16 dev flannel.1  proto kernel  scope link  src 172.16.99.0
172.16.99.0/24 dev docker0  proto kernel  scope link  src 172.16.99.1
... ...

minion node:

# ip route
... ...
172.16.0.0/16 dev flannel.1
172.16.57.0/24 dev docker0  proto kernel  scope link  src 172.16.57.1
... ...

</code></pre>
<p>以上信息将为后续数据包传输分析打下基础。</p>
<h5>e) 平坦的flannel network</h5>
<p>从以上kubernetes和flannel network安装之后获得的网络信息，我们能看出flannel network是一个flat network。在flannel：172.16.0.0/16这个大网下，每个kubernetes node从中分配一个子网片段(/24)：</p>
<pre><code>master node：
  --bip=172.16.99.1/24

minion node：
  --bip=172.16.57.1/24

root@node1:~# etcdctl --endpoints http://127.0.0.1:{etcd listen port} ls  /coreos.com/network/subnets
/coreos.com/network/subnets/172.16.99.0-24
/coreos.com/network/subnets/172.16.57.0-24
</code></pre>
<p>用一张图来诠释可能更为直观：</p>
<p><img src="http://tonybai.com/wp-content/uploads/flat-flannel-network.png" alt="img{512x368}" /></p>
<p>这个是不是有些像x86-64的虚拟内存寻址空间啊（同样是平坦内存地址访问模型）！</p>
<p>在平坦的flannel network中，每个pod都会被分配唯一的ip地址，且每个k8s node的subnet各不重叠，没有交集。不过这样的subnet分配模型也有一定弊端，那就是可能存在ip浪费：一个node上有200多个flannel ip地址(xxx.xxx.xxx.xxx/24)，如果仅仅启动了几个Pod，那么其余ip就处于闲置状态。</p>
<h4>2、Flannel网络通信原理</h4>
<p>这里我们模仿flannel官方的那幅原理图，画了一幅与我们的实验环境匹配的图，作为后续讨论flannel网络通信流程的基础：</p>
<p><img src="http://tonybai.com/wp-content/uploads/kubernetes-flannel.png" alt="img{512x368}" /></p>
<p>如上图所示，我们来看看从pod1：172.16.99.8发出的数据包是如何到达pod3：172.16.57.15的（比如：在pod1的某个container中ping -c 3 172.16.57.15）。</p>
<h5>a) 从Pod出发</h5>
<p>由于k8s更改了docker的DOCKER_OPTS，显式指定了&#8211;bip，这个值与分配给该node上的subnet的范围是一致的。这样一来，docker引擎每次创建一个Docker container，该container被分配到的ip都在flannel subnet范围内。</p>
<p>当我们在Pod1下的某个容器内执行ping -c 3 172.16.57.15，数据包便开始了它在flannel network中的旅程。</p>
<p>Pod是Kubernetes调度的基本unit。Pod内的多个container共享一个<a href="http://tonybai.com/2017/01/11/understanding-linux-network-namespace-for-docker-network/">network namespace</a>。kubernetes在创建Pod时，首先先创建pause容器，然后再以pause的network namespace为基础，创建pod内的其他容器（&#8211;net=container:xxx），这样Pod内的所有容器便共享一个network namespace，这些容器间的访问直接通过localhost即可。比如Pod下A容器启动了一个服务，监听8080端口，那么同一个Pod下面的另外一个B容器通过访问localhost:8080即可访问到A容器下面的那个服务。</p>
<p>在之前的《<a href="http://tonybai.com/2017/01/11/understanding-linux-network-namespace-for-docker-network/">理解Docker容器网络之Linux Network Namespace</a>》一文中，我相信我已经讲清楚了单机下Docker容器数据传输的路径。在这个环节中，数据包的传输路径也并无不同。</p>
<p>我们看一下Pod1中某Container内的路由信息：</p>
<pre><code># docker exec ba75f81455c7 ip route
default via 172.16.99.1 dev eth0
172.16.99.0/24 dev eth0  proto kernel  scope link  src 172.16.99.8
</code></pre>
<p>目的地址172.16.57.15并不在直连网络中，因此数据包通过default路由出去。default路由的路由器地址是172.16.99.1，也就是上面的docker0 bridge的IP地址。相当于docker0 bridge以“三层的工作模式”直接接收到来自容器的数据包(而并非从bridge的二层端口接收)。</p>
<h5>b) docker0与flannel.1之间的包转发</h5>
<p>数据包到达docker0后，docker0的内核栈处理程序发现这个数据包的目的地址是172.16.57.15，并不是真的要送给自己，于是开始为该数据包找下一hop。根据master node上的路由表：</p>
<pre><code>master node：

# ip route
... ...
172.16.0.0/16 dev flannel.1  proto kernel  scope link  src 172.16.99.0
172.16.99.0/24 dev docker0  proto kernel  scope link  src 172.16.99.1
... ...
</code></pre>
<p>我们匹配到“172.16.0.0/16”这条路由！这是一条直连路由，数据包被直接送到flannel.1设备上。</p>
<h5>c) flannel.1设备以及flanneld的功用</h5>
<p>flannel.1是否会重复docker0的套路呢：包不是发给自己，转发数据包？会，也不会。</p>
<p>“会”是指flannel.1肯定要将包转发出去，因为毕竟包不是给自己的（包目的ip是172.16.57.15, vxlan设备ip是172.16.99.0）。<br />
“不会”是指flannel.1不会走寻常套路去转发包，因为它是一个vxlan类型的设备，也称为vtep，virtual tunnel end point。</p>
<p>那么它到底是怎么处理数据包的呢？这里涉及一些Linux内核对vxlan处理的内容，详细内容可参见本文末尾的参考资料。</p>
<p>flannel.1收到数据包后，由于自己不是目的地，也要尝试将数据包重新发送出去。数据包沿着网络协议栈向下流动，在二层时需要封二层以太包，填写目的mac地址，这时一般应该发出arp：”who is 172.16.57.15&#8243;。但vxlan设备的特殊性就在于它并没有真正在二层发出这个arp包，因为下面的这个内核参数设置：</p>
<pre><code>master node:

# cat /proc/sys/net/ipv4/neigh/flannel.1/app_solicit
3
</code></pre>
<p>而是由linux kernel引发一个”L3 MISS”事件并将arp请求发到用户空间的flanned程序。</p>
<p>flanned程序收到”L3 MISS”内核事件以及arp请求(who is 172.16.57.15)后，并不会向外网发送arp request，而是尝试从etcd查找该地址匹配的子网的vtep信息。在前面章节我们曾经展示过etcd中Flannel network的配置信息：</p>
<pre><code>master node:

# etcdctl --endpoints http://127.0.0.1:{etcd listen port} ls  /coreos.com/network/subnets
/coreos.com/network/subnets/172.16.99.0-24
/coreos.com/network/subnets/172.16.57.0-24

# curl -L http://127.0.0.1:{etcd listen port}/v2/keys/coreos.com/network/subnets/172.16.57.0-24
{"action":"get","node":{"key":"/coreos.com/network/subnets/172.16.57.0-24","value":"{\"PublicIP\":\"{minion node local ip}\",\"BackendType\":\"vxlan\",\"BackendData\":{\"VtepMAC\":\"d6:51:2e:80:5c:69\"}}","expiration":"2017-01-17T09:46:20.607339725Z","ttl":21496,"modifiedIndex":2275460,"createdIndex":2275460}}
</code></pre>
<p>flanneld从etcd中找到了答案：</p>
<pre><code>subnet: 172.16.57.0/24
public ip: {minion node local ip}
VtepMAC: d6:51:2e:80:5c:69
</code></pre>
<p>我们查看minion node上的信息，发现minion node上的flannel.1 设备mac就是d6:51:2e:80:5c:69：</p>
<pre><code>minion node:

#ip -d link show

349: flannel.1: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1450 qdisc noqueue state UNKNOWN mode DEFAULT group default
    link/ether d6:51:2e:80:5c:69 brd ff:ff:ff:ff:ff:ff promiscuity 0
    vxlan id 1 local 10.46.181.146 dev eth0 port 0 0 nolearning ageing 300
</code></pre>
<p>接下来，flanned将查询到的信息放入master node host的arp cache表中：</p>
<pre><code>master node:

#ip n |grep 172.16.57.15
172.16.57.15 dev flannel.1 lladdr d6:51:2e:80:5c:69 REACHABLE
</code></pre>
<p>flanneld完成这项工作后，linux kernel就可以在arp table中找到 172.16.57.15对应的mac地址并封装二层以太包了。</p>
<p>到目前为止，已经呈现在大家眼前的封包如下图：</p>
<p><img src="http://tonybai.com/wp-content/uploads/flannel-network-inner-packet.png" alt="img{512x368}" /></p>
<p>不过这个封包还不能在物理网络上传输，因为它实际上只是vxlan tunnel上的packet。</p>
<h5>d) kernel的vxlan封包</h5>
<p>我们需要将上述的packet从master node传输到minion node，需要将上述packet再次封包。这个任务在backend为vxlan的flannel network中由linux kernel来完成。</p>
<p>flannel.1为vxlan设备，linux kernel可以自动识别，并将上面的packet进行vxlan封包处理。在这个封包过程中，kernel需要知道该数据包究竟发到哪个node上去。kernel需要查看node上的fdb(forwarding database)以获得上面对端vtep设备（已经从arp table中查到其mac地址：d6:51:2e:80:5c:69）所在的node地址。如果fdb中没有这个信息，那么kernel会向用户空间的flanned程序发起”L2 MISS”事件。flanneld收到该事件后，会查询etcd，获取该vtep设备对应的node的”Public IP“，并将信息注册到fdb中。</p>
<p>这样Kernel就可以顺利查询到该信息并封包了：</p>
<pre><code>master node:

# bridge fdb show dev flannel.1|grep d6:51:2e:80:5c:69
d6:51:2e:80:5c:69 dst {minion node local ip} self permanent
</code></pre>
<p>由于目标ip是minion node，查找路由表，包应该从master node的eth0发出，这样src ip和src mac地址也就确定了。封好的包示意图如下：</p>
<p><img src="http://tonybai.com/wp-content/uploads/flannel-network-eth0-packet.png" alt="img{512x368}" /></p>
<h5>e) kernel的vxlan拆包</h5>
<p>minion node上的eth0接收到上述vxlan包，kernel将识别出这是一个vxlan包，于是拆包后将flannel.1 packet转给minion node上的vtep（flannel.1）。minion node上的flannel.1再将这个数据包转到minion node上的docker0，继而由docker0传输到Pod3的某个容器里。</p>
<h4>3、Pod内到外部网络</h4>
<p>我们在Pod中除了可以与pod network中的其他pod通信外，还可以访问外部网络，比如：</p>
<pre><code>master node:
# docker exec ba75f81455c7 ping -c 3 baidu.com
PING baidu.com (180.149.132.47): 56 data bytes
64 bytes from 180.149.132.47: icmp_seq=0 ttl=54 time=3.586 ms
64 bytes from 180.149.132.47: icmp_seq=1 ttl=54 time=3.752 ms
64 bytes from 180.149.132.47: icmp_seq=2 ttl=54 time=3.722 ms
--- baidu.com ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max/stddev = 3.586/3.687/3.752/0.072 ms
</code></pre>
<p>这个通信与vxlan就没有什么关系了，主要是通过docker引擎在iptables的POSTROUTING chain中设置的MASQUERADE规则：</p>
<pre><code>mastre node:

#iptables -t nat -nL
... ...
Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination
MASQUERADE  all  --  172.16.99.0/24       0.0.0.0/0
... ...

</code></pre>
<p>docker将容器的pod network地址伪装为node ip出去，包回来时再snat回容器的pod network地址，这样网络就通了。</p>
<h3>四、”不真实”的Service网络</h3>
<p>每当我们在k8s cluster中创建一个service，k8s cluster就会在&#8211;service-cluster-ip-range的范围内为service分配一个cluster-ip，比如本文开始时提到的：</p>
<pre><code># kubectl get services
NAME           CLUSTER-IP      EXTERNAL-IP   PORT(S)     AGE
index-api      192.168.3.168   &lt;none&gt;        30080/TCP   18d
kubernetes     192.168.3.1     &lt;none&gt;        443/TCP     94d
my-nginx       192.168.3.179   &lt;nodes&gt;       80/TCP      90d
nginx-kit      192.168.3.196   &lt;nodes&gt;       80/TCP      12d
rbd-rest-api   192.168.3.22    &lt;none&gt;        8080/TCP    60d
</code></pre>
<p>这个cluster-ip只是一个虚拟的ip，并不真实绑定某个物理网络设备或虚拟网络设备，仅仅存在于iptables的规则中：</p>
<pre><code>Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
KUBE-SERVICES  all  --  0.0.0.0/0            0.0.0.0/0            /* kubernetes service portals */

# iptables -t nat -nL|grep 192.168.3
Chain KUBE-SERVICES (2 references)
target     prot opt source               destination
KUBE-SVC-XGLOHA7QRQ3V22RZ  tcp  --  0.0.0.0/0            192.168.3.182        /* kube-system/kubernetes-dashboard: cluster IP */ tcp dpt:80
KUBE-SVC-NPX46M4PTMTKRN6Y  tcp  --  0.0.0.0/0            192.168.3.1          /* default/kubernetes:https cluster IP */ tcp dpt:443
KUBE-SVC-AU252PRZZQGOERSG  tcp  --  0.0.0.0/0            192.168.3.22         /* default/rbd-rest-api: cluster IP */ tcp dpt:8080
KUBE-SVC-TCOU7JCQXEZGVUNU  udp  --  0.0.0.0/0            192.168.3.10         /* kube-system/kube-dns:dns cluster IP */ udp dpt:53
KUBE-SVC-BEPXDJBUHFCSYIC3  tcp  --  0.0.0.0/0            192.168.3.179        /* default/my-nginx: cluster IP */ tcp dpt:80
KUBE-SVC-UQG6736T32JE3S7H  tcp  --  0.0.0.0/0            192.168.3.196        /* default/nginx-kit: cluster IP */ tcp dpt:80
KUBE-SVC-ERIFXISQEP7F7OF4  tcp  --  0.0.0.0/0            192.168.3.10         /* kube-system/kube-dns:dns-tcp cluster IP */ tcp dpt:53
... ...
</code></pre>
<p>可以看到在PREROUTING环节，k8s设置了一个target: KUBE-SERVICES。而KUBE-SERVICES下面又设置了许多target，一旦destination和dstport匹配，就会沿着chain进行处理。</p>
<p>比如：当我们在pod网络curl 192.168.3.22  8080时，匹配到下面的KUBE-SVC-AU252PRZZQGOERSG target：</p>
<pre><code>KUBE-SVC-AU252PRZZQGOERSG  tcp  --  0.0.0.0/0            192.168.3.22         /* default/rbd-rest-api: cluster IP */ tcp dpt:8080
</code></pre>
<p>沿着target，我们看到”KUBE-SVC-AU252PRZZQGOERSG”对应的内容如下：</p>
<pre><code>Chain KUBE-SVC-AU252PRZZQGOERSG (1 references)
target     prot opt source               destination
KUBE-SEP-I6L4LR53UYF7FORX  all  --  0.0.0.0/0            0.0.0.0/0            /* default/rbd-rest-api: */ statistic mode random probability 0.50000000000
KUBE-SEP-LBWOKUH4CUTN7XKH  all  --  0.0.0.0/0            0.0.0.0/0            /* default/rbd-rest-api: */

Chain KUBE-SEP-I6L4LR53UYF7FORX (1 references)
target     prot opt source               destination
KUBE-MARK-MASQ  all  --  172.16.99.6          0.0.0.0/0            /* default/rbd-rest-api: */
DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0            /* default/rbd-rest-api: */ tcp to:172.16.99.6:8080

Chain KUBE-SEP-LBWOKUH4CUTN7XKH (1 references)
target     prot opt source               destination
KUBE-MARK-MASQ  all  --  172.16.99.7          0.0.0.0/0            /* default/rbd-rest-api: */
DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0            /* default/rbd-rest-api: */ tcp to:172.16.99.7:8080

Chain KUBE-MARK-MASQ (17 references)
target     prot opt source               destination
MARK       all  --  0.0.0.0/0            0.0.0.0/0            MARK or 0x4000
</code></pre>
<p>请求被按5：5开的比例分发（起到负载均衡的作用）到KUBE-SEP-I6L4LR53UYF7FORX 和KUBE-SEP-LBWOKUH4CUTN7XKH，而这两个chain的处理方式都是一样的，那就是先做mark，然后做dnat，将service ip改为pod network中的Pod IP，进而请求被实际传输到某个service下面的pod中处理了。</p>
<h3>五、参考资料</h3>
<ul>
<li><a href="http://www.slideshare.net/enakai/how-vxlan-works-on-linux">How VXLAN works on Linux&amp;VTEP implementation with Flannel</a></li>
<li><a href="http://events.linuxfoundation.org/sites/events/files/slides/LinuxConJapan2014_makita_0.pdf">Virtual switching technologies and Linux bridge</a></li>
<li><a href="http://enakai00.hatenablog.com/entry/2015/04/02/173739">How Flannel&#8217;s VXLAN backend works</a>  建议用google翻译将网页从日文翻译成英文再看^0^。</li>
<li><a href="http://events.linuxfoundation.org/sites/events/files/slides/2013-linuxcon.pdf">Software Defined Networking using VXLAN</a> </li>
</ul>
<p style='text-align:left'>&copy; 2017, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2017/01/17/understanding-flannel-network-for-kubernetes/feed/</wfw:commentRss>
		<slash:comments>15</slash:comments>
		</item>
		<item>
		<title>理解Docker容器网络之Linux Network Namespace</title>
		<link>https://tonybai.com/2017/01/11/understanding-linux-network-namespace-for-docker-network/</link>
		<comments>https://tonybai.com/2017/01/11/understanding-linux-network-namespace-for-docker-network/#comments</comments>
		<pubDate>Wed, 11 Jan 2017 14:30:55 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[brctl]]></category>
		<category><![CDATA[bridge]]></category>
		<category><![CDATA[cni]]></category>
		<category><![CDATA[CNM]]></category>
		<category><![CDATA[curl]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[docker-proxy]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[http]]></category>
		<category><![CDATA[iproute2]]></category>
		<category><![CDATA[iptables]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Namespace]]></category>
		<category><![CDATA[nginx]]></category>
		<category><![CDATA[ping]]></category>
		<category><![CDATA[traceroute]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[veth]]></category>
		<category><![CDATA[交换机]]></category>
		<category><![CDATA[代理]]></category>
		<category><![CDATA[内核]]></category>
		<category><![CDATA[容器]]></category>
		<category><![CDATA[网络]]></category>
		<category><![CDATA[网络名字空间]]></category>
		<category><![CDATA[虚拟网桥]]></category>
		<category><![CDATA[路由表]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=2118</guid>
		<description><![CDATA[由于2016年年中调换工作的原因，对容器网络的研究中断过一段时间。随着当前项目对Kubernetes应用的深入，我感觉之前对于容器网络的粗浅理解已经不够了，容器网络成了摆在前面的“一道坎”。继续深入理解K8s网络、容器网络已经势在必行。而这篇文章就算是一个重新开始，也是对之前浅表理解的一个补充。 我还是先从Docker容器网络入手，虽然Docker与Kubernetes采用了不同的网络模型：K8s是Container Network Interface, CNI模型，而Docker则采用的是Container Network Model, CNM模型。而要了解Docker容器网络，理解Linux Network Namespace是不可或缺的。在本文中我们将尝试理解Linux Network Namespace及相关Linux内核网络设备的概念，并手工模拟Docker容器网络模型的部分实现，包括单机容器网络中的容器与主机连通、容器间连通以及端口映射等。 一、Docker的CNM网络模型 Docker通过libnetwork实现了CNM网络模型。libnetwork设计doc中对CNM模型的简单诠释如下： CNM模型有三个组件： Sandbox(沙盒)：每个沙盒包含一个容器网络栈(network stack)的配置，配置包括：容器的网口、路由表和DNS设置等。 Endpoint(端点)：通过Endpoint，沙盒可以被加入到一个Network里。 Network(网络)：一组能相互直接通信的Endpoints。 光看这些，我们还很难将之与现实中的Docker容器联系起来，毕竟是抽象的模型不对应到实体，总有种漂浮的赶脚。文档中又给出了CNM模型在Linux上的参考实现技术，比如：沙盒的实现可以是一个Linux Network Namespace；Endpoint可以是一对VETH；Network则可以用Linux Bridge或Vxlan实现。 这些实现技术反倒是比较接地气。之前我们在使用Docker容器时，了解过Docker是用linux network namespace实现的容器网络隔离的。使用docker时，在物理主机或虚拟机上会有一个docker0的linux bridge，brctl show时能看到 docker0上“插上了”好多veth网络设备： # ip link show ... ... 3: docker0: &#60;BROADCAST,MULTICAST,UP,LOWER_UP&#62; mtu 1500 qdisc noqueue state UP mode DEFAULT group default link/ether 02:42:30:11:98:ef brd ff:ff:ff:ff:ff:ff 19: veth4559467@if18: &#60;BROADCAST,MULTICAST,UP,LOWER_UP&#62; [...]]]></description>
			<content:encoded><![CDATA[<p>由于2016年年中<a href="http://tonybai.com/2017/01/03/2016-summary/">调换工作</a>的原因，对容器网络的研究中断过一段时间。随着当前项目对<a href="http://tonybai.com/tag/kubernetes">Kubernetes</a>应用的深入，我感觉之前对于<a href="http://tonybai.com/2016/01/15/understanding-container-networking-on-single-host/">容器网络的粗浅理解</a>已经不够了，容器网络成了摆在前面的“一道坎”。继续深入理解K8s网络、容器网络已经势在必行。而这篇文章就算是一个重新开始，也是对之前浅表理解的一个补充。</p>
<p>我还是先从<a href="https://www.docker.com/">Docker</a>容器网络入手，虽然Docker与Kubernetes采用了不同的网络模型：K8s是<a href="https://github.com/containernetworking/cni">Container Network Interface, CNI</a>模型，而Docker则采用的是<a href="https://github.com/docker/libnetwork/blob/master/docs/design.md">Container Network Model, CNM</a>模型。而要了解Docker容器网络，理解Linux Network Namespace是不可或缺的。在本文中我们将尝试理解Linux Network Namespace及相关Linux内核网络设备的概念，并手工模拟Docker容器网络模型的部分实现，包括<a href="http://tonybai.com/2016/01/15/understanding-container-networking-on-single-host/">单机容器网络</a>中的容器与主机连通、容器间连通以及<a href="http://tonybai.com/2016/01/18/understanding-binding-docker-container-ports-to-host/">端口映射</a>等。</p>
<h3>一、Docker的CNM网络模型</h3>
<p>Docker通过<a href="https://github.com/docker/libnetwork">libnetwork</a>实现了CNM网络模型。libnetwork设计doc中对CNM模型的简单诠释如下：</p>
<p><img src="http://tonybai.com/wp-content/uploads/docker-cnm-model.jpg" alt="img{512x368}" /></p>
<p>CNM模型有三个组件：</p>
<ul>
<li>Sandbox(沙盒)：每个沙盒包含一个容器网络栈(network stack)的配置，配置包括：容器的网口、路由表和DNS设置等。</li>
<li>Endpoint(端点)：通过Endpoint，沙盒可以被加入到一个Network里。</li>
<li>Network(网络)：一组能相互直接通信的Endpoints。</li>
</ul>
<p>光看这些，我们还很难将之与现实中的Docker容器联系起来，毕竟是抽象的模型不对应到实体，总有种漂浮的赶脚。文档中又给出了CNM模型在Linux上的参考实现技术，比如：沙盒的实现可以是一个<a href="https://en.wikipedia.org/wiki/Linux_namespaces#Network_.28net.29">Linux Network Namespace</a>；Endpoint可以是一对<a href="https://openvz.org/Virtual_Ethernet_device">VETH</a>；Network则可以用<a href="https://wiki.linuxfoundation.org/networking/bridge">Linux Bridge</a>或<a href="https://en.wikipedia.org/wiki/Virtual_Extensible_LAN">Vxlan</a>实现。</p>
<p>这些实现技术反倒是比较接地气。之前我们在使用Docker容器时，了解过Docker是用linux network namespace实现的容器网络隔离的。使用docker时，在物理主机或虚拟机上会有一个docker0的linux bridge，brctl show时能看到 docker0上“插上了”好多veth网络设备：</p>
<pre><code># ip link show
... ...
3: docker0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc noqueue state UP mode DEFAULT group default
    link/ether 02:42:30:11:98:ef brd ff:ff:ff:ff:ff:ff
19: veth4559467@if18: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default
    link/ether a6:14:99:52:78:35 brd ff:ff:ff:ff:ff:ff link-netnsid 3
... ...

$ brctl show
bridge name    bridge id        STP enabled    interfaces
... ...
docker0        8000.0242301198ef    no        veth4559467
</code></pre>
<p>模型与现实终于有点接驳了！下面我们将进一步深入对这些术语概念的理解。</p>
<h3>二、Linux Bridge、VETH和Network Namespace</h3>
<p><a href="https://wiki.linuxfoundation.org/networking/bridge">Linux Bridge</a>，即Linux网桥设备，是Linux提供的一种虚拟网络设备之一。其工作方式非常类似于物理的网络交换机设备。Linux Bridge可以工作在二层，也可以工作在三层，默认工作在二层。工作在二层时，可以在同一网络的不同主机间转发以太网报文；一旦你给一个Linux Bridge分配了IP地址，也就开启了该Bridge的三层工作模式。在Linux下，你可以用<a href="https://wiki.linuxfoundation.org/networking/iproute2">iproute2</a>工具包或brctl命令对Linux bridge进行管理。</p>
<p>VETH(Virtual Ethernet )是Linux提供的另外一种特殊的网络设备，中文称为虚拟网卡接口。它总是成对出现，要创建就创建一个pair。一个Pair中的veth就像一个网络线缆的两个端点，数据从一个端点进入，必然从另外一个端点流出。每个veth都可以被赋予IP地址，并参与三层网络路由过程。</p>
<p>关于Linux Bridge和VETH的具体工作原理，可以参考IBM developerWorks上的这篇文章《<a href="http://www.ibm.com/developerworks/cn/linux/1310_xiawc_networkdevice/">Linux 上的基础网络设备详解</a>》。</p>
<p>Network namespace，网络名字空间，允许你在Linux创建相互隔离的网络视图，每个网络名字空间都有独立的网络配置，比如：网络设备、路由表等。新建的网络名字空间与主机默认网络名字空间之间是隔离的。我们平时默认操作的是主机的默认网络名字空间。</p>
<p>概念总是抽象的，接下来我们将在一个模拟Docker容器网络的例子中看到这些Linux网络概念和网络设备到底是起到什么作用的以及是如何操作的。</p>
<h3>三、用Network namespace模拟Docker容器网络</h3>
<p>为了进一步了解network namespace、bridge和veth在docker容器网络中的角色和作用，我们来做一个demo：用network namespace模拟Docker容器网络，实际上Docker容器网络在linux上也是基于network namespace实现的，我们只是将其“自动化”的创建过程做成了“分解动作”，便于大家理解。</p>
<h4>1、环境</h4>
<p>我们在一台物理机上进行这个Demo实验。物理机安装了Ubuntu 16.04.1，内核版本：4.4.0-57-generic。Docker容器版本：</p>
<pre><code>Client:
 Version:      1.12.1
 API version:  1.24
 Go version:   go1.6.3
 Git commit:   23cf638
 Built:        Thu Aug 18 05:33:38 2016
 OS/Arch:      linux/amd64

Server:
 Version:      1.12.1
 API version:  1.24
 Go version:   go1.6.3
 Git commit:   23cf638
 Built:        Thu Aug 18 05:33:38 2016
 OS/Arch:      linux/amd64
</code></pre>
<p>另外，环境中需安装了<a href="https://wiki.linuxfoundation.org/networking/iproute2">iproute2</a>和brctl工具。</p>
<h4>2、拓扑</h4>
<p>我们来模拟一个拥有两个容器的容器桥接网络：</p>
<p><img src="http://tonybai.com/wp-content/uploads/linux-network-namespaces-1.png" alt="img{512x368}" /></p>
<p>对应的用手工搭建的模拟版本拓扑如下(由于在同一台主机，模拟版本采用172.16.0.0/16网段)：</p>
<p><img src="http://tonybai.com/wp-content/uploads/linux-network-namespaces-2.png" alt="img{512x368}" /></p>
<h4>3、创建步骤</h4>
<h5>a) 创建Container_ns1和Container_ns2 network namespace</h5>
<p>默认情况下，我们在Host上看到的都是default network namespace的视图。为了模拟容器网络，我们新建两个network namespace：</p>
<pre><code>sudo ip netns add Container_ns1
sudo ip netns add Container_ns2

$ sudo ip netns list
Container_ns2
Container_ns1
</code></pre>
<p>创建的ns也可以在/var/run/netns路径下看到：</p>
<pre><code>$ sudo ls /var/run/netns
Container_ns1  Container_ns2
</code></pre>
<p>我们探索一下新创建的ns的网络空间(通过ip netns exec命令可以在特定ns的内部执行相关程序，这个exec命令是至关重要的，后续还会发挥更大作用)：</p>
<pre><code>$ sudo ip netns exec Container_ns1 ip a
1: lo: &lt;LOOPBACK&gt; mtu 65536 qdisc noop state DOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

$ sudo ip netns exec Container_ns2 ip a
1: lo: &lt;LOOPBACK&gt; mtu 65536 qdisc noop state DOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

$ sudo ip netns exec Container_ns2 ip route

</code></pre>
<p>可以看到，新建的ns的网络设备只有一个loopback口，并且路由表为空。</p>
<h5>b) 创建MyDocker0 bridge</h5>
<p>我们在default network namespace下创建MyDocker0 linux bridge：</p>
<pre><code>$ sudo brctl addbr MyDocker0

$ brctl show
bridge name    bridge id        STP enabled    interfaces
MyDocker0        8000.000000000000    no
</code></pre>
<p>给MyDocker0分配ip地址并生效该设备，开启三层，为后续充当Gateway做准备：</p>
<pre><code>$ sudo ip addr add 172.16.1.254/16 dev MyDocker0
$ sudo ip link set dev MyDocker0 up
</code></pre>
<p>启用后，我们发现default network namespace的路由配置中增加了一条路由：</p>
<pre><code>$ route -n
内核 IP 路由表
目标            网关            子网掩码        标志  跃点   引用  使用 接口
0.0.0.0         10.11.36.1      0.0.0.0         UG    100    0        0 eno1
... ...
172.16.0.0      0.0.0.0         255.255.0.0     U     0      0        0 MyDocker0
... ...
</code></pre>
<h5>c) 创建VETH，连接两对network namespaces</h5>
<p>到目前为止，default ns与Container_ns1、Container_ns2之间还没有任何瓜葛。接下来就是见证奇迹的时刻了。我们通过veth pair建立起多个ns之间的联系：</p>
<p>创建连接default ns与Container_ns1之间的veth pair &#8211; veth1和veth1p：</p>
<pre><code>$sudo ip link add veth1 type veth peer name veth1p

$sudo ip -d link show
... ...
21: veth1p@veth1: &lt;BROADCAST,MULTICAST,M-DOWN&gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 66:6d:e7:75:3f:43 brd ff:ff:ff:ff:ff:ff promiscuity 0
    veth addrgenmode eui64
22: veth1@veth1p: &lt;BROADCAST,MULTICAST,M-DOWN&gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 56:cd:bb:f2:10:3f brd ff:ff:ff:ff:ff:ff promiscuity 0
    veth addrgenmode eui64
... ...
</code></pre>
<p>将veth1“插到”MyDocker0这个bridge上：</p>
<pre><code>$ sudo brctl addif MyDocker0 veth1
$ sudo ip link set veth1 up
$ brctl show
bridge name    bridge id        STP enabled    interfaces
MyDocker0        8000.56cdbbf2103f    no        veth1
</code></pre>
<p>将veth1p“放入”Container_ns1中：</p>
<pre><code>$ sudo ip link set veth1p netns Container_ns1

$ sudo ip netns exec Container_ns1 ip a
1: lo: &lt;LOOPBACK&gt; mtu 65536 qdisc noop state DOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
21: veth1p@if22: &lt;BROADCAST,MULTICAST&gt; mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 66:6d:e7:75:3f:43 brd ff:ff:ff:ff:ff:ff link-netnsid 0
</code></pre>
<p>这时，你在default ns中将看不到veth1p这个虚拟网络设备了。按照上面拓扑，位于Container_ns1中的veth应该更名为eth0：</p>
<pre><code>$ sudo ip netns exec Container_ns1 ip link set veth1p name eth0
$ sudo ip netns exec Container_ns1 ip a
1: lo: &lt;LOOPBACK&gt; mtu 65536 qdisc noop state DOWN group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
21: eth0@if22: &lt;BROADCAST,MULTICAST&gt; mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 66:6d:e7:75:3f:43 brd ff:ff:ff:ff:ff:ff link-netnsid 0
</code></pre>
<p>将Container_ns1中的eth0生效并配置IP地址：</p>
<pre><code>$ sudo ip netns exec Container_ns1 ip link set eth0 up
$ sudo ip netns exec Container_ns1 ip addr add 172.16.1.1/16 dev eth0
</code></pre>
<p>赋予IP地址后，自动生成一条直连路由：</p>
<pre><code>sudo ip netns exec Container_ns1 ip route
172.16.0.0/16 dev eth0  proto kernel  scope link  src 172.16.1.1
</code></pre>
<p>现在在Container_ns1下可以ping通MyDocker0了，但由于没有其他路由，包括默认路由，ping其他地址还是不通的（比如：docker0的地址：172.17.0.1）：</p>
<pre><code>$ sudo ip netns exec Container_ns1 ping -c 3 172.16.1.254
PING 172.16.1.254 (172.16.1.254) 56(84) bytes of data.
64 bytes from 172.16.1.254: icmp_seq=1 ttl=64 time=0.074 ms
64 bytes from 172.16.1.254: icmp_seq=2 ttl=64 time=0.064 ms
64 bytes from 172.16.1.254: icmp_seq=3 ttl=64 time=0.068 ms

--- 172.16.1.254 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 0.064/0.068/0.074/0.010 ms

$ sudo ip netns exec Container_ns1 ping -c 3 172.17.0.1
connect: Network is unreachable

</code></pre>
<p>我们再给Container_ns1添加一条默认路由，让其能ping通物理主机上的其他网络设备或其他ns空间中的网络设备地址：</p>
<pre><code>$ sudo ip netns exec Container_ns1 ip route add default via 172.16.1.254
$ sudo ip netns exec Container_ns1 ip route
default via 172.16.1.254 dev eth0
172.16.0.0/16 dev eth0  proto kernel  scope link  src 172.16.1.1

$ sudo ip netns exec Container_ns1 ping -c 3 172.17.0.1
PING 172.17.0.1 (172.17.0.1) 56(84) bytes of data.
64 bytes from 172.17.0.1: icmp_seq=1 ttl=64 time=0.068 ms
64 bytes from 172.17.0.1: icmp_seq=2 ttl=64 time=0.076 ms
64 bytes from 172.17.0.1: icmp_seq=3 ttl=64 time=0.069 ms

--- 172.17.0.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1999ms
rtt min/avg/max/mdev = 0.068/0.071/0.076/0.003 ms

</code></pre>
<p>不过这时候，如果想在Container_ns1中ping通物理主机之外的地址，比如:google.com，那还是不通的。为什么呢？因为ping的icmp的包的源地址没有做snat（docker是通过设置<a href="https://www.netfilter.org/">iptables</a>规则实现的），导致出去的以172.16.1.1为源地址的包“有去无回”了^0^。</p>
<p>接下来，我们按照上述步骤，再创建连接default ns与Container_ns2之间的veth pair &#8211; veth2和veth2p，由于步骤相同，这里就不列出那么多信息了，只列出关键操作：</p>
<pre><code>$ sudo ip link add veth2 type veth peer name veth2p
$ sudo brctl addif MyDocker0 veth2
$ sudo ip link set veth2 up
$ sudo ip link set veth2p netns Container_ns2
$ sudo ip netns exec Container_ns2 ip link set veth2p name eth0
$ sudo ip netns exec Container_ns2 ip link set eth0 up
$ sudo ip netns exec Container_ns2 ip addr add 172.16.1.2/16 dev eth0
$ sudo ip netns exec Container_ns2 ip route add default via 172.16.1.254
</code></pre>
<p>至此，模拟创建告一段落！两个ns之间以及它们与default ns之间连通了！</p>
<pre><code>$ sudo ip netns exec Container_ns2 ping -c 3 172.16.1.1
PING 172.16.1.1 (172.16.1.1) 56(84) bytes of data.
64 bytes from 172.16.1.1: icmp_seq=1 ttl=64 time=0.101 ms
64 bytes from 172.16.1.1: icmp_seq=2 ttl=64 time=0.083 ms
64 bytes from 172.16.1.1: icmp_seq=3 ttl=64 time=0.087 ms

--- 172.16.1.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 0.083/0.090/0.101/0.010 ms

$ sudo ip netns exec Container_ns1 ping -c 3 172.16.1.2
PING 172.16.1.2 (172.16.1.2) 56(84) bytes of data.
64 bytes from 172.16.1.2: icmp_seq=1 ttl=64 time=0.053 ms
64 bytes from 172.16.1.2: icmp_seq=2 ttl=64 time=0.092 ms
64 bytes from 172.16.1.2: icmp_seq=3 ttl=64 time=0.089 ms

--- 172.16.1.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1999ms
rtt min/avg/max/mdev = 0.053/0.078/0.092/0.017 ms
</code></pre>
<p>当然此时两个ns之间连通，主要还是通过直连网络，实质上是MyDocker0在二层起到的作用。以在Container_ns1中ping Container_ns2的eth0地址为例：</p>
<p>Container_ns1此时的路由表：</p>
<pre><code>$ sudo ip netns exec Container_ns1 ip route
default via 172.16.1.254 dev eth0
172.16.0.0/16 dev eth0  proto kernel  scope link  src 172.16.1.1
</code></pre>
<p>ping 172.16.1.2执行后，根据路由表，将首先匹配到直连网络（第二条），即无需gateway转发便可以直接将数据包送达。arp查询后（要么从arp cache中找到，要么在MyDocker0这个二层交换机中泛洪查询）获得172.16.1.2的mac地址。ip包的目的ip填写172.16.1.2，二层数据帧封包将目的mac填写为刚刚查到的mac地址，通过eth0(172.16.1.1)发送出去。eth0实际上是一个veth pair，另外一端“插”在MyDocker0这个交换机上，因此这一过程就是一个标准的二层交换机的数据报文交换过程, MyDocker0相当于从交换机上的一个端口收到以太帧数据，并将数据从另外一个端口发出去。ping应答包亦如此。</p>
<p>而如果是在Container_ns1中ping某个docker container的地址，比如172.17.0.2。当ping执行后，根据Container_ns1下的路由表，没有匹配到直连网络，只能通过default路由将数据包发给Gateway: 172.16.1.254。虽然都是MyDocker0接收数据，但这次更类似于“数据被直接发到 Bridge 上，而不是Bridge从一个端口接收(这块儿与我之前的文章中的理解稍有差异)”。二层的目的mac地址填写的是gateway 172.16.1.254自己的mac地址（Bridge的mac地址），此时的MyDocker0更像是一块普通网卡的角色，工作在三层。MyDocker0收到数据包后，发现并非是发给自己的ip包，通过主机路由表找到直连链路路由，MyDocker0将数据包Forward到docker0上（封装的二层数据包的目的MAC地址为docker0的mac地址）。此时的docker0也是一种“网卡”的角色，由于目的ip依然不是docker0自身，因此docker0也会继续这一转发流程。通过traceroute可以印证这一过程：</p>
<pre><code>$ sudo ip netns exec Container_ns1  traceroute 172.17.0.2
traceroute to 172.17.0.2 (172.17.0.2), 30 hops max, 60 byte packets
 1  172.16.1.254 (172.16.1.254)  0.082 ms  0.023 ms  0.019 ms
 2  172.17.0.2 (172.17.0.2)  0.054 ms  0.034 ms  0.029 ms

$ sudo ip netns exec Container_ns1  ping -c 3 172.17.0.2
PING 172.17.0.2 (172.17.0.2) 56(84) bytes of data.
64 bytes from 172.17.0.2: icmp_seq=1 ttl=63 time=0.084 ms
64 bytes from 172.17.0.2: icmp_seq=2 ttl=63 time=0.101 ms
64 bytes from 172.17.0.2: icmp_seq=3 ttl=63 time=0.098 ms

--- 172.17.0.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 0.084/0.094/0.101/0.010 ms
</code></pre>
<p>现在，你应该大致了解docker engine在创建单机容器网络时都在背后做了哪些手脚了吧（当然，这里只是简单模拟，docker实际做的要比这复杂许多）。</p>
<h3>四、基于userland proxy的容器端口映射的模拟</h3>
<p><a href="http://tonybai.com/2016/01/18/understanding-binding-docker-container-ports-to-host/">端口映射</a>让位于容器中的service可以将服务范围扩展到主机之外，比如：一个运行于container中的<a href="http://tonybai.com/2016/11/22/deploy-nginx-service-for-the-services-in-kubernetes-cluster/">nginx</a>可以通过宿主机的9091端口对外提供http server服务：</p>
<pre><code>$ sudo docker run -d -p 9091:80 nginx:latest
8eef60e3d7b48140c20b11424ee8931be25bc47b5233aa42550efabd5730ac2f

$ curl 10.11.36.15:9091
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;title&gt;Welcome to nginx!&lt;/title&gt;
&lt;style&gt;
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h1&gt;Welcome to nginx!&lt;/h1&gt;
&lt;p&gt;If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.&lt;/p&gt;

&lt;p&gt;For online documentation and support please refer to
&lt;a href="http://nginx.org/"&gt;nginx.org&lt;/a&gt;.&lt;br/&gt;
Commercial support is available at
&lt;a href="http://nginx.com/"&gt;nginx.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thank you for using nginx.&lt;/em&gt;&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>容器的端口映射实际是通过docker engine的docker proxy功能实现的。默认情况下，docker engine(截至docker 1.12.1版本)采用userland proxy(&#8211;userland-proxy=true)为每个expose端口的容器启动一个proxy实例来做端口流量转发：</p>
<pre><code>$ ps -ef|grep docker-proxy
root     26246  6228  0 16:18 ?        00:00:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 9091 -container-ip 172.17.0.2 -container-port 80
</code></pre>
<p>docker-proxy实际上就是在default ns和container ns之间转发流量而已。我们完全可以模拟这一过程。</p>
<p>我们创建一个fileserver demo：</p>
<pre><code>//testfileserver.go
package main

import "net/http"

func main() {
    http.ListenAndServe(":8080", http.FileServer(http.Dir(".")))
}
</code></pre>
<p>我们在Container_ns1下启动这个Fileserver service:</p>
<pre><code>$ sudo ip netns exec Container_ns1 ./testfileserver

$ sudo ip netns exec Container_ns1 lsof -i tcp:8080
COMMAND    PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
testfiles 3605 root    3u  IPv4 297022      0t0  TCP *:http-alt (LISTEN)
</code></pre>
<p>可以看到在Container_ns1下面，8080已经被testfileserver监听，不过在default ns下，8080端口依旧是avaiable的。</p>
<p>接下来，我们在default ns下创建一个简易的proxy：</p>
<pre><code>//proxy.go
... ...

var (
    host          string
    port          string
    container     string
    containerport string
)

func main() {
    flag.StringVar(&amp;host, "host", "0.0.0.0", "host addr")
    flag.StringVar(&amp;port, "port", "", "host port")
    flag.StringVar(&amp;container, "container", "", "container addr")
    flag.StringVar(&amp;containerport, "containerport", "8080", "container port")

    flag.Parse()

    fmt.Printf("%s\n%s\n%s\n%s", host, port, container, containerport)

    ln, err := net.Listen("tcp", host+":"+port)
    if err != nil {
        // handle error
        log.Println("listen error:", err)
        return
    }
    log.Println("listen ok")

    for {
        conn, err := ln.Accept()
        if err != nil {
            // handle error
            log.Println("accept error:", err)
            continue
        }
        log.Println("accept conn", conn)
        go handleConnection(conn)
    }
}

func handleConnection(conn net.Conn) {
    cli, err := net.Dial("tcp", container+":"+containerport)
    if err != nil {
        log.Println("dial error:", err)
        return
    }
    log.Println("dial ", container+":"+containerport, " ok")

    go io.Copy(conn, cli)
    _, err = io.Copy(cli, conn)
    fmt.Println("communication over: error:", err)
}

</code></pre>
<p>在default ns下执行：</p>
<pre><code>./proxy -host 0.0.0.0 -port 9090 -container 172.16.1.1 -containerport 8080
0.0.0.0
9090
172.16.1.1
80802017/01/11 17:26:10 listen ok
</code></pre>
<p>我们http get一下宿主机的9090端口：</p>
<pre><code>$curl 10.11.36.15:9090
&lt;pre&gt;
&lt;a href="proxy"&gt;proxy&lt;/a&gt;
&lt;a href="proxy.go"&gt;proxy.go&lt;/a&gt;
&lt;a href="testfileserver"&gt;testfileserver&lt;/a&gt;
&lt;a href="testfileserver.go"&gt;testfileserver.go&lt;/a&gt;
&lt;/pre&gt;

</code></pre>
<p>成功获得file list！</p>
<p>proxy的输出日志：</p>
<pre><code>2017/01/11 17:26:16 accept conn &amp;{{0xc4200560e0}}
2017/01/11 17:26:16 dial  172.16.1.1:8080  ok
communication over: error:&lt;nil&gt;
</code></pre>
<p>由于每个做端口映射的Container都要启动至少一个docker proxy与之配合，一旦运行的container增多，那么docker proxy对资源的消耗将是大大的。因此docker engine在docker 1.6之后（好像是这个版本）提供了基于iptables的端口映射机制，无需再启动docker proxy process了。我们只需修改一下docker engine的启动配置即可：</p>
<p>在使用systemd init system的系统中如果为docker engine配置&#8211;userland-proxy=false，可以参考《<a href="http://tonybai.com/2016/12/27/when-docker-meets-systemd">当Docker遇到systemd</a>》这篇文章。</p>
<p>由于这个与network namespace关系不大，后续单独理解^0^。</p>
<h3>六、参考资料</h3>
<p>1、《<a href="https://book.douban.com/subject/26929989/">Docker networking cookbook</a>》<br />
2、《<a href="https://book.douban.com/subject/26631435/">Docker cookbook</a>》</p>
<p style='text-align:left'>&copy; 2017, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2017/01/11/understanding-linux-network-namespace-for-docker-network/feed/</wfw:commentRss>
		<slash:comments>7</slash:comments>
		</item>
		<item>
		<title>为Kubernetes集群中服务部署Nginx入口服务</title>
		<link>https://tonybai.com/2016/11/22/deploy-nginx-service-for-the-services-in-kubernetes-cluster/</link>
		<comments>https://tonybai.com/2016/11/22/deploy-nginx-service-for-the-services-in-kubernetes-cluster/#comments</comments>
		<pubDate>Tue, 22 Nov 2016 05:49:28 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[Ceph]]></category>
		<category><![CDATA[clusterip]]></category>
		<category><![CDATA[deployment]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[image]]></category>
		<category><![CDATA[iptables]]></category>
		<category><![CDATA[k8s]]></category>
		<category><![CDATA[kubectl]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[loadbalance]]></category>
		<category><![CDATA[nginx]]></category>
		<category><![CDATA[nodeport]]></category>
		<category><![CDATA[PaaS]]></category>
		<category><![CDATA[pod]]></category>
		<category><![CDATA[rbd]]></category>
		<category><![CDATA[registry]]></category>
		<category><![CDATA[reverseproxy]]></category>
		<category><![CDATA[Service]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[vip]]></category>
		<category><![CDATA[反向代理]]></category>
		<category><![CDATA[负载均衡]]></category>
		<category><![CDATA[镜像库]]></category>
		<category><![CDATA[阿里云]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=2072</guid>
		<description><![CDATA[这段日子，一直在搞与Kubernetes有关的东东：像什么Kubernetes集群搭建、DNS插件安装和配置、集成Ceph RBD持久卷、Private Registry镜像库访问等，这些都缘于正在开发的一个类PaaS小平台的需要：“平台虽小，五脏俱全”。整个平台由Kubernetes集群承载，对于K8s集群内部的Service来说，目前还欠缺一个服务入口。之前的《Kubernetes集群中的Nginx配置热更新方案》一文实际上就是入口方案设计的一个前奏，而本文则是说明一下Nginx入口服务部署设计和实施过程中遇到的一些坑。 一、Nginx入口方案简述 Nginx作为集群入口服务，从功能上说，一般都是充当反向代理和负载均衡的角色。在我们这里它更多是用于反向代理，因为负载均衡的事情“移交”给了K8s去实现了。k8s通过ClusterIP- 一种VIP机制，默认基于iptables的负载分担实现服务请求的负载均衡（如iptable nat table的规则：-m statistic &#8211;mode random &#8211;probability 0.33332999982），查看iptables nat链的rules，可以看到如下样例： # iptables -t nat -nL ... ... Chain KUBE-SVC-UQG6736T32JE3S7H (2 references) target prot opt source destination KUBE-SEP-Z7UQLD332S673VAF all -- 0.0.0.0/0 0.0.0.0/0 /* default/nginx-kit: */ statistic mode random probability 0.50000000000 KUBE-SEP-TWOIACCAJCPK3HWO all -- 0.0.0.0/0 0.0.0.0/0 /* default/nginx-kit: */ ... .. 接下来，我们简单说说我们的Nginx入口方案。事先声明：这绝对不是一个理想的方案，因为它还有诸多缺陷，只是在目前平台需求上下文和资源的约束前提下，它可以作为我们的一个可用的过渡方案，方案示意图如下： Nginx以Kubernetes [...]]]></description>
			<content:encoded><![CDATA[<p>这段日子，一直在搞与<a href="http://tonybai.com/tag/kubernetes">Kubernetes</a>有关的东东：像什么<a href="http://tonybai.com/2016/10/18/learn-how-to-install-kubernetes-on-ubuntu/">Kubernetes集群搭建</a>、<a href="http://tonybai.com/2016/10/23/install-dns-addon-for-k8s/">DNS插件安装和配置</a>、<a href="http://tonybai.com/2016/11/07/integrate-kubernetes-with-ceph-rbd/">集成Ceph RBD持久卷</a>、<a href="http://tonybai.com/2016/11/16/how-to-pull-images-from-private-registry-on-kubernetes-cluster/">Private Registry镜像库访问</a>等，这些都缘于正在开发的一个类<a href="https://en.wikipedia.org/wiki/Platform_as_a_service">PaaS</a>小平台的需要：“平台虽小，五脏俱全”。整个平台由Kubernetes集群承载，对于K8s集群内部的Service来说，目前还欠缺一个服务入口。之前的《<a href="http://tonybai.com/2016/11/17/nginx-config-hot-reloading-approach-for-kubernetes-cluster/">Kubernetes集群中的Nginx配置热更新方案</a>》一文实际上就是入口方案设计的一个前奏，而本文则是说明一下Nginx入口服务部署设计和实施过程中遇到的一些坑。</p>
<h4>一、Nginx入口方案简述</h4>
<p><a href="http://tonybai.com/tag/nginx">Nginx</a>作为集群入口服务，从功能上说，一般都是充当反向代理和负载均衡的角色。在我们这里它更多是用于反向代理，因为负载均衡的事情“移交”给了K8s去实现了。k8s通过ClusterIP- 一种VIP机制，默认<a href="http://kubernetes.io/docs/user-guide/debugging-services/#is-the-kube-proxy-working">基于iptables的负载分担</a>实现服务请求的负载均衡（如iptable nat table的规则：-m statistic &#8211;mode random &#8211;probability 0.33332999982），查看iptables nat链的rules，可以看到如下样例：</p>
<pre><code># iptables -t nat -nL
... ...
Chain KUBE-SVC-UQG6736T32JE3S7H (2 references)
target     prot opt source               destination
KUBE-SEP-Z7UQLD332S673VAF  all  --  0.0.0.0/0            0.0.0.0/0            /* default/nginx-kit: */ statistic mode random probability 0.50000000000
KUBE-SEP-TWOIACCAJCPK3HWO  all  --  0.0.0.0/0            0.0.0.0/0            /* default/nginx-kit: */
... ..
</code></pre>
<p>接下来，我们简单说说我们的Nginx入口方案。事先声明：这绝对不是一个理想的方案，因为它还有诸多缺陷，只是在目前平台需求上下文和资源的约束前提下，它可以作为我们的一个可用的过渡方案，方案示意图如下：</p>
<p><img src="http://tonybai.com/wp-content/uploads/nginx-reverse-proxy-for-k8s.png" alt="img{512x368}" /></p>
<ul>
<li>Nginx以Kubernetes service的形式运行于K8s cluster内部，并限制只能被K8s调度到带有label: role=entry的Node上；</li>
<li>最外层，通过DNS域名的轮询机制，实现用户请求在Node这一层上的“负载均衡”；</li>
<li>访问某个NodeIP:NodePort的请求，被转发到Nginx ClusterIP: Port，并通过iptables nat的负载机制，分发到Nginx service的多个real endpoints上；</li>
<li>位于real endpoint上的Nginx程序处理用户请求，并根据配置，将请求proxy_pass到后端服务的ClusterIP:Port上，并最终由k8s实现将请求均衡分发到后端服务的endpoint。</li>
</ul>
<h4>二、Nginx入口服务部署</h4>
<p>部署前，我们先来给运行Nginx Pod的Node打label：</p>
<pre><code># kubectl label node/10.47.136.60 role=entry
node "10.47.136.60" labeled

# kubectl label node/10.47.136.60 role=entry
node "10.47.136.60" labeled

# kubectl get nodes --show-labels
NAME            STATUS    AGE       LABELS
10.46.181.146   Ready     39d       beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=10.46.181.146,role=entry,zone=ceph
10.47.136.60    Ready     39d       beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=10.47.136.60,role=entry,zone=ceph
</code></pre>
<p>在<a href="http://tonybai.com/2016/11/17/nginx-config-hot-reloading-approach-for-kubernetes-cluster/">Nginx配置热加载方案</a>一文中，我们提到一个nginx pod中包含三个Container：nginx、nginx-conf-generator和init container，Nginx service的yaml示例如下：</p>
<pre><code>//nginx-kit.yaml

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: nginx-kit
spec:
  replicas: 2
  template:
    metadata:
      labels:
        run: nginx-kit
      annotations:
        pod.beta.kubernetes.io/init-containers: '[
          {
               "name": "nginx-kit-init-container",
               "image": "registry.cn-beijing.aliyuncs.com/xxxx/nginx-conf-generator",
               "imagePullPolicy": "IfNotPresent",
               "command": ["/root/conf-generator/nginx-conf-gen", "-mode", "gen-once"],
               "volumeMounts": [
                   {
                      "name": "conf-volume",
                      "mountPath": "/etc/nginx/conf.d"
                   }
               ]
          }
        ]'
    spec:
      containers:
      - name: nginx-conf-generator
        volumeMounts:
        - mountPath: /etc/nginx/conf.d
          name: conf-volume
        image: registry.cn-beijing.aliyuncs.com/xxxx/nginx-conf-generator:latest
        imagePullPolicy: IfNotPresent
      - name: xxxx-nginx
        volumeMounts:
        - mountPath: /etc/nginx/conf.d
          name: conf-volume
        image: registry.cn-hangzhou.aliyuncs.com/xxxx/nginx:latest
        imagePullPolicy: IfNotPresent
        command: ["/home/auto-reload-nginx.sh"]
        ports:
        - containerPort: 80
      volumes:
      - name: conf-volume
        emptyDir: {}
      nodeSelector:
        role: entry
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-kit
  labels:
    run: nginx-kit
spec:
  type: NodePort
  ports:
  - port: 80
    nodePort: 28888
    protocol: TCP
  selector:
    run: nginx-kit
</code></pre>
<p>关于这个yaml，有几点我们是必须要说说的：</p>
<h5>1、关于init container</h5>
<p>通过上述yaml文件内容，我们可以看到init container和nginx-conf-generator container都是基于同一镜像创建的，只是工作mode不同罢了。在deployment描述文件中，init container的描述需要放在deployment.spec.template.metadata下面，而不是deployment的metadata下面。如果按照后者编写，那么init container将不会被创建和启动，nginx container启动后也就会提示：找不到”default.conf”。</p>
<p>另外，虽然源自同一个image，但init container启动时却提示在$PATH里找不到名为”-mode”的可执行程序，显然init container中的ENTRYPOINT并不起作用，nginx-conf-generator的Dockerfile节选如下：</p>
<pre><code>//Dockerfile
From ubuntu:14.04
... ...
ENTRYPOINT ["/root/conf-generator/nginx-conf-gen"]
</code></pre>
<p>为此我们在init container的”command”命令参数中增加了可执行程序全路径以供container执行：</p>
<pre><code> "command" : ["/root/conf-generator/nginx-conf-gen", "-mode", "gen-once"],
</code></pre>
<p>最后，通过上面yaml文件创建nginx-kit服务依旧要用kubectl apply，而不是kubectl create，否则init container不会被理会。</p>
<h5>2、关于nginx conf模板</h5>
<p>由于种种原因，当前我们是通过server host的location path来映射后端cluster中的不同Service的，nginx default.conf模板如下：</p>
<pre><code>server {
    listen 80;
    #server_name opp.neusoft.com;

    {{range .}}
    location {{.Path}} {
        proxy_pass http://{{.ClusterIP}}:{{.Port}}/;
        proxy_redirect off;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
    {{end}}

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}
</code></pre>
<p>这里要注意的是proxy_pass directive后面值的写法，如果你选择这样写：</p>
<pre><code>proxy_pass http://{{.ClusterIP}}:{{.Port}};
</code></pre>
<p>那么当访问某个路径时，比如：localhost/volume/api/v1/pools时，nginx后端的Service收到的url访问路径将是：/volume/api/v1/pools，volume这个location path并不能被去除，后端的Service在做路由匹配时基本都是会出错的。fix的方法是赋予proxy_pass directive下面这样的值：</p>
<pre><code>proxy_pass http://{{.ClusterIP}}:{{.Port}}/;
</code></pre>
<p>没错，在最后加上一个”/”，这样nginx所反向代理的Service将会收到/api/v1/pools这样的访问URl路径。</p>
<p style='text-align:left'>&copy; 2016, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2016/11/22/deploy-nginx-service-for-the-services-in-kubernetes-cluster/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>理解Docker跨多主机容器网络</title>
		<link>https://tonybai.com/2016/02/15/understanding-docker-multi-host-networking/</link>
		<comments>https://tonybai.com/2016/02/15/understanding-docker-multi-host-networking/#comments</comments>
		<pubDate>Mon, 15 Feb 2016 09:04:39 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[aufs]]></category>
		<category><![CDATA[bridge]]></category>
		<category><![CDATA[cgroups]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[coreos]]></category>
		<category><![CDATA[DevOps]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[ethtool]]></category>
		<category><![CDATA[firewall]]></category>
		<category><![CDATA[flannel]]></category>
		<category><![CDATA[gateway]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[ifconfig]]></category>
		<category><![CDATA[iproute2]]></category>
		<category><![CDATA[iptables]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[mesos]]></category>
		<category><![CDATA[NAT]]></category>
		<category><![CDATA[netns]]></category>
		<category><![CDATA[netperf]]></category>
		<category><![CDATA[netstat]]></category>
		<category><![CDATA[networking]]></category>
		<category><![CDATA[overlay]]></category>
		<category><![CDATA[overlaynetwork]]></category>
		<category><![CDATA[OVS]]></category>
		<category><![CDATA[rancher]]></category>
		<category><![CDATA[Router]]></category>
		<category><![CDATA[SDN]]></category>
		<category><![CDATA[switch]]></category>
		<category><![CDATA[tcpip]]></category>
		<category><![CDATA[telnet]]></category>
		<category><![CDATA[traceroute]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[VNI]]></category>
		<category><![CDATA[VTEP]]></category>
		<category><![CDATA[VXLAN]]></category>
		<category><![CDATA[交换机]]></category>
		<category><![CDATA[内核]]></category>
		<category><![CDATA[容器]]></category>
		<category><![CDATA[网关]]></category>
		<category><![CDATA[网桥]]></category>
		<category><![CDATA[网络]]></category>
		<category><![CDATA[覆盖网]]></category>
		<category><![CDATA[路由器]]></category>
		<category><![CDATA[软件定义网络]]></category>
		<category><![CDATA[防火墙]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1941</guid>
		<description><![CDATA[在Docker 1.9 出世前，跨多主机的容器通信方案大致有如下三种： 1、端口映射 将宿主机A的端口P映射到容器C的网络空间监听的端口P&#8217;上，仅提供四层及以上应用和服务使用。这样其他主机上的容器通过访问宿主机A的端口P实 现与容器C的通信。显然这个方案的应用场景很有局限。 2、将物理网卡桥接到虚拟网桥，使得容器与宿主机配置在同一网段下 在各个宿主机上都建立一个新虚拟网桥设备br0，将各自物理网卡eth0桥接br0上，eth0的IP地址赋给br0；同时修改Docker daemon的DOCKER_OPTS，设置-b=br0（替代docker0），并限制Container IP地址的分配范围为同物理段地址（&#8211;fixed-cidr）。重启各个主机的Docker Daemon后，处于与宿主机在同一网段的Docker容器就可以实现跨主机访问了。这个方案同样存在局限和扩展性差的问题：比如需将物理网段的地址划分 成小块，分布到各个主机上，防止IP冲突；子网划分依赖物理交换机设置；Docker容器的主机地址空间大小依赖物理网络划分等。 3、使用第三方的基于SDN的方案：比如 使用Open vSwitch &#8211; OVS 或CoreOS的Flannel 等。 关于这些第三方方案的细节大家可以参考O&#8217;Reilly的《Docker Cookbook》 一书。 Docker在1.9版本中给大家带来了一种原生的跨多主机容器网络的解决方案，该方案的实质是采用了基于VXLAN 的覆盖网技术。方案的使用有一些前提条件： 1、Linux Kernel版本 >= 3.16； 2、需要一个外部Key-value Store（官方例子中使用的是consul）； 3、各物理主机上的Docker Daemon需要一些特定的启动参数； 4、物理主机允许某些特定TCP/UDP端口可用。 本文将带着大家一起利用Docker 1.9.1创建一个跨多主机容器网络，并分析基于该网络的容器间通信原理。 一、实验环境建立 1、升级Linux Kernel 由于实验环境采用的是Ubuntu 14.04 server amd64，其kernel版本不能满足建立跨多主机容器网络要求，因此需要对内核版本进行升级。在Ubuntu的内核站点 下载3.16.7 utopic内核 的三个文件： linux-headers-3.16.7-031607_3.16.7-031607.201410301735_all.deb linux-image-3.16.7-031607-generic_3.16.7-031607.201410301735_amd64.deb linux-headers-3.16.7-031607-generic_3.16.7-031607.201410301735_amd64.deb 在本地执行下面命令安装： sudo dpkg -i linux-headers-3.16.7-*.deb linux-image-3.16.7-*.deb 需要注意的是：kernel [...]]]></description>
			<content:encoded><![CDATA[<p>在<a href="https://blog.docker.com/tag/docker-1-9/">Docker 1.9</a> 出世前，跨多主机的容器通信方案大致有如下三种：</p>
<p>1、<a href="http://tonybai.com/2016/01/18/understanding-binding-docker-container-ports-to-host/">端口映射</a></p>
<p>将宿主机A的端口P映射到容器C的网络空间监听的端口P&#8217;上，仅提供四层及以上应用和服务使用。这样其他主机上的容器通过访问宿主机A的端口P实 现与容器C的通信。显然这个方案的应用场景很有局限。</p>
<p>2、将物理网卡桥接到虚拟网桥，使得容器与宿主机配置在同一网段下</p>
<p>在各个宿主机上都建立一个新虚拟网桥设备br0，将各自物理网卡eth0桥接br0上，eth0的IP地址赋给br0；同时修改Docker daemon的DOCKER_OPTS，设置-b=br0（替代docker0），并限制Container IP地址的分配范围为同物理段地址（&#8211;fixed-cidr）。重启各个主机的Docker Daemon后，处于与宿主机在同一网段的Docker容器就可以实现跨主机访问了。这个方案同样存在局限和扩展性差的问题：比如需将物理网段的地址划分 成小块，分布到各个主机上，防止IP冲突；子网划分依赖物理交换机设置；Docker容器的主机地址空间大小依赖物理网络划分等。</p>
<p>3、使用第三方的基于<a href="https://en.wikipedia.org/wiki/Software-defined_networking">SDN</a>的方案：比如 使用<a href="http://openvswitch.org/">Open vSwitch &#8211; OVS</a> 或<a href="https://coreos.com/">CoreOS</a>的<a href="https://github.com/coreos/flannel">Flannel</a> 等。</p>
<p>关于这些第三方方案的细节大家可以参考O&#8217;Reilly的《<a href="http://book.douban.com/subject/26631435/">Docker Cookbook</a>》 一书。</p>
<p>Docker在1.9版本中给大家带来了一种原生的跨多主机容器网络的解决方案，该方案的实质是采用了基于<a href="https://datatracker.ietf.org/doc/rfc7348">VXLAN</a> 的覆盖网技术。方案的使用有一些前提条件：</p>
<p>1、Linux Kernel版本 >= 3.16；<br />
2、需要一个外部Key-value Store（官方例子中使用的是<a href="https://en.wikipedia.org/wiki/Software-defined_networking">consul</a>）；<br />
3、各物理主机上的Docker Daemon需要一些特定的启动参数；<br />
4、物理主机允许某些特定TCP/UDP端口可用。</p>
<p>本文将带着大家一起利用Docker 1.9.1创建一个跨多主机容器网络，并分析基于该网络的容器间通信原理。</p>
<h3>一、实验环境建立</h3>
<h4>1、升级Linux Kernel</h4>
<p>由于实验环境采用的是Ubuntu 14.04 server amd64，其kernel版本不能满足建立跨多主机容器网络要求，因此需要对内核版本进行升级。在<a href="http://kernel.ubuntu.com/~kernel-ppa/mainline/">Ubuntu的内核站点</a> 下载<a href="http://kernel.ubuntu.com/~kernel-ppa/mainline/v3.16.7-utopic/">3.16.7 utopic内核</a> 的三个文件：</p>
<pre><code>linux-headers-3.16.7-031607_3.16.7-031607.201410301735_all.deb
linux-image-3.16.7-031607-generic_3.16.7-031607.201410301735_amd64.deb
linux-headers-3.16.7-031607-generic_3.16.7-031607.201410301735_amd64.deb
</code></pre>
<p>在本地执行下面命令安装：</p>
<pre><code>sudo dpkg -i linux-headers-3.16.7-*.deb linux-image-3.16.7-*.deb
</code></pre>
<p>需要注意的是：kernel mainline上的3.16.7内核没有带linux-image-extra，也就没有了<a href="https://en.wikipedia.org/wiki/Aufs">aufs</a> 的驱动，因此Docker Daemon将不支持默认的存储驱动：&#8211;storage-driver=aufs，我们需要将storage driver更换为<a href="http://en.wikipedia.org/wiki/Device_mapper">devicemapper</a>。</p>
<p>内核升级是一个有风险的操作，并且是否能升级成功还要看点“运气”：我的两台刀片服务器，就是一台升级成功一台升级失败（一直报网卡问题）。</p>
<h4>2、升级Docker到1.9.1版本</h4>
<p>从国内下载Docker官方的安装包比较慢，这里利用<a href="http://get.daocloud.io/#install-docker">daocloud.io提供的方法</a> 快速安装Docker最新版本：</p>
<pre><code>$ curl -sSL https://get.daocloud.io/docker | sh
</code></pre>
<h4>3、拓扑</h4>
<p>本次的跨多主机容器网络基于两台在不同子网网段内的物理机承载，基于物理机搭建，目的是简化后续网络通信原理分析。</p>
<p>拓扑图如下：</p>
<p><img src="http://tonybai.com/wp-content/uploads/the-topology-of-docker-multi-host-networking.png" alt="img{512x368}" /></p>
<h3>二、跨多主机容器网络搭建</h3>
<h4>1、创建<a href="https://github.com/hashicorp/consul">consul</a> 服务</h4>
<p>考虑到kv store在本文并非关键，仅作跨多主机容器网络创建启动的前提条件之用，因此仅用包含一个server节点的”cluster”。</p>
<p>参照拓扑图，我们在10.10.126.101上启动一个consul，关于consul集群以及服务注册、服务发现等细节可以参考我之前的<a href="http://tonybai.com/2015/07/06/implement-distributed-services-registery-and-discovery-by-consul/">一 篇文章</a>：</p>
<pre><code>$./consul -d agent -server -bootstrap-expect 1 -data-dir ./data -node=master -bind=10.10.126.101 -client=0.0.0.0 &amp;
</code></pre>
<h4>2、修改Docker Daemon DOCKER_OPTS参数</h4>
<p>前面提到过，通过Docker 1.9创建跨多主机容器网络需要重新配置每个主机节点上的Docker Daemon的启动参数：</p>
<pre><code>ubuntu系统这个配置在/etc/default/docker下：

DOCKER_OPTS="--dns 8.8.8.8 --dns 8.8.4.4  -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock --cluster-advertise eth0:2375 --cluster-store consul://10.10.126.101:8500/network --storage-driver=devicemapper"
</code></pre>
<p>这里多说几句：</p>
<p>-H(或&#8211;host)配置的是Docker client(包括本地和远程的client)与Docker Daemon的通信媒介，也是Docker REST api的服务端口。默认是/var/run/docker.sock（仅用于本地），当然也可以通过tcp协议通信以方便远程Client访问，就像上面 配置的那样。非加密网通信采用2375端口，而TLS加密连接则用2376端口。这两个端口已经<a href="http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=docker">申请在IANA注册并获批</a>，变成了知名端口。-H可以配置多个，就像上面配置的那样。 unix socket便于本地docker client访问本地docker daemon；tcp端口则用于远程client访问。这样一来：docker pull ubuntu，走docker.sock；而docker -H 10.10.126.101:2375 pull ubuntu则走tcp socket。</p>
<p>&#8211;cluster-advertise 配置的是本Docker Daemon实例在cluster中的地址；<br />
&#8211;cluster-store配置的是Cluster的分布式KV store的访问地址；</p>
<p>如果你之前手工修改过iptables的规则，建议重启Docker Daemon之前清理一下iptables规则：sudo iptables -t nat -F, sudo iptables -t filter -F等。</p>
<h4>3、启动各节点上的Docker Daemon</h4>
<p>以10.10.126.101为例：</p>
<pre><code>$ sudo service docker start

$ ps -ef|grep docker
root      2069     1  0 Feb02 ?        00:01:41 /usr/bin/docker -d --dns 8.8.8.8 --dns 8.8.4.4 --storage-driver=devicemapper -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock --cluster-advertise eth0:2375 --cluster-store consul://10.10.126.101:8500/network
</code></pre>
<p>启动后iptables的nat, filter规则与<a href="http://tonybai.com/2016/01/15/understanding-container-networking-on-single-host/">单机Docker网络</a>初始情况并无二致。</p>
<pre><code>101节点上初始网络driver类型：
$docker network ls
NETWORK ID          NAME                DRIVER
47e57d6fdfe8        bridge              bridge
7c5715710e34        none                null
19cc2d0d76f7        host                host
</code></pre>
<h4>4、创建overlay网络net1和net2</h4>
<p>在101节点上，创建net1：</p>
<pre><code>$ sudo docker network create -d overlay net1
</code></pre>
<p>在71节点上，创建net2:</p>
<pre><code>$ sudo docker network create -d overlay net2
</code></pre>
<p>之后无论在71节点还是101节点，我们查看当前网络以及驱动类型都是如下结果：</p>
<pre><code>$ docker network ls
NETWORK ID          NAME                DRIVER
283b96845cbe        net2                overlay
da3d1b5fcb8e        net1                overlay
00733ecf5065        bridge              bridge
71f3634bf562        none                null
7ff8b1007c09        host                host
</code></pre>
<p>此时，iptables规则也并无变化。</p>
<h4>5、启动两个overlay net下的containers</h4>
<p>我们分别在net1和net2下面启动两个container，每个节点上各种net1和net2的container各一个：</p>
<pre><code>101:
sudo docker run -itd --name net1c1 --net net1 ubuntu:14.04
sudo docker run -itd --name net2c1 --net net2 ubuntu:14.04

71:
sudo docker run -itd --name net1c2 --net net1 ubuntu:14.04
sudo docker run -itd --name net2c2 --net net2 ubuntu:14.04
</code></pre>
<p>启动后，我们就得到如下网络信息（容器的ip地址可能与前面拓扑图中的不一致，每次容器启动ip地址都可能变化）：</p>
<pre><code>net1:
    net1c1 - 10.0.0.7
    net1c2 - 10.0.0.5

net2:
    net2c1 - 10.0.0.4
    net2c2 -  10.0.0.6
</code></pre>
<h4>6、容器连通性</h4>
<p>在net1c1中，我们来看看其到net1和net2的连通性：</p>
<pre><code>root@021f14bf3924:/# ping net1c2
PING 10.0.0.5 (10.0.0.5) 56(84) bytes of data.
64 bytes from 10.0.0.5: icmp_seq=1 ttl=64 time=0.670 ms
64 bytes from 10.0.0.5: icmp_seq=2 ttl=64 time=0.387 ms
^C
--- 10.0.0.5 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 999ms
rtt min/avg/max/mdev = 0.387/0.528/0.670/0.143 ms

root@021f14bf3924:/# ping 10.0.0.4
PING 10.0.0.4 (10.0.0.4) 56(84) bytes of data.
^C
--- 10.0.0.4 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 1008ms
</code></pre>
<p>可见，net1中的容器是互通的，但net1和net2这两个overlay net之间是隔离的。</p>
<h3>三、跨多主机容器网络通信原理</h3>
<p>在“<a href="http://tonybai.com/2016/01/15/understanding-container-networking-on-single-host/">单机容器网络</a>”一文中，我们说过容器间的通信以及容器到外部网络的通信是通过docker0网桥并结合iptables实现的。那么在上面已经建立的跨多主机容器网络里，容器的通信又是如何实现的呢？下面我们一起来理解一下。注意：有了单机容器网络基础后，这里很多网络细节就不再赘述了。</p>
<p>我们先来看看，在net1下的容器的网络配置，以101上的net1c1容器为例：</p>
<pre><code>$ sudo docker attach net1c1

root@021f14bf3924:/# ip route
default via 172.19.0.1 dev eth1
10.0.0.0/24 dev eth0  proto kernel  scope link  src 10.0.0.4
172.19.0.0/16 dev eth1  proto kernel  scope link  src 172.19.0.2

root@021f14bf3924:/# ip a
1: lo: &lt;LOOPBACK,UP,LOWER_UP&gt; mtu 65536 qdisc noqueue state UNKNOWN group default
    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
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
8: eth0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1450 qdisc noqueue state UP group default
    link/ether 02:42:0a:00:00:04 brd ff:ff:ff:ff:ff:ff
    inet 10.0.0.4/24 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:aff:fe00:4/64 scope link
       valid_lft forever preferred_lft forever
10: eth1: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ac:13:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.19.0.2/16 scope global eth1
       valid_lft forever preferred_lft forever
    inet6 fe80::42:acff:fe13:2/64 scope link
       valid_lft forever preferred_lft forever
</code></pre>
<p>可以看出net1c1有两个网口：eth0(10.0.0.4)和eth1(172.19.0.2)；从路由表来看，目的地址在172.19.0.0/16范围内的，走eth1；目的地址在10.0.0.0/8范围内的，走eth0。</p>
<p>我们跳出容器，回到主机网络范畴：</p>
<pre><code>在101上：
$ ip a
... ...
5: docker_gwbridge: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc noqueue state UP
    link/ether 02:42:52:35:c9:fc brd ff:ff:ff:ff:ff:ff
    inet 172.19.0.1/16 scope global docker_gwbridge
       valid_lft forever preferred_lft forever
    inet6 fe80::42:52ff:fe35:c9fc/64 scope link
       valid_lft forever preferred_lft forever
6: docker0: &lt;NO-CARRIER,BROADCAST,MULTICAST,UP&gt; mtu 1500 qdisc noqueue state DOWN
    link/ether 02:42:4b:70:68:9a brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 scope global docker0
       valid_lft forever preferred_lft forever
11: veth26f6db4: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc noqueue master docker_gwbridge state UP
    link/ether b2:32:d7:65:dc:b2 brd ff:ff:ff:ff:ff:ff
    inet6 fe80::b032:d7ff:fe65:dcb2/64 scope link
       valid_lft forever preferred_lft forever
16: veth54881a0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc noqueue master docker_gwbridge state UP
    link/ether 9e:45:fa:5f:a0:15 brd ff:ff:ff:ff:ff:ff
    inet6 fe80::9c45:faff:fe5f:a015/64 scope link
       valid_lft forever preferred_lft forever

</code></pre>
<p>我们看到除了我们熟悉的docker0网桥外，还多出了一个docker_gwbridge网桥：</p>
<pre><code>$ brctl show
bridge name    bridge id        STP enabled    interfaces
docker0        8000.02424b70689a    no
docker_gwbridge        8000.02425235c9fc    no        veth26f6db4
                            veth54881a0
</code></pre>
<p>并且从brctl的输出结果来看，两个veth都桥接在docker_gwbridge上，而不是docker0上；docker0在跨多主机容器网络中并没有被用到。docker_gwbridge替代了docker0，用来实现101上隶属于net1网络或net2网络中容器间的通信以及容器到外部的通信，其职能就和单机容器网络中docker0一样。</p>
<p>但位于不同host且隶属于net1的两个容器net1c1和net1c2间的通信显然并没有通过docker_gwbridge完成，从net1c1路由表来看，当net1c1 ping net1c2时，消息是通过eth0，即10.0.0.4这个ip出去的。从host的视角，net1c1的eth0似乎没有网络设备与之连接，那网络通信是如何完成的呢？</p>
<p>这一切是从创建network开始的。前面我们执行docker network create -d overlay net1来创建net1 overlay network，这个命令会创建一个新的network namespace。</p>
<p>我们知道每个容器都有自己的网络namespace，从容器的视角看其网络名字空间，我们能看到网络设备诸如：lo、eth0。这个eth0与主机网络名字空间中的vethx是一个虚拟网卡pair。overlay network也有自己的net ns，而overlay network的net ns与容器的net ns之间也有着一些网络设备对应关系。</p>
<p>我们先来查看一下network namespace的id。为了能利用<a href="https://en.wikipedia.org/wiki/Iproute2">iproute2</a>工具对network ns进行管理，我们需要做如下操作：</p>
<pre><code>$cd /var/run
$sudo ln -s /var/run/docker/netns netns

</code></pre>
<p>这是因为iproute2只能操作/var/run/netns下的net ns，而docker默认的net ns却放在/var/run/docker/netns下。上面的操作成功执行后，我们就可以通过ip命令查看和管理net ns了：</p>
<pre><code>$ sudo ip netns
29170076ddf6
1-283b96845c
5ae976d9dc6a
1-da3d1b5fcb
</code></pre>
<p>我们看到在101主机上，有4个已经建立的net ns。我们大胆猜测一下，这四个net ns分别是两个container的net ns和两个overlay network的net ns。从netns的ID格式以及结合下面命令输出结果中的network id来看：</p>
<pre><code>$ docker network ls
NETWORK ID          NAME                DRIVER
283b96845cbe        net2                overlay
da3d1b5fcb8e        net1                overlay
dd84da8e80bf        host                host
3295c22b22b8        docker_gwbridge     bridge
b96e2d8d4068        bridge              bridge
23749ee4292f        none                null

</code></pre>
<p>我们大致可以猜测出来：</p>
<pre><code>1-da3d1b5fcb 是 net1的net ns；
1-283b96845c是 net2的net ns；
29170076ddf6和5ae976d9dc6a则分属于两个container的net ns。
</code></pre>
<p>由于我们以net1为例，因此下面我们就来分析net1的net ns &#8211; 1-da3d1b5fcb。通过ip命令我们可以得到如下结果：</p>
<pre><code>$ sudo ip netns exec 1-da3d1b5fcb ip a
1: lo: &lt;LOOPBACK,UP,LOWER_UP&gt; mtu 65536 qdisc noqueue state UNKNOWN
    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
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: br0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1450 qdisc noqueue state UP
    link/ether 06:b0:c6:93:25:f3 brd ff:ff:ff:ff:ff:ff
    inet 10.0.0.1/24 scope global br0
       valid_lft forever preferred_lft forever
    inet6 fe80::b80a:bfff:fecc:a1e0/64 scope link
       valid_lft forever preferred_lft forever
7: vxlan1: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc noqueue master br0 state UNKNOWN
    link/ether ea:0c:e0:bc:19:c5 brd ff:ff:ff:ff:ff:ff
    inet6 fe80::e80c:e0ff:febc:19c5/64 scope link
       valid_lft forever preferred_lft forever
9: veth2: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1450 qdisc noqueue master br0 state UP
    link/ether 06:b0:c6:93:25:f3 brd ff:ff:ff:ff:ff:ff
    inet6 fe80::4b0:c6ff:fe93:25f3/64 scope link
       valid_lft forever preferred_lft forever

$ sudo ip netns exec 1-da3d1b5fcb ip route
10.0.0.0/24 dev br0  proto kernel  scope link  src 10.0.0.1

$ sudo ip netns exec 1-da3d1b5fcb brctl show
bridge name    bridge id        STP enabled    interfaces
br0        8000.06b0c69325f3    no        veth2
                            vxlan1

</code></pre>
<p>看到br0、veth2，我们心里终于有了底儿了。我们猜测net1c1容器中的eth0与veth2是一个veth pair，并桥接在br0上，通过ethtool查找veth序号的对应关系可以证实这点：</p>
<pre><code>$ sudo docker attach net1c1
root@021f14bf3924:/# ethtool -S eth0
NIC statistics:
     peer_ifindex: 9

101主机：
$ sudo ip netns exec 1-da3d1b5fcb ip -d link
1: lo: &lt;LOOPBACK,UP,LOWER_UP&gt; mtu 65536 qdisc noqueue state UNKNOWN
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: br0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1450 qdisc noqueue state UP
    link/ether 06:b0:c6:93:25:f3 brd ff:ff:ff:ff:ff:ff
    bridge
7: vxlan1: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc noqueue master br0 state UNKNOWN
    link/ether ea:0c:e0:bc:19:c5 brd ff:ff:ff:ff:ff:ff
    vxlan
9: veth2: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1450 qdisc noqueue master br0 state UP
    link/ether 06:b0:c6:93:25:f3 brd ff:ff:ff:ff:ff:ff
    veth

</code></pre>
<p>可以看到net1c1的eth0的pair peer index为9，正好与net ns 1-da3d1b5fcb中的veth2的序号一致。</p>
<p>那么vxlan1呢？注意这个vxlan1并非是veth设备，在ip -d link输出的信息中，它的设备类型为vxlan。前面说过Docker的跨多主机容器网络是基于vxlan的，这里的vxlan1就是net1这个overlay network的一个 VTEP，即VXLAN Tunnel End Point &#8211; VXLAN隧道端点。它是VXLAN网络的边缘设备。VXLAN的相关处理都在VTEP上进行，例如识别以太网数据帧所属的VXLAN、基于 VXLAN对数据帧进行二层转发、封装/解封装报文等。</p>
<p>至此，我们可以大致画出一幅跨多主机网络的原理图：</p>
<p><img src="http://tonybai.com/wp-content/uploads/the-schematic-of-docker-multi-host-networking.png" alt="img{512x368}" /></p>
<p>如果在net1c1中ping net1c2，数据包的行走路径是怎样的呢？</p>
<p>1、net1c1(10.0.0.4)中ping net1c2(10.0.0.5)，根据net1c1的路由表，数据包可通过直连网络到达net1c2。于是arp请求获取net1c2的MAC地址（在vxlan上的arp这里不详述了），得到mac地址后，封包，从eth0发出；<br />
2、eth0桥接在net ns 1-da3d1b5fcb中的br0上，这个br0是个网桥(交换机)虚拟设备，需要将来自eth0的包转发出去，于是将包转给了vxlan设备；这个可以通过arp -a看到一些端倪：</p>
<pre><code>$ sudo ip netns exec 1-da3d1b5fcb arp -a
? (10.0.0.5) at 02:42:0a:00:00:05 [ether] PERM on vxlan1
</code></pre>
<p>3、vxlan是个特殊设备，收到包后，由vxlan设备创建时注册的设备处理程序对包进行处理，即进行VXLAN封包（这期间会查询consul中存储的net1信息），将ICMP包整体作为UDP包的payload封装起来，并将UDP包通过宿主机的eth0发送出去。</p>
<p>4、71宿主机收到UDP包后，发现是VXLAN包，根据VXLAN包中的相关信息（比如Vxlan Network Identifier，VNI=256)找到vxlan设备，并转给该vxlan设备处理。vxlan设备的处理程序进行解包，并将UDP中的payload取出，整体通过br0转给veth口，net1c2从eth0收到ICMP数据包，回复icmp reply。</p>
<p>我们可以通过<a href="https://www.wireshark.org">wireshark</a>抓取相关vxlan包，高版本wireshark内置VXLAN协议分析器，可以直接识别和展示VXLAN包，这里安装的是2.0.1版本（注意：一些低版本wireshark不支持VXLAN分析器，比如1.6.7版本）：</p>
<p><img src="http://tonybai.com/wp-content/uploads/docker-multi-host-networking-wireshark.png" alt="img{512x368}" /></p>
<p>关于VXLAN协议的细节，过于复杂，在后续的文章中maybe会有进一步理解。</p>
<p style='text-align:left'>&copy; 2016, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2016/02/15/understanding-docker-multi-host-networking/feed/</wfw:commentRss>
		<slash:comments>14</slash:comments>
		</item>
		<item>
		<title>理解Docker容器端口映射</title>
		<link>https://tonybai.com/2016/01/18/understanding-binding-docker-container-ports-to-host/</link>
		<comments>https://tonybai.com/2016/01/18/understanding-binding-docker-container-ports-to-host/#comments</comments>
		<pubDate>Mon, 18 Jan 2016 09:43:28 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[aufs]]></category>
		<category><![CDATA[bridge]]></category>
		<category><![CDATA[cgroups]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[coreos]]></category>
		<category><![CDATA[DevOps]]></category>
		<category><![CDATA[DNAT]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[docker-proxy]]></category>
		<category><![CDATA[firewall]]></category>
		<category><![CDATA[flannel]]></category>
		<category><![CDATA[gateway]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[ifconfig]]></category>
		<category><![CDATA[iproute2]]></category>
		<category><![CDATA[iptables]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[mesos]]></category>
		<category><![CDATA[NAT]]></category>
		<category><![CDATA[netperf]]></category>
		<category><![CDATA[netstat]]></category>
		<category><![CDATA[networking]]></category>
		<category><![CDATA[rancher]]></category>
		<category><![CDATA[Router]]></category>
		<category><![CDATA[SNAT]]></category>
		<category><![CDATA[sparkyfish]]></category>
		<category><![CDATA[switch]]></category>
		<category><![CDATA[tcpip]]></category>
		<category><![CDATA[telnet]]></category>
		<category><![CDATA[traceroute]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[un-DNAT]]></category>
		<category><![CDATA[un-SNAT]]></category>
		<category><![CDATA[交换机]]></category>
		<category><![CDATA[内核]]></category>
		<category><![CDATA[容器]]></category>
		<category><![CDATA[端口映射]]></category>
		<category><![CDATA[网关]]></category>
		<category><![CDATA[网桥]]></category>
		<category><![CDATA[网络]]></category>
		<category><![CDATA[路由器]]></category>
		<category><![CDATA[防火墙]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1938</guid>
		<description><![CDATA[在”理解Docker单机容器网络“一文中，还有一个Docker容器网络的功能尚未提及，那就是Docker容器的端口映射。即将容器的服务端口P&#8217; 绑定到宿主机的端口P上，最终达到一种效果：外部程序通过宿主机的P端口访问，就像直接访问Docker容器网络内部容器提供的服务一样。 Docker针对端口映射前后有两种方案，一种是1.7版本之前docker-proxy+iptables DNAT的方式；另一种则是1.7版本(及之后)提供的完全由iptables DNAT实现的端口映射。不过在目前docker 1.9.1中，前一种方式依旧是默认方式。但是从Docker 1.7版本起，Docker提供了一个配置项：&#8211;userland-proxy，以让Docker用户决定是否启用docker-proxy，默认为true，即启用docker-proxy。本文续前文，继续探讨使用端口映射时Docker容器网络的通信流程。 本文中的实验环境依旧保持与上文相同：docker 1.9.1，ubuntu 12.04宿主机，docker image基于官方ubuntu 14.04 image做的一些软件安装。 一、&#8211;userland-proxy=true(defaut)的情况下端口映射 我们首先在实验环境下采用默认的方式进行端口映射，即&#8211;userland-proxy=true。 我们来建立一个 新container &#8211; container3(172.17.0.4)，实现了0.0.0.0:12580 -> container3:12580。 $docker run -it --name container3 -p 12580:12580 dockernetworking/ubuntu:14.04 /bin/bash 这个命令执行后，iptables增加了三条rules： filter forward链: Chain DOCKER (1 references) pkts bytes target prot opt in out source destination 0 0 ACCEPT tcp -- !docker0 docker0 0.0.0.0/0 172.17.0.4 [...]]]></description>
			<content:encoded><![CDATA[<p>在”<a href="http://tonybai.com/2016/01/15/understanding-container-networking-on-single-host/">理解Docker单机容器网络</a>“一文中，还有一个<a href="http://tonybai.com/tag/docker">Docker</a>容器网络的功能尚未提及，那就是Docker容器的端口映射。即将容器的服务端口P&#8217; 绑定到宿主机的端口P上，最终达到一种效果：外部程序通过宿主机的P端口访问，就像直接访问Docker容器网络内部容器提供的服务一样。</p>
<p>Docker针对端口映射前后有两种方案，一种是1.7版本之前docker-proxy+iptables <a href="http://idallen.com/dnat.txt">DNAT</a>的方式；另一种则是1.7版本(及之后)提供的完全由iptables DNAT实现的端口映射。不过在目前docker 1.9.1中，前一种方式依旧是默认方式。但是从Docker 1.7版本起，Docker提供了一个配置项：&#8211;userland-proxy，以让Docker用户决定是否启用docker-proxy，默认为true，即启用docker-proxy。本文续前文，继续探讨使用端口映射时Docker容器网络的通信流程。</p>
<p>本文中的实验环境依旧保持与上文相同：docker 1.9.1，<a href="http://tonybai.com/tag/ubuntu">ubuntu</a> 12.04宿主机，docker image基于官方ubuntu 14.04 image做的一些软件安装。</p>
<h3>一、&#8211;userland-proxy=true(defaut)的情况下端口映射</h3>
<p>我们首先在实验环境下采用默认的方式进行端口映射，即&#8211;userland-proxy=true。</p>
<p>我们来建立一个 新container &#8211; container3(172.17.0.4)，实现了0.0.0.0:12580 -> container3:12580。</p>
<pre><code>$docker run -it --name container3 -p 12580:12580 dockernetworking/ubuntu:14.04 /bin/bash
</code></pre>
<p>这个命令执行后，iptables增加了三条rules：</p>
<pre><code>filter forward链:
Chain DOCKER (1 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 ACCEPT     tcp  --  !docker0 docker0  0.0.0.0/0            172.17.0.4           tcp dpt:12580

nat output链:
Chain DOCKER (1 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 DNAT       tcp  --  !docker0 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:12580 to:172.17.0.4:12580

nat postrouting链：

Chain POSTROUTING (policy ACCEPT 24 packets, 1472 bytes)
 pkts bytes target     prot opt in     out     source               destination
    0     0 MASQUERADE  tcp  --  *      *       172.17.0.4           172.17.0.4           tcp dpt:12580

</code></pre>
<p>我们可以看到了一个DNAT target，是在nat output链中，这个是一个关键点。同样是考虑到调试的方便，在这新增的rules前面，增加LOG target，新的iptables导出内容为：</p>
<pre><code>iptables.portmap.stage1.rules

# Generated by iptables-save v1.4.12 on Fri Jan 15 15:31:06 2016
*raw
: PREROUTING ACCEPT [5737658:60554342802]
:OUTPUT ACCEPT [4294004:56674784720]
-A PREROUTING -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-RawPrerouting:" --log-level 7
-A PREROUTING -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-RawPrerouting:" --log-level 7
-A OUTPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-RawOutput:" --log-level 7
-A OUTPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-RawOutput:" --log-level 7
COMMIT
# Completed on Fri Jan 15 15:31:06 2016
# Generated by iptables-save v1.4.12 on Fri Jan 15 15:31:06 2016
*filter
:INPUT ACCEPT [4444190:53498587744]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [4292173:56674165678]
: DOCKER - [0:0]
:FwdId0Od0 - [0:0]
:FwdId0Ond0 - [0:0]
:FwdOd0 - [0:0]
-A INPUT ! -i lo -p icmp -j LOG --log-prefix "[TonyBai]-EnterFilterInput:" --log-level 7
-A INPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-FilterInput:" --log-level 7
-A INPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-FilterInput:" --log-level 7
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j FwdOd0
-A FORWARD -i docker0 ! -o docker0 -j FwdId0Ond0
-A FORWARD -i docker0 -o docker0 -j FwdId0Od0
-A OUTPUT ! -s 127.0.0.1/32 -p icmp -j LOG --log-prefix "[TonyBai]-EnterFilterOutput:" --log-level 7
-A OUTPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-FilterOutput:" --log-level 7
-A OUTPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-FilterOutput:" --log-level 7
-A DOCKER -d 172.17.0.4/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-PortmapFowardDocker:" --log-level 7
-A DOCKER -d 172.17.0.4/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 12580 -j ACCEPT
-A FwdId0Od0 -i docker0 -o docker0 -j LOG --log-prefix "[TonyBai]-FwdId0Od0:" --log-level 7
-A FwdId0Od0 -i docker0 -o docker0 -j ACCEPT
-A FwdId0Ond0 -i docker0 ! -o docker0 -j LOG --log-prefix "[TonyBai]-FwdId0Ond0:" --log-level 7
-A FwdId0Ond0 -i docker0 ! -o docker0 -j ACCEPT
-A FwdOd0 -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j LOG --log-prefix "[TonyBai]-FwdOd0:" --log-level 7
-A FwdOd0 -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
COMMIT
# Completed on Fri Jan 15 15:31:06 2016
# Generated by iptables-save v1.4.12 on Fri Jan 15 15:31:06 2016
*nat
: PREROUTING ACCEPT [24690:5091417]
:INPUT ACCEPT [10942:2271167]
:OUTPUT ACCEPT [7756:523318]
: POSTROUTING ACCEPT [7759:523498]
: DOCKER - [0:0]
:LogNatPostRouting - [0:0]
-A PREROUTING -p icmp -j LOG --log-prefix "[TonyBai]-Enter iptables:" --log-level 7
-A PREROUTING -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-NatPrerouting:" --log-level 7
-A PREROUTING -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-NatPrerouting:" --log-level 7
-A INPUT ! -i lo -p icmp -j LOG --log-prefix "[TonyBai]-EnterNatInput:" --log-level 7
-A INPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-NatInput:" --log-level 7
-A INPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-NatInput:" --log-level 7
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j LogNatPostRouting
-A POSTROUTING -s 172.17.0.4/32 -d 172.17.0.4/32 -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-PortmapNatPostRouting:" --log-level 7
-A POSTROUTING -s 172.17.0.4/32 -d 172.17.0.4/32 -p tcp -m tcp --dport 12580 -j MASQUERADE
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-PortmapNatOutputDocker:" --log-level 7
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 12580 -j DNAT --to-destination 172.17.0.4:12580
-A LogNatPostRouting -s 172.17.0.0/16 ! -o docker0 -j LOG --log-prefix "[TonyBai]-NatPostRouting:" --log-level 7
-A LogNatPostRouting -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
COMMIT
# Completed on Fri Jan 15 15:31:06 2016
</code></pre>
<p>另外我们可以查看到宿主机中多了一个进程，这就是前面所说的docker-proxy，每增加一个端口映射，宿主机就会多出一个docker-proxy进程：</p>
<pre><code>root      5742  2113  0 08:48 ?        00:00:00 docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 12580 -container-ip 172.17.0.4 -container-port 12580
</code></pre>
<h4>1、从10.10.126.187访问宿主机(10.10.126.101)的12580端口</h4>
<p>10.10.126.187是与101在同一直连网路的主机，我们在其上执行telnet 10.10.126.101 12580。如果container3中有server在监听12580，则建立连接和数据通信(发送一个hello)的过程如下。</p>
<p>【187到101的tcp握手sync包】</p>
<p>101从eth0网卡收到目的地址是自己的sync数据包：</p>
<pre><code>Jan 15 16:04:54 pc-baim kernel: [28410.162828] [TonyBai]-RawPrerouting:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=32617 DF PROTO=TCP SPT=33250 DPT=12580 WINDOW=5840 RES=0x00 SYN URGP=0
Jan 15 16:04:54 pc-baim kernel: [28410.162862] [TonyBai]-NatPrerouting:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=32617 DF PROTO=TCP SPT=33250 DPT=12580 WINDOW=5840 RES=0x00 SYN URGP=0
</code></pre>
<p>由于目的地址就是自己，因此在iptables中走input chain将数据包发给user层：</p>
<pre><code>Jan 15 16:04:54 pc-baim kernel: [28410.162885] [TonyBai]-FilterInput:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=32617 DF PROTO=TCP SPT=33250 DPT=12580 WINDOW=5840 RES=0x00 SYN URGP=0
Jan 15 16:04:54 pc-baim kernel: [28410.162900] [TonyBai]-NatInput:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=32617 DF PROTO=TCP SPT=33250 DPT=12580 WINDOW=5840 RES=0x00 SYN URGP=0
</code></pre>
<p>【101回复ack sync包】</p>
<p>101上的用户层是docker-proxy在监听12580端口，当收到sync后，会回复ack sync。由于是user空间自产包，路由后走output链。</p>
<pre><code>Jan 15 16:04:54 pc-baim kernel: [28410.162933] [TonyBai]-RawOutput:IN= OUT=eth0 SRC=10.10.126.101 DST=10.10.126.187 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=33250 WINDOW=28960 RES=0x00 ACK SYN URGP=0
Jan 15 16:04:54 pc-baim kernel: [28410.162948] [TonyBai]-FilterOutput:IN= OUT=eth0 SRC=10.10.126.101 DST=10.10.126.187 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=33250 WINDOW=28960 RES=0x00 ACK SYN URGP=0
</code></pre>
<p>【187回复ack，101与187握手完成】</p>
<p>187回复握手过程最后的一个ack。这个过程与sync类似：</p>
<pre><code>Jan 15 16:04:54 pc-baim kernel: [28410.163397] [TonyBai]-RawPrerouting:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=10.10.126.101 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=32618 DF PROTO=TCP SPT=33250 DPT=12580 WINDOW=92 RES=0x00 ACK URGP=0
Jan 15 16:04:54 pc-baim kernel: [28410.163437] [TonyBai]-FilterInput:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=10.10.126.101 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=32618 DF PROTO=TCP SPT=33250 DPT=12580 WINDOW=92 RES=0x00 ACK URGP=0
</code></pre>
<p>重点是接下来发生的事情：101上的docker-proxy向container3上的server程序建立tcp连接！</p>
<p>【host向container3发送sync】</p>
<pre><code>Jan 15 16:04:54 pc-baim kernel: [28410.163863] [TonyBai]-RawOutput:IN= OUT=docker0 SRC=172.17.0.1 DST=172.17.0.4 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=5768 DF PROTO=TCP SPT=43771 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0
Jan 15 16:04:54 pc-baim kernel: [28410.163901] [TonyBai]-FilterOutput:IN= OUT=docker0 SRC=172.17.0.1 DST=172.17.0.4 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=5768 DF PROTO=TCP SPT=43771 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0
</code></pre>
<p>我们看到SYN数据包源地址用的是172.17.0.1，不知是否是docker-proxy内部有意选择了网桥的ip。由于是user层发出的包，于是走iptables output链。</p>
<p>【container3回复ack sync】</p>
<p>container3回复ack sync，目的地址是172.17.0.1，host从docker0网卡收到ack sync数据，路由后发现是发给自己的包，于是走input chain.</p>
<pre><code>Jan 15 16:04:54 pc-baim kernel: [28410.164000] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethf0cc298 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=172.17.0.1 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=43771 WINDOW=28960 RES=0x00 ACK SYN URGP=0
Jan 15 16:04:54 pc-baim kernel: [28410.164026] [TonyBai]-FilterInput:IN=docker0 OUT= PHYSIN=vethf0cc298 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=172.17.0.1 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=43771 WINDOW=28960 RES=0x00 ACK SYN URGP=0
</code></pre>
<p>【host回复ack，host与container3握手完成】</p>
<p>host回复握手过程最后的一个ack。user空间自产数据包，于是走output chain：</p>
<pre><code>Jan 15 16:04:54 pc-baim kernel: [28410.164049] [TonyBai]-RawOutput:IN= OUT=docker0 SRC=172.17.0.1 DST=172.17.0.4 LEN=52 TOS=0x00 PREC=0x00 TTL=64 ID=5769 DF PROTO=TCP SPT=43771 DPT=12580 WINDOW=229 RES=0x00 ACK URGP=0
Jan 15 16:04:54 pc-baim kernel: [28410.164058] [TonyBai]-FilterOutput:IN= OUT=docker0 SRC=172.17.0.1 DST=172.17.0.4 LEN=52 TOS=0x00 PREC=0x00 TTL=64 ID=5769 DF PROTO=TCP SPT=43771 DPT=12580 WINDOW=229 RES=0x00 ACK URGP=0
</code></pre>
<p>【187 在已经建立的连接上发送”hello”】</p>
<p>187发送hello to host，docker-proxy收到hello数据：</p>
<pre><code>Jan 15 16:04:58 pc-baim kernel: [28413.840854] [TonyBai]-RawPrerouting:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=10.10.126.101 LEN=59 TOS=0x10 PREC=0x00 TTL=64 ID=32619 DF PROTO=TCP SPT=33250 DPT=12580 WINDOW=92 RES=0x00 ACK PSH URGP=0
Jan 15 16:04:58 pc-baim kernel: [28413.840874] [TonyBai]-FilterInput:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=10.10.126.101 LEN=59 TOS=0x10 PREC=0x00 TTL=64 ID=32619 DF PROTO=TCP SPT=33250 DPT=12580 WINDOW=92 RES=0x00 ACK PSH URGP=0
</code></pre>
<p>【host返回 ack push】</p>
<pre><code>Jan 15 16:04:58 pc-baim kernel: [28413.840893] [TonyBai]-RawOutput:IN= OUT=eth0 SRC=10.10.126.101 DST=10.10.126.187 LEN=52 TOS=0x00 PREC=0x00 TTL=64 ID=22415 DF PROTO=TCP SPT=12580 DPT=33250 WINDOW=227 RES=0x00 ACK URGP=0
Jan 15 16:04:58 pc-baim kernel: [28413.840902] [TonyBai]-FilterOutput:IN= OUT=eth0 SRC=10.10.126.101 DST=10.10.126.187 LEN=52 TOS=0x00 PREC=0x00 TTL=64 ID=22415 DF PROTO=TCP SPT=12580 DPT=33250 WINDOW=227 RES=0x00 ACK URGP=0
</code></pre>
<p>接下来，docker-proxy将hello从已有连接上转发给container3。</p>
<p>【host转发hello到container3】</p>
<pre><code>Jan 15 16:04:58 pc-baim kernel: [28413.841000] [TonyBai]-RawOutput:IN= OUT=docker0 SRC=172.17.0.1 DST=172.17.0.4 LEN=59 TOS=0x00 PREC=0x00 TTL=64 ID=5770 DF PROTO=TCP SPT=43771 DPT=12580 WINDOW=229 RES=0x00 ACK PSH URGP=0
Jan 15 16:04:58 pc-baim kernel: [28413.841026] [TonyBai]-FilterOutput:IN= OUT=docker0 SRC=172.17.0.1 DST=172.17.0.4 LEN=59 TOS=0x00 PREC=0x00 TTL=64 ID=5770 DF PROTO=TCP SPT=43771 DPT=12580 WINDOW=229 RES=0x00 ACK PSH URGP=0
</code></pre>
<p>【container3回复ack 】</p>
<pre><code>Jan 15 16:04:58 pc-baim kernel: [28413.841101] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethf0cc298 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=172.17.0.1 LEN=52 TOS=0x00 PREC=0x00 TTL=64 ID=61139 DF PROTO=TCP SPT=12580 DPT=43771 WINDOW=227 RES=0x00 ACK URGP=0
Jan 15 16:04:58 pc-baim kernel: [28413.841119] [TonyBai]-FilterInput:IN=docker0 OUT= PHYSIN=vethf0cc298 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=172.17.0.1 LEN=52 TOS=0x00 PREC=0x00 TTL=64 ID=61139 DF PROTO=TCP SPT=12580 DPT=43771 WINDOW=227 RES=0x00 ACK URGP=0
</code></pre>
<p>通信过程到此结束。通过这个过程，我们至少了解到两点：</p>
<p>1、docker-proxy将外部建立在host:12580上的连接上的数据转发到container中，反之亦然，如果container 通过与host已经建立的连接向外发送数据，docker-proxy也会将数据转发给187。<br />
2、通过iptables log输出我们可以看到：为了port map而添加的DNAT和MASQUERADE 并没有被匹配到，也就是说在这个过程中并没有用到DNAT，而是完全依靠docker-proxy做的4层代理。</p>
<h4>2、从宿主机上访问10.10.126.101:12580</h4>
<p>我们在宿主机本机上访问10.10.126.101:12580，看看这个通信过程与上面的是否有差异。</p>
<p>【与本机12580端口建立连接，发送sync包】</p>
<p>由于是user层发送数据包，因此走iptables output链。</p>
<pre><code>Jan 15 16:40:15 pc-baim kernel: [30532.594545] [TonyBai]-RawOutput:IN= OUT=lo SRC=10.10.126.101 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=53747 DF PROTO=TCP SPT=48039 DPT=12580 WINDOW=43690 RES=0x00 SYN URGP=0
</code></pre>
<p>在output链上，匹配到nat output上的规则：</p>
<pre><code>Chain DOCKER (1 references)
 pkts bytes target     prot opt in     out     source               destination
    1    60 LOG        tcp  --  !docker0 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:12580 LOG flags 0 level 7 prefix "[TonyBai]-PortmapNatOutputDoc"
    1    60 DNAT       tcp  --  !docker0 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:12580 to:172.17.0.4:12580
</code></pre>
<p>于是这里将做一个DNAT，数据包的目的地址10.10.126.101被替换为172.17.0.4。</p>
<pre><code>Jan 15 16:40:15 pc-baim kernel: [30532.594561] [TonyBai]-PortmapNatOutputDoc IN= OUT=lo SRC=10.10.126.101 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=53747 DF PROTO=TCP SPT=48039 DPT=12580 WINDOW=43690 RES=0x00 SYN URGP=0

Jan 15 16:40:15 pc-baim kernel: [30532.594572] [TonyBai]-FilterOutput:IN= OUT=lo SRC=10.10.126.101 DST=172.17.0.4 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=53747 DF PROTO=TCP SPT=48039 DPT=12580 WINDOW=43690 RES=0x00 SYN URGP=0
</code></pre>
<p>DNAT后，将按照目的地址做一个重新路由：叫实际路由。消息实际重定向到docker0进行封包发送，sync包直接进入到container3 中。</p>
<p>【container3发送ack sync包】</p>
<p>docker0出来的ack sync 通过input chain送到user空间。这块应该由一个自动un-DNAT，将172.17.0.4自动转回10.10.126.101，但通过iptables日志无法确认这点。</p>
<pre><code>Jan 15 16:40:15 pc-baim kernel: [30532.594615] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethf0cc298 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=10.10.126.101 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=48039 WINDOW=28960 RES=0x00 ACK SYN URGP=0
Jan 15 16:40:15 pc-baim kernel: [30532.594624] [TonyBai]-FilterInput:IN=docker0 OUT= PHYSIN=vethf0cc298 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=10.10.126.101 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=48039 WINDOW=28960 RES=0x00 ACK SYN URGP=0
</code></pre>
<p>【host发送ack，完成握手】</p>
<p>host回复ack。user层自产包，走output链，看rawoutput，dst依旧是126.101(telnet自然不应该知道 172.17.0.4的存在)，但是filter output 前，iptables对该地址自动做了dnat，无需重新进入到nat output链，因为之前已经进过了。在filter output中，我们看到dst ip已经变成了container3的ip地址：</p>
<pre><code>Jan 15 16:40:15 pc-baim kernel: [30532.594637] [TonyBai]-RawOutput:IN= OUT=lo SRC=10.10.126.101 DST=10.10.126.101 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=53748 DF PROTO=TCP SPT=48039 DPT=12580 WINDOW=342 RES=0x00 ACK URGP=0
Jan 15 16:40:15 pc-baim kernel: [30532.594643] [TonyBai]-FilterOutput:IN= OUT=lo SRC=10.10.126.101 DST=172.17.0.4 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=53748 DF PROTO=TCP SPT=48039 DPT=12580 WINDOW=342 RES=0x00 ACK URGP=0
</code></pre>
<p>【host发送hello】</p>
<p>这个过程同上，不赘述。</p>
<pre><code>Jan 15 16:40:18 pc-baim kernel: [30535.344921] [TonyBai]-RawOutput:IN= OUT=lo SRC=10.10.126.101 DST=10.10.126.101 LEN=59 TOS=0x10 PREC=0x00 TTL=64 ID=53749 DF PROTO=TCP SPT=48039 DPT=12580 WINDOW=342 RES=0x00 ACK PSH URGP=0
Jan 15 16:40:18 pc-baim kernel: [30535.344956] [TonyBai]-FilterOutput:IN= OUT=lo SRC=10.10.126.101 DST=172.17.0.4 LEN=59 TOS=0x10 PREC=0x00 TTL=64 ID=53749 DF PROTO=TCP SPT=48039 DPT=12580 WINDOW=342 RES=0x00 ACK PSH URGP=0
</code></pre>
<p>【container回复ack】</p>
<p>不赘述。</p>
<pre><code>Jan 15 16:40:18 pc-baim kernel: [30535.345027] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethf0cc298 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=10.10.126.101 LEN=52 TOS=0x00 PREC=0x00 TTL=64 ID=43021 DF PROTO=TCP SPT=12580 DPT=48039 WINDOW=227 RES=0x00 ACK URGP=0
Jan 15 16:40:18 pc-baim kernel: [30535.345056] [TonyBai]-FilterInput:IN=docker0 OUT= PHYSIN=vethf0cc298 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=10.10.126.101 LEN=52 TOS=0x00 PREC=0x00 TTL=64 ID=43021 DF PROTO=TCP SPT=12580 DPT=48039 WINDOW=227 RES=0x00 ACK URGP=0
</code></pre>
<p>从这个过程可以看到，在宿主机上访问container的映射端口，通信流程不走docker-proxy，而是直接通过output 的dnat将数据包被直接转给container中的server程序。</p>
<h4>3、container to container</h4>
<p>在container1中telnet 10.10.126.101 12580会发生什么呢？这里就不长篇大论的列log了，直接给出结论：通过docker-proxy转发，因为不满足nat output中DNAT的匹配条件。</p>
<h3>二、在&#8211;userland-proxy=false的情况下</h3>
<p>我们修改了一下/etc/default/docker配置，为DOCKER_OPTS增加一个option: &#8211;userland-proxy=false。</p>
<pre><code>DOCKER_OPTS="--dns 8.8.8.8 --dns 8.8.4.4 --userland-proxy=false"
</code></pre>
<p>重启docker daemon并清理iptables规则(-F)，并启动做端口映射的container3。启动后，你会发现之前的docker-proxy并没有出现在启动进程列表中，iptables的规则与&#8211;userland-proxy=true时也有所不同：</p>
<pre><code>$ sudo iptables -nL -v
Chain INPUT (policy ACCEPT 1645 packets, 368K bytes)
 pkts bytes target     prot opt in     out     source               destination

Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination
    0     0 DOCKER     all  --  *      docker0  0.0.0.0/0            0.0.0.0/0
    0     0 ACCEPT     all  --  *      docker0  0.0.0.0/0            0.0.0.0/0            ctstate RELATED,ESTABLISHED
    0     0 ACCEPT     all  --  docker0 !docker0  0.0.0.0/0            0.0.0.0/0
    0     0 ACCEPT     all  --  docker0 docker0  0.0.0.0/0            0.0.0.0/0

Chain OUTPUT (policy ACCEPT 263 packets, 134K bytes)
 pkts bytes target     prot opt in     out     source               destination

Chain DOCKER (1 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 ACCEPT     tcp  --  !docker0 docker0  0.0.0.0/0            172.17.0.4           tcp dpt:12580

$ sudo iptables -t nat -nL -v
Chain PREROUTING (policy ACCEPT 209 packets, 65375 bytes)
 pkts bytes target     prot opt in     out     source               destination
   71 49357 DOCKER     all  --  *      *       0.0.0.0/0            0.0.0.0/0            ADDRTYPE match dst-type LOCAL

Chain INPUT (policy ACCEPT 98 packets, 39060 bytes)
 pkts bytes target     prot opt in     out     source               destination

Chain OUTPUT (policy ACCEPT 34 packets, 2096 bytes)
 pkts bytes target     prot opt in     out     source               destination
   21  1302 DOCKER     all  --  *      *       0.0.0.0/0            0.0.0.0/0            ADDRTYPE match dst-type LOCAL

Chain POSTROUTING (policy ACCEPT 34 packets, 2096 bytes)
 pkts bytes target     prot opt in     out     source               destination
    0     0 MASQUERADE  all  --  *      docker0  0.0.0.0/0            0.0.0.0/0            ADDRTYPE match src-type LOCAL
    0     0 MASQUERADE  all  --  *      !docker0  172.17.0.0/16        0.0.0.0/0
    0     0 MASQUERADE  tcp  --  *      *       172.17.0.4           172.17.0.4           tcp dpt:12580

Chain DOCKER (2 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 DNAT       tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:12580 to:172.17.0.4:12580
</code></pre>
<p>可以看到nat表中prerouting链增加了target为DOCKER链的规则，并且Docker链中对dnat的匹配条件也放开了，只要是dst-type是LOCAL的，dport=12580的，都将ip映射为172.17.0.4。</p>
<p>由于iptables的规则有所变化，因此因此我的log target的匹配条件也该调整一下了，调整后的iptables为：</p>
<pre><code>iptables.portmap.stage1.tmp.rules

# Generated by iptables-save v1.4.12 on Mon Jan 18 09:06:06 2016
*mangle
: POSTROUTING ACCEPT [0:0]
-A POSTROUTING -o docker0 -m addrtype --src-type LOCAL -j LOG --log-prefix "[TonyBai]-manglepost1" --log-level 7
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j LOG --log-prefix "[TonyBai]-manglepost2" --log-level 7
-A POSTROUTING -s 172.17.0.4/32 -d 172.17.0.4/32 -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-manglepost3" --log-level 7
COMMIT

*raw
: PREROUTING ACCEPT [1008742:377375989]
:OUTPUT ACCEPT [426678:274235692]
-A PREROUTING -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-RawPrerouting:" --log-level 7
-A PREROUTING -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-RawPrerouting:" --log-level 7
-A OUTPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-RawOutput:" --log-level 7
-A OUTPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-RawOutput:" --log-level 7
COMMIT
# Completed on Mon Jan 18 09:06:06 2016
# Generated by iptables-save v1.4.12 on Mon Jan 18 09:06:06 2016
*filter
:INPUT ACCEPT [187016:64478647]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [81342:51955911]
: DOCKER - [0:0]
:FwdId0Od0 - [0:0]
:FwdId0Ond0 - [0:0]
:FwdOd0 - [0:0]
-A INPUT ! -i lo -p icmp -j LOG --log-prefix "[TonyBai]-EnterFilterInput:" --log-level 7
-A INPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-FilterInput:" --log-level 7
-A INPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-FilterInput:" --log-level 7
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j FwdOd0
-A FORWARD -i docker0 ! -o docker0 -j FwdId0Ond0
-A FORWARD -i docker0 -o docker0 -j FwdId0Od0
-A OUTPUT ! -s 127.0.0.1/32 -p icmp -j LOG --log-prefix "[TonyBai]-EnterFilterOutput:" --log-level 7
-A OUTPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-FilterOutput:" --log-level 7
-A OUTPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-FilterOutput:" --log-level 7
-A DOCKER -d 172.17.0.4/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-PortmapFowardDocker" --log-level 7
-A DOCKER -d 172.17.0.4/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 12580 -j ACCEPT
-A FwdId0Od0 -i docker0 -o docker0 -j LOG --log-prefix "[TonyBai]-FwdId0Od0:" --log-level 7
-A FwdId0Od0 -i docker0 -o docker0 -j ACCEPT
-A FwdId0Ond0 -i docker0 ! -o docker0 -j LOG --log-prefix "[TonyBai]-FwdId0Ond0:" --log-level 7
-A FwdId0Ond0 -i docker0 ! -o docker0 -j ACCEPT
-A FwdOd0 -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j LOG --log-prefix "[TonyBai]-FwdOd0:" --log-level 7
-A FwdOd0 -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
COMMIT
# Completed on Mon Jan 18 09:06:06 2016
# Generated by iptables-save v1.4.12 on Mon Jan 18 09:06:06 2016
*nat
: PREROUTING ACCEPT [34423:7014094]
:INPUT ACCEPT [9475:1880078]
:OUTPUT ACCEPT [3524:218202]
: POSTROUTING ACCEPT [3508:217098]
: DOCKER - [0:0]
:LogNatPostRouting1 - [0:0]
:LogNatPostRouting2 - [0:0]
:LogNatPostRouting3 - [0:0]
-A PREROUTING -p icmp -j LOG --log-prefix "[TonyBai]-Enter iptables:" --log-level 7
-A PREROUTING -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-NatPrerouting:" --log-level 7
-A PREROUTING -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-NatPrerouting:" --log-level 7
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A INPUT ! -i lo -p icmp -j LOG --log-prefix "[TonyBai]-EnterNatInput:" --log-level 7
-A INPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-NatInput:" --log-level 7
-A INPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-NatInput:" --log-level 7
-A OUTPUT -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -p tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-NatPostrouteEnter" --log-level 7
-A POSTROUTING -p tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-NatPostrouteEnter" --log-level 7
-A POSTROUTING -o docker0 -m addrtype --src-type LOCAL -j LogNatPostRouting1
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j LogNatPostRouting2
-A POSTROUTING -s 172.17.0.4/32 -d 172.17.0.4/32 -p tcp -m tcp --dport 12580 -j LogNatPostRouting3
-A DOCKER -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-PortmapNatPrerouting" --log-level 7
-A DOCKER -p tcp -m tcp --dport 12580 -j DNAT --to-destination 172.17.0.4:12580
-A LogNatPostRouting1 -o docker0 -m addrtype --src-type LOCAL -j LOG --log-prefix "[TonyBai]-NatPost1" --log-level 7
-A LogNatPostRouting1 -o docker0 -m addrtype --src-type LOCAL -j MASQUERADE
-A LogNatPostRouting2 -s 172.17.0.0/16 ! -o docker0 -j LOG --log-prefix "[TonyBai]-NatPost2" --log-level 7
-A LogNatPostRouting2 -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A LogNatPostRouting3 -s 172.17.0.4/32 -d 172.17.0.4/32 -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-NatPost3" --log-level 7
-A LogNatPostRouting3 -s 172.17.0.4/32 -d 172.17.0.4/32 -p tcp -m tcp --dport 12580 -j MASQUERADE
COMMIT
# Completed on Mon Jan 18 09:06:06 2016

</code></pre>
<p>接下来，我们按照上面的方法再做一遍实验例子，看看通信流程有何不同。这次我们将187主机换为10.10.105.71，其他无差别。</p>
<h4>1、 在71上telnet 10.10.126.101 12580</h4>
<p>宿主机从eth0接口收到syn，nat prerouting中做DNAT。路由后，通过forward链转发到docker0：</p>
<pre><code>Jan 18 13:35:55 pc-baim kernel: [278835.389225] [TonyBai]-RawPrerouting:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:23:89:7d:b6:b1:08:00 SRC=10.10.105.71 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=63 ID=61480 DF PROTO=TCP SPT=41502 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0
Jan 18 13:35:55 pc-baim kernel: [278835.389275] [TonyBai]-NatPrerouting:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:23:89:7d:b6:b1:08:00 SRC=10.10.105.71 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=63 ID=61480 DF PROTO=TCP SPT=41502 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0
Jan 18 13:35:55 pc-baim kernel: [278835.389290] [TonyBai]-PortmapNatPreroutinIN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:23:89:7d:b6:b1:08:00 SRC=10.10.105.71 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=63 ID=61480 DF PROTO=TCP SPT=41502 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0
Jan 18 13:35:55 pc-baim kernel: [278835.389326] [TonyBai]-PortmapFowardDockerIN=eth0 OUT=docker0 MAC=2c:59:e5:01:98:28:00:23:89:7d:b6:b1:08:00 SRC=10.10.105.71 DST=172.17.0.4 LEN=60 TOS=0x10 PREC=0x00 TTL=62 ID=61480 DF PROTO=TCP SPT=41502 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0
Jan 18 13:35:55 pc-baim kernel: [278835.389339] [TonyBai]-NatPostrouteEnterIN= OUT=docker0 SRC=10.10.105.71 DST=172.17.0.4 LEN=60 TOS=0x10 PREC=0x00 TTL=62 ID=61480 DF PROTO=TCP SPT=41502 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0
</code></pre>
<p>接下来从docker0网卡收到container3的ack syn应答，在从eth0转发出去前自动un-DNAT， src ip从172.17.0.4变为101.0126.101，但这个在日志中看不出来。</p>
<pre><code>Jan 18 13:35:55 pc-baim kernel: [278835.389496] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=veth0d66af2 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=10.10.105.71 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=41502 WINDOW=28960 RES=0x00 ACK SYN URGP=0
Jan 18 13:35:55 pc-baim kernel: [278835.389519] [TonyBai]-FwdId0Ond0:IN=docker0 OUT=eth0 PHYSIN=veth0d66af2 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=10.10.105.71 LEN=60 TOS=0x00 PREC=0x00 TTL=63 ID=0 DF PROTO=TCP SPT=12580 DPT=41502 WINDOW=28960 RES=0x00 ACK SYN URGP=0
Jan 18 13:35:55 pc-baim kernel: [278835.389528] [TonyBai]-manglepost2IN= OUT=eth0 PHYSIN=veth0d66af2 SRC=172.17.0.4 DST=10.10.105.71 LEN=60 TOS=0x00 PREC=0x00 TTL=63 ID=0 DF PROTO=TCP SPT=12580 DPT=41502 WINDOW=28960 RES=0x00 ACK SYN URGP=0
</code></pre>
<p>回送ack，这回无需再匹配natprerouting链，前面进过链一次，后续自动进行DNAT：</p>
<pre><code>Jan 18 13:35:55 pc-baim kernel: [278835.390079] [TonyBai]-RawPrerouting:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:23:89:7d:b6:b1:08:00 SRC=10.10.105.71 DST=10.10.126.101 LEN=52 TOS=0x10 PREC=0x00 TTL=63 ID=61481 DF PROTO=TCP SPT=41502 DPT=12580 WINDOW=229 RES=0x00 ACK URGP=0
Jan 18 13:35:55 pc-baim kernel: [278835.390149] [TonyBai]-PortmapFowardDockerIN=eth0 OUT=docker0 MAC=2c:59:e5:01:98:28:00:23:89:7d:b6:b1:08:00 SRC=10.10.105.71 DST=172.17.0.4 LEN=52 TOS=0x10 PREC=0x00 TTL=62 ID=61481 DF PROTO=TCP SPT=41502 DPT=12580 WINDOW=229 RES=0x00 ACK URGP=0
</code></pre>
<p>这次我们看到，在这种方式下，外部流量也是通过DNAT方式导入到container中的。</p>
<h4>2、在宿主机上 telnet 10.10.126.101 12580</h4>
<p>telnet发起tcp握手，syn包进入output链，匹配到nat output规则，做DNAT。目的ip转换为172.17.0.4。注意继续向下，我们看iptables匹配到了NatPost1，也就是规则：</p>
<pre><code>-A LogNatPostRouting1 -o docker0 -m addrtype --src-type LOCAL -j MASQUERADE
</code></pre>
<p>即将源地址伪装为出口网卡docker0的当前地址：172.0.0.1。于是实际上进入到container3的syn数据包的源地址为172.0.0.1，目的地址：172.0.0.4。</p>
<pre><code>Jan 18 13:49:43 pc-baim kernel: [279663.426497] [TonyBai]-RawOutput:IN= OUT=lo SRC=10.10.126.101 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=40854 DF PROTO=TCP SPT=52736 DPT=12580 WINDOW=43690 RES=0x00 SYN URGP=0
Jan 18 13:49:43 pc-baim kernel: [279663.426526] [TonyBai]-PortmapNatPreroutinIN= OUT=lo SRC=10.10.126.101 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=40854 DF PROTO=TCP SPT=52736 DPT=12580 WINDOW=43690 RES=0x00 SYN URGP=0
Jan 18 13:49:43 pc-baim kernel: [279663.426545] [TonyBai]-FilterOutput:IN= OUT=lo SRC=10.10.126.101 DST=172.17.0.4 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=40854 DF PROTO=TCP SPT=52736 DPT=12580 WINDOW=43690 RES=0x00 SYN URGP=0
Jan 18 13:49:43 pc-baim kernel: [279663.426553] [TonyBai]-manglepost1IN= OUT=docker0 SRC=10.10.126.101 DST=172.17.0.4 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=40854 DF PROTO=TCP SPT=52736 DPT=12580 WINDOW=43690 RES=0x00 SYN URGP=0
Jan 18 13:49:43 pc-baim kernel: [279663.426561] [TonyBai]-NatPostrouteEnterIN= OUT=docker0 SRC=10.10.126.101 DST=172.17.0.4 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=40854 DF PROTO=TCP SPT=52736 DPT=12580 WINDOW=43690 RES=0x00 SYN URGP=0
Jan 18 13:49:43 pc-baim kernel: [279663.426567] [TonyBai]-NatPost1IN= OUT=docker0 SRC=10.10.126.101 DST=172.17.0.4 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=40854 DF PROTO=TCP SPT=52736 DPT=12580 WINDOW=43690 RES=0x00 SYN URGP=0
</code></pre>
<p>container3返回ack，从宿主机角度来看，相当于从docker0网卡收到ack。我们看到进来的原始数据：dst =  172.17.0.1，这是上面MASQUERADE的作用。在进入input链前，做自动un-SNAT，目的地址由172.17.0.1转换为10.10.126.101。在真正送到user层之前（output链等同的左边同纬度位置），做自动un-DNAT(但在下面日志中看不出来)，src由172.17.0.4变为10.10.126.101。数据包的变换总体次序依次为：即DNAT -> SNAT -> (应答包)un-SNAT -> un-DNAT。</p>
<pre><code>Jan 18 13:49:43 pc-baim kernel: [279663.426646] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=veth0d66af2 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=172.17.0.1 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=52736 WINDOW=28960 RES=0x00 ACK SYN URGP=0
Jan 18 13:49:43 pc-baim kernel: [279663.426665] [TonyBai]-FilterInput:IN=docker0 OUT= PHYSIN=veth0d66af2 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=10.10.126.101 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=52736 WINDOW=28960 RES=0x00 ACK SYN URGP=0
</code></pre>
<p>宿主机回复ack，握手完成。由于之前走过nat output和post链，因此这里不会再匹配，而是自动DNAT和SNAT：</p>
<pre><code>Jan 18 13:49:43 pc-baim kernel: [279663.426690] [TonyBai]-RawOutput:IN= OUT=lo SRC=10.10.126.101 DST=10.10.126.101 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=40855 DF PROTO=TCP SPT=52736 DPT=12580 WINDOW=342 RES=0x00 ACK URGP=0
Jan 18 13:49:43 pc-baim kernel: [279663.426707] [TonyBai]-FilterOutput:IN= OUT=lo SRC=10.10.126.101 DST=172.17.0.4 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=40855 DF PROTO=TCP SPT=52736 DPT=12580 WINDOW=342 RES=0x00 ACK URGP=0
Jan 18 13:49:43 pc-baim kernel: [279663.426719] [TonyBai]-manglepost1IN= OUT=docker0 SRC=10.10.126.101 DST=172.17.0.4 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=40855 DF PROTO=TCP SPT=52736 DPT=12580 WINDOW=342 RES=0x00 ACK URGP=0
</code></pre>
<h4>3、从container1 telnet 10.10.126.101 12580</h4>
<p>container1向服务发起tcp连接，宿主机从docker0网卡收到sync包。</p>
<pre><code>Jan 18 13:51:10 pc-baim kernel: [279750.806496] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=veth44a97d7 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=31888 DF PROTO=TCP SPT=54408 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0
Jan 18 13:51:10 pc-baim kernel: [279750.806519] [TonyBai]-NatPrerouting:IN=docker0 OUT= PHYSIN=veth44a97d7 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=31888 DF PROTO=TCP SPT=54408 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0
Jan 18 13:51:10 pc-baim kernel: [279750.806531] [TonyBai]-PortmapNatPreroutinIN=docker0 OUT= PHYSIN=veth44a97d7 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=31888 DF PROTO=TCP SPT=54408 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0
</code></pre>
<p>做DNAT后，再次路由到docker0，于是走forward链，但是没有匹配上nat postrouting，也就没有做SNAT：</p>
<pre><code>Jan 18 13:51:10 pc-baim kernel: [279750.806581] [TonyBai]-FwdId0Od0:IN=docker0 OUT=docker0 PHYSIN=veth44a97d7 PHYSOUT=veth0d66af2 MAC=02:42:ac:11:00:04:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=172.17.0.4 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=31888 DF PROTO=TCP SPT=54408 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0
Jan 18 13:51:10 pc-baim kernel: [279750.806608] [TonyBai]-NatPostrouteEnterIN= OUT=docker0 PHYSIN=veth44a97d7 PHYSOUT=veth0d66af2 SRC=172.17.0.2 DST=172.17.0.4 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=31888 DF PROTO=TCP SPT=54408 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0
</code></pre>
<p>container3回复ack sync。宿主机从docker0收到ack sync包，目的地址172.17.0.2，再次路由到docker0。</p>
<pre><code>Jan 18 13:51:10 pc-baim kernel: [279750.806719] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=veth0d66af2 MAC=02:42:ac:11:00:02:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=172.17.0.2 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=54408 WINDOW=28960 RES=0x00 ACK SYN URGP=0
Jan 18 13:51:10 pc-baim kernel: [279750.806746] [TonyBai]-FwdOd0:IN=docker0 OUT=docker0 PHYSIN=veth0d66af2 PHYSOUT=veth44a97d7 MAC=02:42:ac:11:00:02:02:42:ac:11:00:04:08:00 SRC=172.17.0.4 DST=172.17.0.2 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=54408 WINDOW=28960 RES=0x00 ACK SYN URGP=0
</code></pre>
<p>由于之前docker0上做过DNAT，因此从docker0回到172.17.0.2时，src地址会自动un-DNAT，从172.17.0.4改为10.10.126.101，不过在上面日志中看不出这一点。</p>
<p>172.17.0.2回复ack，握手完成，DNAT自动进行：</p>
<pre><code>Jan 18 13:51:10 pc-baim kernel: [279750.806823] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=veth44a97d7 MAC=02:42:23:39:fd:f5:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.101 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=31889 DF PROTO=TCP SPT=54408 DPT=12580 WINDOW=229 RES=0x00 ACK URGP=0
Jan 18 13:51:10 pc-baim kernel: [279750.806852] [TonyBai]-FwdOd0:IN=docker0 OUT=docker0 PHYSIN=veth44a97d7 PHYSOUT=veth0d66af2 MAC=02:42:ac:11:00:04:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=172.17.0.4 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=31889 DF PROTO=TCP SPT=54408 DPT=12580 WINDOW=229 RES=0x00 ACK URGP=0
</code></pre>
<h3>三、网络性能考量</h3>
<p>docker-proxy常被docker使用者诟病，一是因为每个映射端口都要启动一个docker-proxy进程，映射端口多了，大量进程被创建、被调度势必消耗大量系统资源；二来，在高负载场合，docker-proxy的转发性能也力不从心。理论上，docker-proxy代理转发流量的方式在性能方面要比单纯iptables DNAT要弱上一些。不过我在单机上通过<a href="https://github.com/chrissnell/sparkyfish">sparkyfish</a>测试的结果倒是二者相差不大，估计是因为我仅仅启动了一个docker-proxy，系统负荷并不大的缘故。</p>
<p style='text-align:left'>&copy; 2016, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2016/01/18/understanding-binding-docker-container-ports-to-host/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>理解Docker单机容器网络</title>
		<link>https://tonybai.com/2016/01/15/understanding-container-networking-on-single-host/</link>
		<comments>https://tonybai.com/2016/01/15/understanding-container-networking-on-single-host/#comments</comments>
		<pubDate>Fri, 15 Jan 2016 04:43:18 +0000</pubDate>
		<dc:creator>bigwhite</dc:creator>
				<category><![CDATA[技术志]]></category>
		<category><![CDATA[aufs]]></category>
		<category><![CDATA[bridge]]></category>
		<category><![CDATA[cgroups]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[coreos]]></category>
		<category><![CDATA[DevOps]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[firewall]]></category>
		<category><![CDATA[flannel]]></category>
		<category><![CDATA[gateway]]></category>
		<category><![CDATA[github]]></category>
		<category><![CDATA[Go]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[ifconfig]]></category>
		<category><![CDATA[iproute2]]></category>
		<category><![CDATA[iptables]]></category>
		<category><![CDATA[Kernel]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[mesos]]></category>
		<category><![CDATA[NAT]]></category>
		<category><![CDATA[netperf]]></category>
		<category><![CDATA[netstat]]></category>
		<category><![CDATA[networking]]></category>
		<category><![CDATA[rancher]]></category>
		<category><![CDATA[Router]]></category>
		<category><![CDATA[sparkyfish]]></category>
		<category><![CDATA[switch]]></category>
		<category><![CDATA[tcpip]]></category>
		<category><![CDATA[telnet]]></category>
		<category><![CDATA[traceroute]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[交换机]]></category>
		<category><![CDATA[内核]]></category>
		<category><![CDATA[容器]]></category>
		<category><![CDATA[网关]]></category>
		<category><![CDATA[网桥]]></category>
		<category><![CDATA[网络]]></category>
		<category><![CDATA[路由器]]></category>
		<category><![CDATA[防火墙]]></category>

		<guid isPermaLink="false">http://tonybai.com/?p=1930</guid>
		<description><![CDATA[Docker容器是近两年最 火的IT技术之一，用“火山爆发式“来形容Docker的成 长也不为过。Docker在产品服务的devops 运维、云 计算(CaaS)、大数据以及企业内部应用等领域正在被越来越多的接受和广泛应用。Docker技术的本质在于提升计算密度和提升部署效率，高屋 建瓴的讲，它的出现符合人类社会对绿色发展的追求，降低资源消耗，提升资源的单位利用率。不过经历了两年多的发展，Docker依旧年轻，尚未成 熟，在集群调度、存储、网络、安全等方面，Docker依旧有很长的路要走。 在一年多以前，也就是Docker发布1.0后没几个月时，我曾经学习过一段时间的Docker，主要学习Docker的概念和基本使用方法。由于当时docker 还相对“稚嫩”，在产品和项目中暂无用武之地，也就没有深入，但对Docker技术的跟踪倒是没有停下来。今年Docker 1.9发布，支持跨主机container netwoking；第三方容器集群调度和服务编织工具蓬勃发展，如Kubernetes 、mesos、 flannel以及rancher等；国内基于Docker的云服 务及产品也 如雨后春笋般发展开来。虽然不到2年，但Docker的演进速度是飞快的，要想跟的上Docker的步伐，仅仅跟踪技术信息是不够的，对伴生 Docker发展起来的一些新理念、新技术、新方案需要更深入的理解，这便是这篇文章（以及后续关于这个主题文章）编写的初衷。 我计划从容器网络开始，我们先来看看单机容器网络。 一、目标 Docker实质上是汇集了linux容器（各种namespaces）、cgroups以及“叠加”类文件系统等多种核心技术的一种复合技术。 其默认容器网络的建立和控制是一种结合了network namespace、iptables、linux网桥、route table等多种Linux内核技术的综合方案。理解Docker容器网络，首先是以对TCP/IP网络体系的理解为前提的，不过也不需要多深刻，大学本 科学的那套“计算机网络”足矣^_^，另外还要考虑Linux上对虚拟网络设备实现的独特性（区分于硬件网络设备）。 本篇文章主要针对单机Docker容器网络，目的是了解Docker容器网络中容器与容器间通信、容器与宿主机间通信、容器与宿主机所在的物理网 络中主机通信、容器网络控制等机制，为后续理解跨主机容器网络的理解打下基础。同时稍带利用工具对Docker容器网络的网络性能做初步测量，通 过直观数据初步评估容器网络的适用性。 二、试验环境以及拓扑 本文试验环境如下： - 宿主机 Ubuntu 12.04 x86_64 3.13.0-61-generic - 容器OS：基于Ubuntu 14.04 Server x86_64的自制image - Docker版本 - v1.9.1 for linux/amd64 为了试验方便，这里基于官方ubuntu:14.04 image制作了带有traceroute、brctl以及tcpdump等网络调试工具的image，简单起见（考虑到公司内网代理），这里就没有写 Dockerfile(即便写也很简单)，而是直接z在容器内apt-get install后，再通过docker commit基于已经安装好上述工具的container创建的一个新image： $sudo docker commit 0580adb079a3 dockernetworking/ubuntu:14.04 [...]]]></description>
			<content:encoded><![CDATA[<p><a href="https://www.docker.com/">Docker</a>容器是近两年最 火的IT技术之一，用“火山爆发式“来形容Docker的成 长也不为过。Docker在产品服务的<a href="https://en.wikipedia.org/wiki/DevOps">devops</a> 运维、云 计算(CaaS)、大数据以及企业内部应用等领域正在被越来越多的接受和广泛应用。Docker技术的本质在于提升计算密度和提升部署效率，高屋 建瓴的讲，它的出现符合人类社会对绿色发展的追求，降低资源消耗，提升资源的单位利用率。不过经历了两年多的发展，Docker依旧年轻，尚未成 熟，在集群调度、存储、网络、安全等方面，Docker依旧有很长的路要走。</p>
<p>在一年多以前，也就是<a href="http://blog.docker.com/2014/06/its-here-docker-1-0/">Docker发布1.0</a>后没几个月时，我曾经学习过一段时间的<a href="http://tonybai.com/tag/docker">Docker</a>，主要学习Docker的概念和基本使用方法。由于当时docker 还相对“稚嫩”，在产品和项目中暂无用武之地，也就没有深入，但对Docker技术的跟踪倒是没有停下来。今年<a href="https://blog.docker.com/2015/11/docker-1-9-production-ready-swarm-multi-host-networking/">Docker 1.9发布</a>，支持跨主机container netwoking；第三方容器集群调度和服务编织工具蓬勃发展，如<a href="http://kubernetes.io/">Kubernetes</a> 、<a href="http://mesos.apache.org/">mesos</a>、 <a href="https://github.com/coreos/flannel">flannel</a>以及<a href="http://rancher.com/">rancher</a>等；国内基于Docker的云服 务及产品也 如雨后春笋般发展开来。虽然不到2年，但Docker的演进速度是飞快的，要想跟的上Docker的步伐，仅仅跟踪技术信息是不够的，对伴生 Docker发展起来的一些新理念、新技术、新方案需要更深入的理解，这便是这篇文章（以及后续关于这个主题文章）编写的初衷。</p>
<p>我计划从容器网络开始，我们先来看看单机容器网络。</p>
<h3>一、目标</h3>
<p>Docker实质上是汇集了linux容器（各种namespaces）、cgroups以及“叠加”类文件系统等多种核心技术的一种复合技术。 其默认容器网络的建立和控制是一种结合了network namespace、iptables、linux网桥、route table等多种Linux内核技术的综合方案。理解Docker容器网络，首先是以对TCP/IP网络体系的理解为前提的，不过也不需要多深刻，大学本 科学的那套“计算机网络”足矣^_^，另外还要考虑Linux上对虚拟网络设备实现的独特性（区分于硬件网络设备）。</p>
<p>本篇文章主要针对单机Docker容器网络，目的是了解Docker容器网络中容器与容器间通信、容器与宿主机间通信、容器与宿主机所在的物理网 络中主机通信、容器网络控制等机制，为后续理解跨主机容器网络的理解打下基础。同时稍带利用工具对Docker容器网络的网络性能做初步测量，通 过直观数据初步评估容器网络的适用性。</p>
<h3>二、试验环境以及拓扑</h3>
<p>本文试验环境如下：</p>
<pre><code>- 宿主机 Ubuntu 12.04 x86_64 3.13.0-61-generic
- 容器OS：基于Ubuntu 14.04 Server x86_64的自制image
- Docker版本 - v1.9.1 for linux/amd64
</code></pre>
<p>为了试验方便，这里基于官方<a href="http://tonybai.com/tag/ubuntu">ubuntu</a>:14.04 image制作了带有traceroute、brctl以及tcpdump等网络调试工具的image，简单起见（考虑到公司内网代理），这里就没有写 Dockerfile(即便写也很简单)，而是直接z在容器内apt-get install后，再通过docker commit基于已经安装好上述工具的container创建的一个新image：</p>
<pre><code>$sudo docker commit 0580adb079a3 dockernetworking/ubuntu:14.04
a692757cbb7bd7d8b70f393930e954cce625934485e93cf1b28c15efedb5f2d3
$ docker images
REPOSITORY                TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
dockernetworking/ubuntu   14.04               a692757cbb7b        5 seconds ago       302.1 MB
</code></pre>
<p>后续的container均是基于dockernetworking/ubuntu创建的。</p>
<p>另外试验环境的拓扑图如下：</p>
<p><img src="http://tonybai.com/wp-content/uploads/docker-single-host-networking-topological-diagram.jpg" alt="img{500x428}" /></p>
<p>从拓扑图中我们可以看到，物理宿主机为10.10.126.101，置于物理局域网10.10.126.0/24中。在宿主机上我们创建了两 个 Container：Container1和Container2，Container所用网段为172.17.0.0/16。</p>
<h3>三、Docker Daemon初始网络</h3>
<p>当你在一个clean环境下，启动Docker daemon后，比如在Ubuntu下，使用sudo service docker start，Docker Daemon就会初始化后续创建容器时所需的基础网络设备和配置。</p>
<p>以下是从宿主机的角度看到的：</p>
<pre><code>// 网桥
$ brctl show
bridge name    bridge id        STP enabled    interfaces
docker0        8000.0242f9f8c9ad    no

// 网络设备
$ ip link show
1: lo: &lt;LOOPBACK,UP,LOWER_UP&gt; mtu 65536 qdisc noqueue state UNKNOWN
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 2c:59:e5:01:98:28 brd ff:ff:ff:ff:ff:ff
... ...
5: docker0: &lt;NO-CARRIER,BROADCAST,MULTICAST,UP&gt; mtu 1500 qdisc noqueue state DOWN
    link/ether 02:42:f9:f8:c9:ad brd ff:ff:ff:ff:ff:ff

// 网络设备ip地址
$ ip addr show
1: lo: &lt;LOOPBACK,UP,LOWER_UP&gt; mtu 65536 qdisc noqueue state UNKNOWN
    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
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: eth0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 2c:59:e5:01:98:28 brd ff:ff:ff:ff:ff:ff
    inet 10.10.126.101/24 brd 10.10.126.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::2e59:e5ff:fe01:9828/64 scope link
       valid_lft forever preferred_lft forever
... ...
5: docker0: &lt;NO-CARRIER,BROADCAST,MULTICAST,UP&gt; mtu 1500 qdisc noqueue state DOWN
    link/ether 02:42:f9:f8:c9:ad brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:f9ff:fef8:c9ad/64 scope link
       valid_lft forever preferred_lft forever

</code></pre>
<p>可以看出，与Docker Daemon启动前相比，宿主物理机中多出来一个虚拟网络设备：docker0。</p>
<p>docker0是一个标准Linux虚拟网桥设备。在Docker默认的桥接网络工作模式中，docker0网桥起到了至关重要的作用。物理网桥 是标准的二层网络设备，一般说，标准物理网桥只有两个网口，可以将两个物理网络（区分以IP为寻址单位的逻辑网络）连接在一起。但与物理层设备集 线器等相比，网桥具备隔离冲突域的功能。网桥通过MAC地址学习和泛洪的方式实现二层相对高效的通信。在今天，标准网桥设备已经基本被淘汰了，替 代网桥的是是二层交换机。二层交换机也可以看成一个多口网桥。在不划分vlan的前提下，可以将其当做两两端口间都是独立通道的”hub”使用。</p>
<p>前面说过docker0是一个标准Linux虚拟网桥设备，即一个以软件实现的网桥，由于其支持多口，实际上它算是一个虚拟交换机设备。与物理网 桥不同的是，它不但可以二层转发包，还可以将包送到用户层进行处理。在我们尚未创建container的时候，docker0以一个Linux网 络设 备的身份存在，并且Linux虚拟网桥可以配置IP，可以作为在三层网络上的一个Gateway，在主机眼中和物理网口设备eth0区别不大。与 Linux其他网络设备也可以在三层相互通信，前提是Docker Daemon打开了ip包转发功能：</p>
<pre><code>$ cat /proc/sys/net/ipv4/ip_forward
1
</code></pre>
<p>宿主机的路由表也增加了一条路由(见最后一条)：</p>
<pre><code>$ ip route
default via 10.10.126.1 dev eth0  proto static
10.10.126.0/24 dev eth0  proto kernel  scope link  src 10.10.126.101  metric 1
172.17.0.0/16 dev docker0  proto kernel  scope link  src 172.17.0.1
</code></pre>
<p>除此之外，Docker Daemon还设置了若干iptables规则以管理containers间的通信以及辅助container访问外部网络（NAT转换）：</p>
<pre><code>sudo iptables-save &gt; ./iptables.init.rules

# Generated by iptables-save v1.4.12 on Wed Jan 13 17:25:55 2016
*raw
: PREROUTING ACCEPT [9469:2320376]
:OUTPUT ACCEPT [2990:1335235]
COMMIT
# Completed on Wed Jan 13 17:25:55 2016
# Generated by iptables-save v1.4.12 on Wed Jan 13 17:25:55 2016
*filter
:INPUT ACCEPT [1244:341290]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [483:153047]
: DOCKER - [0:0]
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -i docker0 ! -o docker0 -j ACCEPT
-A FORWARD -i docker0 -o docker0 -j ACCEPT
COMMIT
# Completed on Wed Jan 13 17:25:55 2016
# Generated by iptables-save v1.4.12 on Wed Jan 13 17:25:55 2016
*nat
: PREROUTING ACCEPT [189:88629]
:INPUT ACCEPT [111:60817]
:OUTPUT ACCEPT [23:1388]
: POSTROUTING ACCEPT [23:1388]
: DOCKER - [0:0]
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
COMMIT
# Completed on Wed Jan 13 17:25:55 2016

</code></pre>
<p><a href="https://en.wikipedia.org/wiki/iptables">iptables</a>是Linux内核自带的包过滤防火墙，支持<a href="https://en.wikipedia.org/wiki/Network_address_translation">NAT</a>等诸多功能。iptables由表和规则chain概念组成，Docker中所 用的表包括filter表和nat表（参见上述命令输出结果），这也是iptables中最常用的两个表。iptables是一个复杂的存在，曾 有一本书《<a href="http://book.douban.com/subject/2148593/">linux firewalls</a>》 专门讲解iptables，这里先借用本书 中的一幅图来描述一下ip packets在各个表和chain之间的流转过程：</p>
<p><img src="http://tonybai.com/wp-content/uploads/docker-single-host-networking-iptables.jpg" alt="img{500x165}" /></p>
<p>网卡收到的数据包进入到iptables后，做路由选择，本地的包通过INPUT链送往user层应用；转发到其他网口的包通过FORWARD chain；本地产生的数据包在路由选择后，通过OUTPUT chain；最后POSTROUTING chain多用于source nat转换。</p>
<p>iptables在容器网络中最重要的两个功能：</p>
<p>1、限制container间的通信<br />
2、将container到外部网络包的源地址换成宿主主机地址(MASQUERADE)</p>
<p>后续还会在详细描述容器通信流程中还会掺杂说明iptables的规则在容器通信中的作用。</p>
<h3>四、准备工作：让iptables输出log</h3>
<p>iptables在Docker单机容器默认网络工作模式下扮演着重要的角色，并且由于是虚拟设备网络，数据的流转是十分复杂的，为了便于跟踪 iptables在docker容器网络数据通信过程中起到的作用，这里在默认iptables规则的基础上，做一些调整，在关键位置输出一些 log，以便调试和理解，这些修改不会影响iptables对数据包的匹配和操作。注意：在操作iptables前，建议通过iptables- save命令备份一份iptables的配置数据。</p>
<p>iptables自身就支持LOG target，日志会输出到/var/log/syslog或kern.log中。我们的目标就是在关键节点输出iptables的数据日志。考虑到日志 量较大，我们仅拦截icmp包（ping)以及tcp 源端口或目的端口为12580的数据。</p>
<p>考虑到篇幅有限，这里仅给出配置后导出的iptables.final.rules，需要的同学可以通过iptables-restore &lt; iptables.final.rules导入。</p>
<pre><code># Generated by iptables-save v1.4.12 on Thu Jan 14 09:28:43 2016
*raw
: PREROUTING ACCEPT [788:127290]
:OUTPUT ACCEPT [574:100918]
-A PREROUTING -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-RawPrerouting:" --log-level 7
-A PREROUTING -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-RawPrerouting:" --log-level 7
-A OUTPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-RawOutput:" --log-level 7
-A OUTPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-RawOutput:" --log-level 7
COMMIT
# Completed on Thu Jan 14 09:28:43 2016
# Generated by iptables-save v1.4.12 on Thu Jan 14 09:28:43 2016
*filter
:INPUT ACCEPT [284:49631]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [81:28047]
: DOCKER - [0:0]
:FwdId0Od0 - [0:0]
:FwdId0Ond0 - [0:0]
:FwdOd0 - [0:0]
-A INPUT ! -i lo -p icmp -j LOG --log-prefix "[TonyBai]-EnterFilterInput:" --log-level 7
-A INPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-FilterInput:" --log-level 7
-A INPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-FilterInput:" --log-level 7
-A FORWARD -o docker0 -j DOCKER
-A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j FwdOd0
-A FORWARD -i docker0 ! -o docker0 -j FwdId0Ond0
-A FORWARD -i docker0 -o docker0 -j FwdId0Od0
-A OUTPUT ! -s 127.0.0.1/32 -p icmp -j LOG --log-prefix "[TonyBai]-EnterFilterOutput:" --log-level 7
-A OUTPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-FilterOutput:" --log-level 7
-A OUTPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-FilterOutput:" --log-level 7
-A FwdId0Od0 -i docker0 -o docker0 -j LOG --log-prefix "[TonyBai]-FwdId0Od0:" --log-level 7
-A FwdId0Od0 -i docker0 -o docker0 -j ACCEPT
-A FwdId0Ond0 -i docker0 ! -o docker0 -j LOG --log-prefix "[TonyBai]-FwdId0Ond0:" --log-level 7
-A FwdId0Ond0 -i docker0 ! -o docker0 -j ACCEPT
-A FwdOd0 -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j LOG --log-prefix "[TonyBai]-FwdOd0:" --log-level 7
-A FwdOd0 -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
COMMIT
# Completed on Thu Jan 14 09:28:43 2016
# Generated by iptables-save v1.4.12 on Thu Jan 14 09:28:43 2016
*nat
: PREROUTING ACCEPT [37:6070]
:INPUT ACCEPT [20:2585]
:OUTPUT ACCEPT [6:364] <img src='https://tonybai.com/wp-includes/images/smilies/icon_razz.gif' alt=':P' class='wp-smiley' /> OSTROUTING ACCEPT [6:364]
: DOCKER - [0:0]
:LogNatPostRouting - [0:0]
-A PREROUTING -p icmp -j LOG --log-prefix "[TonyBai]-Enter iptables:" --log-level 7
-A PREROUTING -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-NatPrerouting:" --log-level 7
-A PREROUTING -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-NatPrerouting:" --log-level 7
-A INPUT ! -i lo -p icmp -j LOG --log-prefix "[TonyBai]-EnterNatInput:" --log-level 7
-A INPUT -p tcp -m tcp --dport 12580 -j LOG --log-prefix "[TonyBai]-NatInput:" --log-level 7
-A INPUT -p tcp -m tcp --sport 12580 -j LOG --log-prefix "[TonyBai]-NatInput:" --log-level 7
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j LogNatPostRouting
-A LogNatPostRouting -s 172.17.0.0/16 ! -o docker0 -j LOG --log-prefix "[TonyBai]-NatPostRouting:" --log-level 7
-A LogNatPostRouting -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
COMMIT
# Completed on Thu Jan 14 09:28:43 2016

</code></pre>
<p>一切就绪，只待对docker网络的分析了。</p>
<h3>五、容器网络</h3>
<p>现在我们来启动容器。根据试验环境拓扑图，我们需要创建和启动两个容器：container1和container2。</p>
<pre><code>$ docker run -it --name container1 dockernetworking/ubuntu:14.04 /bin/bash
$ docker run -it --name container2 dockernetworking/ubuntu:14.04 /bin/bash

$ docker ps
CONTAINER ID        IMAGE                           COMMAND             CREATED             STATUS              PORTS               NAMES
1104fc63c571        dockernetworking/ubuntu:14.04   "/bin/bash"         7 seconds ago       Up 6 seconds                            container2
8b38131deb28        dockernetworking/ubuntu:14.04   "/bin/bash"         16 seconds ago      Up 15 seconds                           container1
</code></pre>
<p>容器启动后，从宿主机的视角，可以看到网络配置有如下变化：</p>
<pre><code>$ brctl show
bridge name    bridge id        STP enabled    interfaces
docker0        8000.0242f9f8c9ad    no        veth00855d7
                            vethee8659f

$ifconfig -a
... ...
veth00855d7 Link encap:以太网  硬件地址 ea:70:65:cf:28:6b
          inet6 地址: fe80::e870:65ff:fecf:286b/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  跃点数:1
          接收数据包:8 错误:0 丢弃:0 过载:0 帧数:0
          发送数据包:37 错误:0 丢弃:0 过载:0 载波:0
          碰撞:0 发送队列长度:0
          接收字节:648 (648.0 B)  发送字节:5636 (5.6 KB)

vethee8659f Link encap:以太网  硬件地址 fa:30:bb:0b:1d:eb
          inet6 地址: fe80::f830:bbff:fe0b:1deb/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  跃点数:1
          接收数据包:61 错误:0 丢弃:0 过载:0 帧数:0
          发送数据包:82 错误:0 丢弃:0 过载:0 载波:0
          碰撞:0 发送队列长度:0
          接收字节:5686 (5.6 KB)  发送字节:9678 (9.6 KB)
... ...

</code></pre>
<p>Docker Daemon创建了两个veth网络设备，并将veth挂接到docker0网桥上了。veth是一种虚拟网卡设备，创建时成对(veth pair)出现，从一个veth peer发出的数据包可以到达其pair peer。不过从上面命令输出来看，我们似乎并没有看到veth pair，这是因为每个pair的另一peer被放到container的network namespace中了，变成了container中的eth0。veth pair常用于在不同网络命名空间之间通信。在拓扑图中，container1中的eth0与veth-x是一个pair；container2中的 eth0与veth-y是另一个pair。veth-x和veth-y挂接在docker0网桥上，这对于container1和 container2来说，就好比用网线将本地网卡(eth0)与网桥设备docker0的网口连接起来一样。在docker容器网络默认桥接模式 中，veth只是在二层起作用。</p>
<p>下面是从container1内部看到的网络配置：</p>
<pre><code>root@8b38131deb28:/# ip addr
1: lo: &lt;LOOPBACK,UP,LOWER_UP&gt; mtu 65536 qdisc noqueue state UNKNOWN group default
    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
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
47: eth0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.2/16 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:acff:fe11:2/64 scope link
       valid_lft forever preferred_lft forever

root@8b38131deb28:/# netstat -rn
Kernel IP routing table
Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface
0.0.0.0         172.17.0.1      0.0.0.0         UG        0 0          0 eth0
172.17.0.0      0.0.0.0         255.255.0.0     U         0 0          0 eth0
</code></pre>
<p>container网络配置很简单，一个eth0网卡，一个loopback口，route表里将网桥作为默认Gateway。</p>
<p>至此，我们拓扑图中的环境已经全部就绪。接下来我们来探索和理解一下容器网络的几种通信流程。</p>
<h3>六、Docker0的“双重身份”</h3>
<p>在正式进入每个通信流程前，我们先来点预备性内容 &#8211; 如何理解Docker0。下图中我们给出了Docker0的双重身份，并对比物理交换机，我们来理解一下Docker0这个软网桥。</p>
<p><img src="http://tonybai.com/wp-content/uploads/docker-single-host-networking-docker0.jpg" alt="img{500x165}" /></p>
<h4>1、从容器视角，网桥（交换机）身份</h4>
<p>docker0对于通过veth pair“插在”网桥上的container1和container2来说，首先就是一个二层的交换机的角色：泛洪、维护cam表，在二层转发数据包；同 时由于docker0自身也具有mac地址（这个与纯二层交换机不同），并且绑定了ip(这里是172.17.0.1)，因此在 container中还作为container default路由的默认Gateway而存在。</p>
<h4>2、从宿主机视角，网卡身份</h4>
<p>物理交换机提供了由硬件实现的高效的背板通道，供连接在交换机上的主机高效实现二层通信；对于开启了三层协议的物理交换机而言，其ip路由的处理 也是由物理交换机管理程序提供的。对于docker0而言，其负责处理二层交换机逻辑以及三层的处理程序其实就是宿主机上的Linux内核 tcp/ip协议栈程序。而从宿主机来看，所有docker0从veth（只是个二层的存在，没有绑定ipv4地址）接收到的数据包都会被宿主机 看成从docker0这块网卡（第二个身份，绑定172.17.0.1)接收进来的数据包，尤其是在进入三层时，宿主机上的iptables就会 对docker0进来的数据包按照rules进行相应处理（通过一些内核网络设置也可以忽略docker0 brigde数据的处理）。</p>
<p>在后续的Docker容器网络通信流程分析中，docker0将在这两种身份间来回切换。</p>
<h3>七、容器网络通信流程</h3>
<p>考虑到大部分tcp/ip实现都是在内核实现的ping服务器，这可能会导致iptables流程走不全，影响我们的理解，因此我这里通过tcp 连接建立的握手过程(sync, ack sync, ack)的通信包来理解container网络通信。我们可以简单在服务端启动一个python httpserver: python -m SimpleHTTPServer 12580或用<a href="http://tonybai.com/tag/go">Go</a>写个简单的http server来监听12580端口；客户端用telnet ip port的方式与服务端建立连接。</p>
<p>iptables的log我们可以在宿主机(ubuntu 12.04)的/var/log/syslog中查看到。考虑到篇幅，头两个例子会作详细说明，后续将简要阐述。</p>
<h4>1、container to container</h4>
<p>场景：我们在container2(172.17.0.3)中启动监听12580的服务程序，并在container1(172.17.0.2) 中执行：telnet 172.17.0.3 12580。</p>
<p>分析：</p>
<p>我们首先从container1的视角去看。</p>
<p>在container1中无需考虑iptables过程，可以理解为未开启。container1的用户层的数据进入该网络名字空间 (network namespace)的网络协议栈处理。在route decision过程中，协议栈处理程序发现目的地址匹配172.17.0.0/16这条网络路由，该条路由的Flag为U，即该网络为直连链路上的网 络，即无需使用Gateway，直接可以将数据包发到eth0上并封包发出去即可。</p>
<p>由于可以在直连网路链路上找到目的主机，于是二层欲填写的目的mac地址为172.17.0.3这个ip对应的mac。container1在 arp缓存中查询172.17.0.3对应的mac地址。如没有发现172.17.0.3这个ip地址对应的缓存mac地址，则发起一个arp请 求，arp请求的二层目的mac地址填写为二层广播地址：bit全1的mac地址（48bit），并通过eth0发出去。</p>
<p>docker0在这个过程中二层交换机的作用。接收到来自veth上的广播arp请求后，将请求通过二层网络转发到其他docker0上的 veth口上。这时container2收到了arp请求，container2上的以太网驱动程序收到arp请求后，将其发给 container2上的arp协议处理程序(不走iptables)，arp协议处理程序封装arp reply后转出。container1收到reply后，处理二层封包，将container2的mac地址填入以太网数据帧的目的mac地址字段中， 并发出。</p>
<p>上一节提到过，docker0收到container1发来的ip数据包，交由其处理程序，也就是linux内核协议栈处理程序处理，这时 docker0的身份开始转换了。</p>
<p>我们现在转换到宿主机视角。</p>
<p>从宿主机视角，docker0是一个mac地址为02:42:f9:f8:c9:ad，ip为172.17.0.1的网卡（网卡身份）。 container1发出的进入到docker0的包，对于host来说，就好比从docker0这块网卡设备进入到宿主机的数据包。当数据包进 入到三层时，iptables的处理规则就起了作用。我们看到在raw prerouting中的日志：</p>
<pre><code>Jan 14 10:08:12 pc-baim kernel: [830038.910054] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethd9f6465 MAC=02:42:ac:11:00:03:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=172.17.0.3 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=24284 DF PROTO=TCP SPT=43292 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0
</code></pre>
<p>这是第一个ip包，承载着tcp sync数据。按照iptables的数据流转，接下来的route decision发现目的地址是172.17.0.3，不是自身绑定的172.17.0.1，不用送到user层（不走input链），在host的路由 表中继续匹配路由表项，匹配到如下路由表项：172.17.0.0/16 dev docker0，于是走forward链：</p>
<pre><code>Jan 14 10:08:12 pc-baim kernel: [830038.910120] [TonyBai]-FwdId0Od0:IN=docker0 OUT=docker0 PHYSIN=vethd9f6465 PHYSOUT=vethfcceafa MAC=02:42:ac:11:00:03:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=172.17.0.3 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=24284 DF PROTO=TCP SPT=43292 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0
</code></pre>
<p>这又是一个直连网络，无需Gateway作为下一跳，于是再从docker0将数据送出。</p>
<p>docker0送出时，docker0又回到二层功能范畴。在cam表中查找mac地址02:42:ac:11:00:03对应的网口 vethfcceafa，将数据从vethfcceafa送出去。根据veth pair的描述，container2中的eth0将收到这份数据。container2发现数据包中目的地址是172.17.0.3，就是自身eth0 的地址，于是送到user层处理。</p>
<p>接下来是container 3 回复ack sync的过程。与上面类似，container3通过直连网络将数据包发给docker0。从host视角看，数据包从docker0这个网卡设备进 来：</p>
<pre><code>Jan 14 10:08:12 pc-baim kernel: [830038.910200] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethfcceafa MAC=02:42:ac:11:00:02:02:42:ac:11:00:03:08:00 SRC=172.17.0.3 DST=172.17.0.2 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=43292 WINDOW=28960 RES=0x00 ACK SYN URGP=0
</code></pre>
<p>route decision，由于目的地址不是docker0自身的目的地址，匹配路由条目：172.17.0.0/16 dev docker0，于是走forward链。这次在iptables forward链中匹配到的rules是：FwdOd0</p>
<p>Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)<br />
 pkts bytes target     prot opt in     out     source               destination<br />
    6   328 DOCKER     all  &#8212;  *      docker0  0.0.0.0/0            0.0.0.0/0<br />
    5   268 FwdOd0     all  &#8212;  *      docker0  0.0.0.0/0            0.0.0.0/0            ctstate RELATED,ESTABLISHED<br />
&#8230; &#8230;</p>
<p>因为这次是conn established相关的链路上回包，日志如下：</p>
<pre><code>Jan 14 10:08:12 pc-baim kernel: [830038.910230] [TonyBai]-FwdOd0:IN=docker0 OUT=docker0 PHYSIN=vethfcceafa PHYSOUT=vethd9f6465 MAC=02:42:ac:11:00:02:02:42:ac:11:00:03:08:00 SRC=172.17.0.3 DST=172.17.0.2 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=43292 WINDOW=28960 RES=0x00 ACK SYN URGP=0
</code></pre>
<p>于是ack sync再从docker0送出。docker0送出时封装包时回到二层功能范畴。在cam表中查找mac地址02:42:ac:11:00:02对应的 网口vethd9f6465，将数据从vethd9f6465送出去。根据veth pair的描述，container1中的eth0将收到这份数据包。container1发现数据包中目的地址是172.17.0.2，就是自身 eth0的地址，于是送到user层处理。</p>
<p>container1接下来的回送ack过程与sync过程类似，这里就不赘述了。</p>
<h4>2、container to docker0</h4>
<p>场景：我在container1(172.17.0.2)中执行：telnet 172.17.0.1 12580。docker0所在宿主机上并没有程序在监听12580端口，因此这个tcp连接是无法建立起来的。sync过去后，对方返回ack rst，而不是ack sync。</p>
<p>分析：</p>
<p>我们首先从container1的视角去看。</p>
<p>container1向172.17.0.1建立连接，在路由decision后，发现目标主机在直连网络中，于是将对方mac地址封装到二层协 议帧中后通过eth0将包转出。docker0收到包后，送到宿主机网络协议栈，也就是docker0的管理程序去处理。</p>
<p>切换到宿主机视角。宿主机从网卡docker0获取数据包，宿主机网络协议栈处理数据包，进入iptables中：</p>
<pre><code>Jan 14 12:53:02 pc-baim kernel: [839935.434253] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=172.17.0.1 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=29166 DF PROTO=TCP SPT=41362 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0
</code></pre>
<p>路由decision后发现目的地址就是docker0自己的地址(172.17.0.1)，要送给user层，于是走filter input链：</p>
<pre><code>Jan 14 12:53:02 pc-baim kernel: [839935.434309] [TonyBai]-FilterInput:IN=docker0 OUT= PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=172.17.0.1 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=29166 DF PROTO=TCP SPT=41362 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0
</code></pre>
<p>送到user层后，user层发现没有程序监听12580端口，于是向下发出ack rst包。数据包重新路由后，发现是直连网络，从docker0口出。但出去之前需要先进入iptables的filter output链：</p>
<pre><code>Jan 14 12:53:02 pc-baim kernel: [839935.434344] [TonyBai]-FilterOutput:IN= OUT=docker0 SRC=172.17.0.1 DST=172.17.0.2 LEN=40 TOS=0x10 PREC=0x00 TTL=64 ID=781 DF PROTO=TCP SPT=12580 DPT=41362 WINDOW=0 RES=0x00 ACK RST URGP=0
</code></pre>
<p>数据包从docker0进入后，docker0承担网桥角色，在二层转发给container1，结束处理。</p>
<h4>3、container to host</h4>
<p>场景：我在container1(172.17.0.2)中执行：telnet 10.10.126.101 12580。docker0所在宿主机上启动服务程序在监听12580端口，因此这是个标准tcp连接建立过程（sync, ack sync, ack）。</p>
<p>分析：</p>
<p>我们首先从container1的视角去看。</p>
<p>container1在经过路由判断后，匹配到default路由，需要走gateway(flags = UG)，于是将目的mac填写为Gateway 172.0.0.1的mac地址，将包通过eth0转给Gateway，即docker0。</p>
<p>切换到宿主机视角。</p>
<p>宿主机从网卡docker0收到一个数据包，进入iptables：</p>
<pre><code>Jan 14 14:11:28 pc-baim kernel: [844644.563436] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=55780 DF PROTO=TCP SPT=59373 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0
</code></pre>
<p>路由decision，由于目的地址是10.10.126.101，docker0的管理程序，也就是host的linux网络栈处理程序发现这 不是我自己么（虽然是从 docker0收到的，但网络栈程序知道172.0.0.1和10.10.126.101都是自己），于是user层收下了这个包。因此在路由 后，数据包走到filter input:</p>
<pre><code>Jan 14 14:11:28 pc-baim kernel: [844644.563476] [TonyBai]-FilterInput:IN=docker0 OUT= PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.101 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=55780 DF PROTO=TCP SPT=59373 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0
</code></pre>
<p>user层监听12580的服务程序收到包后，回复ack syn到172.17.0.2，路由Decision后，发现在直连网络中，通过docker0转出，于是走iptable filter output。</p>
<pre><code>Jan 14 14:11:28 pc-baim kernel: [844644.563519] [TonyBai]-FilterOutput:IN= OUT=docker0 SRC=10.10.126.101 DST=172.17.0.2 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=59373 WINDOW=28960 RES=0x00 ACK SYN URGP=0
</code></pre>
<p>container1收到ack syn后再回复ack，路径与sync一致，日志如下：</p>
<pre><code>Jan 14 14:11:28 pc-baim kernel: [844644.563566] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.101 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=55781 DF PROTO=TCP SPT=59373 DPT=12580 WINDOW=229 RES=0x00 ACK URGP=0
Jan 14 14:11:28 pc-baim kernel: [844644.563584] [TonyBai]-FilterInput:IN=docker0 OUT= PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.101 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=55781 DF PROTO=TCP SPT=59373 DPT=12580 WINDOW=229 RES=0x00 ACK URGP=0
</code></pre>
<h4>4、host to container</h4>
<p>场景：我在宿主机(10.10.126.101)中执行：telnet 172.17.0.2  12580。container1上启动服务程序在监听12580端口，因此这是个标准tcp连接建立过程（sync, ack sync, ack）。</p>
<p>分析：</p>
<p>这次我们首先从宿主机角度出发。</p>
<p>host的telnet程序在用户层产生数据包，经路由decision，匹配直连网络路由，出口docker0，然后进入iptables的 filter output链：</p>
<pre><code>Jan 14 14:19:25 pc-baim kernel: [845121.897441] [TonyBai]-FilterOutput:IN= OUT=docker0 SRC=172.17.0.1 DST=172.17.0.2 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=51756 DF PROTO=TCP SPT=44120 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0
</code></pre>
<p>你会发现在这个log中，数据包的src ip地址为172.17.0.1，这是协议栈处理程序的选择，没有选择10.10.126.101，这些地址都标识host自己。</p>
<p>container1在收到sync后，回复ack sync，这就相当于container to host。host这次从docker0收到目的为172.17.0.1的ack sync包 , 走的是filer input，这里不赘述。</p>
<pre><code>Jan 14 14:19:25 pc-baim kernel: [845121.897552] [TonyBai]-FilterInput:IN=docker0 OUT= PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=172.17.0.1 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=44120 WINDOW=28960 RES=0x00 ACK SYN URGP=0
</code></pre>
<p>host再回复ack，与sync相同，走filter output链，不赘述。</p>
<pre><code>Jan 14 14:19:25 pc-baim kernel: [845121.897588] [TonyBai]-FilterOutput:IN= OUT=docker0 SRC=172.17.0.1 DST=172.17.0.2 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=51757 DF PROTO=TCP SPT=44120 DPT=12580 WINDOW=229 RES=0x00 ACK URGP=0
</code></pre>
<h4>5、container to 10.10.126.187</h4>
<p>场景：我们在container1中向与宿主机直接网络的主机10.10.126.187建立连接。我在container1中执 行：telnet 10.10.126.187 12580。187上启动服务程序在监听12580端口，因此这是个标准tcp连接建立过程（sync, ack sync, ack）。</p>
<p>分析：</p>
<p>container1视角：将sync包发个目的地址10.10.126.187，根据路由选择，从默认路由走，下一跳为Gateway，即 172.17.0.1。消息发到docker0。</p>
<p>切换到host视角：host从docker0网卡收到一个sync包，目的地址是10.10.126.187，进入到iptables：</p>
<pre><code>Jan 14 14:47:17 pc-baim kernel: [846795.243863] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.187 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=34160 DF PROTO=TCP SPT=55148 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0
</code></pre>
<p>路由选择后，匹配到host的直连网络路由(10.10.126.0/24 via eth0)，包将从eth0出去，于是docker0转发到eth0，走foward chain：</p>
<pre><code>Jan 14 14:47:17 pc-baim kernel: [846795.243931] [TonyBai]-FwdId0Ond0:IN=docker0 OUT=eth0 PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.187 LEN=60 TOS=0x10 PREC=0x00 TTL=63 ID=34160 DF PROTO=TCP SPT=55148 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0
</code></pre>
<p>出forward chain后，匹配到nat表的postrouting链，做Masquerade(SNAT)。将源地址从172.0.0.2换为 10.10.126.101再发出去。</p>
<pre><code>Jan 14 14:47:17 pc-baim kernel: [846795.243940] [TonyBai]-NatPostRouting:IN= OUT=eth0 PHYSIN=vethd9f6465 SRC=172.17.0.2 DST=10.10.126.187 LEN=60 TOS=0x10 PREC=0x00 TTL=63 ID=34160 DF PROTO=TCP SPT=55148 DPT=12580 WINDOW=29200 RES=0x00 SYN URGP=0
</code></pre>
<p>10.10.126.187收到后，回复ack sync。由于10.10.126.187上增加了172.17.0.0/16的路由，gateway为10.10.126.101，因此ack sync被回送给宿主机，host会从187收到ack sync包。</p>
<pre><code>Jan 14 14:47:17 pc-baim kernel: [846795.244155] [TonyBai]-RawPrerouting:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=10.10.126.101 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=55148 WINDOW=5792 RES=0x00 ACK SYN URGP=0
</code></pre>
<p>进入iptables时，目的地址还是10.10.126.101，进入路由选择前iptables会将10.10.126.101换成 172.17.0.2（由于之间在natpostrouting做了masquerade）。这样后续路由的目的地址为docker0，需要由 eth0转到docker0，走 forward链。由于是RELATED, ESTABLISHED 连接，因此匹配到FwdOd0:</p>
<pre><code>Jan 14 14:47:17 pc-baim kernel: [846795.244182] [TonyBai]-FwdOd0:IN=eth0 OUT=docker0 MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=172.17.0.2 LEN=60 TOS=0x00 PREC=0x00 TTL=63 ID=0 DF PROTO=TCP SPT=12580 DPT=55148 WINDOW=5792 RES=0x00 ACK SYN URGP=0
</code></pre>
<p>切换到container1视角。收到ack sync后，回复ack，同sync流程，不赘述：</p>
<pre><code>Jan 14 14:47:17 pc-baim kernel: [846795.244249] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.187 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=34161 DF PROTO=TCP SPT=55148 DPT=12580 WINDOW=229 RES=0x00 ACK URGP=0
Jan 14 14:47:17 pc-baim kernel: [846795.244266] [TonyBai]-FwdId0Ond0:IN=docker0 OUT=eth0 PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.187 LEN=52 TOS=0x10 PREC=0x00 TTL=63 ID=34161 DF PROTO=TCP SPT=55148 DPT=12580 WINDOW=229 RES=0x00 ACK URGP=0
</code></pre>
<p>不用再走一遍natpostrouting，属于一个流的包只会 经过这个表一次。如果第一个包被允许做NAT或Masqueraded，那么余下的包都会自 动地被做 相同的操作。也就是说,余下的包不会再通过这个表一个一个的被NAT，而是自动地完成。</p>
<h4>6、10.10.126.187 to container</h4>
<p>场景：我们在10.10.126.187向container1建立连接。我在187中执行：telnet 172.17.0.2 12580。container1上启动服务程序在监听12580端口，因此这是个标准tcp连接建立过程（sync, ack sync, ack）。</p>
<p>分析：</p>
<p>由于187上增加了container1的路由，187将sync包发到gateway 10.10.126.101。</p>
<p>宿主机视角：从eth0收到目的地址为172.17.0.2的sync包，到达iptables：</p>
<pre><code>Jan 14 15:06:08 pc-baim kernel: [847926.218791] [TonyBai]-RawPrerouting:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=172.17.0.2 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=48735 DF PROTO=TCP SPT=53225 DPT=12580 WINDOW=5840 RES=0x00 SYN URGP=0
</code></pre>
<p>路由后应该通过docker0发到直连网络。应该走Forward链，但由于上面的log没有覆盖到，只是匹配到DOCKER chain，没有匹配到可以log的rules，没有打印出来log。</p>
<p>docker0将sync发给container1，container1回复ack sync。消息报目的地址187，走gateway，即docker0。</p>
<p>再回到主机视角，host从docker0网卡收到ack sync包，目的187，因此路由后，走直连网络转发口eth0。iptables中走forward chain：FwdId0Ond0:</p>
<pre><code>Jan 14 15:06:08 pc-baim kernel: [847926.219010] [TonyBai]-RawPrerouting:IN=docker0 OUT= PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.187 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=12580 DPT=53225 WINDOW=28960 RES=0x00 ACK SYN URGP=0
Jan 14 15:06:08 pc-baim kernel: [847926.219103] [TonyBai]-FwdId0Ond0:IN=docker0 OUT=eth0 PHYSIN=vethd9f6465 MAC=02:42:f9:f8:c9:ad:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=10.10.126.187 LEN=60 TOS=0x00 PREC=0x00 TTL=63 ID=0 DF PROTO=TCP SPT=12580 DPT=53225 WINDOW=28960 RES=0x00 ACK SYN URGP=0

</code></pre>
<p>注意这块是已经建立的连接，双方都知道对方的地址了（187上配置了172.17.0.2的路由），因此并没有走nat postroutiing chain，没有SNAT转换地址。</p>
<p>187收到后，回复ack。这个过程重复sync过程，但forward链可以匹配到FwdOd0：</p>
<pre><code>Jan 14 15:06:08 pc-baim kernel: [847926.219417] [TonyBai]-RawPrerouting:IN=eth0 OUT= MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=172.17.0.2 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=48736 DF PROTO=TCP SPT=53225 DPT=12580 WINDOW=92 RES=0x00 ACK URGP=0
Jan 14 15:06:08 pc-baim kernel: [847926.219477] [TonyBai]-FwdOd0:IN=eth0 OUT=docker0 MAC=2c:59:e5:01:98:28:00:19:bb:5e:0a:86:08:00 SRC=10.10.126.187 DST=172.17.0.2 LEN=52 TOS=0x10 PREC=0x00 TTL=63 ID=48736 DF PROTO=TCP SPT=53225 DPT=12580 WINDOW=92 RES=0x00 ACK URGP=0
</code></pre>
<h3>八、容器网络性能测量</h3>
<p>这里顺便对容器网络性能做一个初步的测量，测量可以考虑使用传统工具：<a href="http://www.netperf.org/netperf/">netperf</a>，其服务端为netserver，会同netperf一并安装到主机中。但前些时候发现了一款显示结果更直观的用go实现的工具：<a href="https://github.com/chrissnell/sparkyfish">sparkyfish</a>。这里我打算用这个新工具来粗粗的测量一下容器网络的性能。</p>
<p>由于sparkyfish会执行upload和download场景，因此server放在哪个位置均可。</p>
<p>我们执行两个场景，对比host和container的网络性能：</p>
<h4>1、与同局域网的一个主机通信</h4>
<p>我们在一台与host在同一局域网的主机(105.71)上启动sparkyfish-server，然后分别在host和container上执行sparkyfish-cli 10.10.105.71，结果截图如下：</p>
<p><img src="http://tonybai.com/wp-content/uploads/docker-single-host-networking-host-to-71.png" alt="img{}" /><br />
host to 105.71</p>
<p><img src="http://tonybai.com/wp-content/uploads/docker-single-host-networking-container-to-71.png" alt="img{}" /><br />
container to 105.71</p>
<p>对比发现：container、host到外部网络的度量值差不多，avg值几乎相同。</p>
<h4>2、container to host and container</h4>
<p>我们在host和另一个container2上分别启动一个sparkyfish-server，然后在container1上执行分别执行sparkyfish-cli 10.10.126.101和sparkyfish-cli 172.17.0.3，结果截图如下：</p>
<p><img src="http://tonybai.com/wp-content/uploads/docker-single-host-networking-container-to-host.png" alt="img{}" /><br />
  container to host</p>
<p><img src="http://tonybai.com/wp-content/uploads/docker-single-host-networking-container-to-container.png" alt="img{}" /><br />
container to container</p>
<p>对比可以看出：container to container的出入网络性能均仅为container to host的网络性能的三分之一不到。</p>
<h3>九、小结</h3>
<p>以上粗略理解了docker单机容器网络，有些地方理解难免有偏颇，甚至是错误，欢迎指正。<br />
Docker技术虽然成长迅猛，前景广阔，但Docker也非银弹，深入之处必然有坑。填坑之路虽然痛苦，但能有所收获也算是很好了。</p>
<p style='text-align:left'>&copy; 2016, <a href='https://tonybai.com'>bigwhite</a>. 版权所有. </p>
]]></content:encoded>
			<wfw:commentRss>https://tonybai.com/2016/01/15/understanding-container-networking-on-single-host/feed/</wfw:commentRss>
		<slash:comments>15</slash:comments>
		</item>
	</channel>
</rss>
