Fundamentals 30 min read

Understanding Docker Virtual Network Fundamentals

This article explains Docker's network virtualization principles, covering veth devices, bridge networks, Linux namespaces, and NAT techniques for container communication.

Refining Core Development Skills
Refining Core Development Skills
Refining Core Development Skills
Understanding Docker Virtual Network Fundamentals

大家好,我是飞哥!

如今服务器虚拟化技术已经发展到了深水区。现在业界已经有很多公司都迁移到容器上了。我们的开发写出来的代码大概率是要运行在容器上的。因此深刻理解容器网络的工作原理非常的重要。只有这样将来遇到问题的时候才知道该如何下手处理。

网络虚拟化,其实用一句话来概括就是 用软件来模拟实现真实的物理网络连接 。比如 Docker 就是用纯软件的方式在宿主机上模拟出来的独立网络环境。我们今天来徒手打造一个虚拟网络,实现在这个网络里访问外网资源,同时监听端口提供对外服务的功能。

看完这一篇后,相信你对 Docker 虚拟网络能有进一步的理解。好了,我们开始!

一、基础知识回顾

1.1 veth、bridge 与 namespace

Linux 下的 veth 是一对儿虚拟网卡设备,和我们常见的 lo 很类似。在这儿设备里,从一端发送数据后,内核会寻找该设备的另一半,所以在另外一端就能收到。不过 veth 只能解决一对一通信的问题。详情参见 轻松理解 Docker 网络虚拟化基础之 veth 设备!

如果有很多对儿 veth 需要互相通信的话,就需要引入 bridge 这个虚拟交换机。各个 veth 对儿可以把一头连接在 bridge 的接口上,bridge 可以和交换机一样在端口之间转发数据,使得各个端口上的 veth 都可以互相通信。参见 聊聊 Linux 上软件实现的"交换机" - Bridge!

Namespace 解决的是隔离性的问题。每个虚拟网卡设备、进程、socket、路由表等等网络栈相关的对象默认都是归属在 init_net 这个缺省的 namespace 中的。不过我们希望不同的虚拟化环境之间是隔离的,用 Docker 来举例,那就是不能让 A 容器用到 B 容器的设备、路由表、socket 等资源,甚至连看一眼都不可以。只有这样才能保证不同的容器之间复用资源的同时,还不会影响其它容器的正常运行。参见 彻底弄懂 Linux 网络命名空间

通过 veth、namespace 和 bridge 我们在一台 Linux 上就能虚拟多个网络环境出来。而且它们之间、和宿主机之间都可以互相通信。

关于这三个技术的详情,可以参考下面这三篇文章:

轻松理解 Docker 网络虚拟化基础之 veth 设备!

聊聊 Linux 上软件实现的"交换机" - Bridge!

彻底弄懂 Linux 网络命名空间

但是这三篇文章过后,我们还剩下一个问题没有解决,那就是虚拟出来的网络环境和外部网络的通信。还拿 Docker 容器来举例,你启动的容器里的服务肯定是需要访问外部的数据库的。还有就是可能需要暴露比如 80 端口对外提供服务。例如在 Docker 中我们通过下面的命令将容器的 80 端口上的 web 服务要能被外网访问的到。

我们今天的文章主要就是解决这两个问题的,一是从虚拟网络中访问外网,二是在虚拟网络中提供服务供外网使用。解决它们需要用到路由和 nat 技术。

二、 实现虚拟网络外网通信

1. 实验环境准备

我们先来创建一个虚拟的网络环境出来,其命名空间为 net1。宿主机的 IP 是 10.162 的网段,可以访问外部机器。虚拟网络为其分配 192.168.0 的网段,这个网段是私有的,外部机器无法识别。

这个虚拟网络的搭建过程如下。先创建一个 netns 出来,命名为 net1。

<code style='padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px'><span style='color: #5c6370; font-style: italic; line-height: 26px'># ip netns add net1</span></code>

创建一个 veth 对儿(veth1 - veth1_p),把其中的一头 veth1 放在 net1 中,给它配置上 IP,并把它启动起来。

<code style='padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px'><span style='color: #5c6370; font-style: italic; line-height: 26px'># ip link add veth1 type veth peer name veth1_p</span><br/><span style='color: #5c6370; font-style: italic; line-height: 26px'># ip link set veth1 netns net1</span><br/><span style='color: #5c6370; font-style: italic; line-height: 26px'># ip netns exec net1 ip addr add 192.168.0.2/24 dev veth1  # IP</span><br/><span style='color: #5c6370; font-style: italic; line-height: 26px'># ip netns exec net1 ip link set veth1 up</span></code>

创建一个 bridge,给它也设置上 ip。接下来把 veth 的另外一端 veth1_p 插到 bridge 上面。最后把网桥和 veth1_p 都启动起来。

<code style='padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px'><span style='color: #5c6370; font-style: italic; line-height: 26px'># brctl addbr br0</span><br/><span style='color: #5c6370; font-style: italic; line-height: 26px'># ip addr add 192.168.0.1/24 dev br0</span><br/><span style='color: #5c6370; font-style: italic; line-height: 26px'># ip link set dev veth1_p master br0</span><br/><span style='color: #5c6370; font-style: italic; line-height: 26px'># ip link set veth1_p up</span><br/><span style='color: #5c6370; font-style: italic; line-height: 26px'># ip link set br0 up</span></code>

这样我们就在 Linux 上创建出了一个虚拟的网络。创建过程和 聊聊 Linux 上软件实现的"交换机" - Bridge! 中一样,只不过今天为了省事,只创建了一个网络出来,上一篇中创建出来了两个。

2. 请求外网资源

现在假设我们上面的 net1 这个网络环境中想访问外网。这里的外网是指的虚拟网络宿主机外部的网络。

我们假设它要访问的另外一台机器 IP 是 10.153.*.*,这个 10.153.*.* 后面两段由于是我的内部网络,所以隐藏起来了。你在实验的过程中,用自己的 IP 代替即可。

我们直接来访问一下试试

<code style='padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px'><span style='color: #5c6370; font-style: italic; line-height: 26px'># ip netns exec net1 ping 10.153.*.* -c 2</span><br/>connect: Network is unreachable</code>

提示网络不通,这是怎么回事?用这段报错关键字在内核源码里搜索一下:

<code style='padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px'><span style='color: #5c6370; font-style: italic; line-height: 26px'>//file: arch/parisc/include/uapi/asm/errno.h</span><br/><span style='color: #61aeee; line-height: 26px'>#define ENETUNREACH 229 /* Network is unreachable */</span><br/><br/><span style='color: #5c6370; font-style: italic; line-height: 26px'>//file: net/ipv4/ping.c</span><br/><span style='line-height: 26px'><span style='color: #c678dd; line-height: 26px'>static</span> <span style='color: #c678dd; line-height: 26px'>int</span> <span style='color: #61aeee; line-height: 26px'>ping_sendmsg</span><span style='line-height: 26px'>(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,<br/>   <span style='color: #c678dd; line-height: 26px'>size_t</span> len)</span><br/></span>{<br/>  ...<br/>  rt = ip_route_output_flow(net, &fl4, sk);<br/>  <span style='color: #c678dd; line-height: 26px'>if</span> (IS_ERR(rt)) {<br/>    err = PTR_ERR(rt);<br/>    rt = <span style='color: #56b6c2; line-height: 26px'>NULL</span>;<br/>    <span style='color: #c678dd; line-height: 26px'>if</span> (err == -ENETUNREACH)<br/>     IP_INC_STATS_BH(net, IPSTATS_MIB_OUTNOROUTES);<br/>    <span style='color: #c678dd; line-height: 26px'>goto</span> out;<br/>  }<br/>  ...<br/>out:  <br/>  <span style='color: #c678dd; line-height: 26px'>return</span> err;  <br/>}</code>

在 ip_route_output_flow 这里的返回值判断如果是 ENETUNREACH 就退出了。这个宏定义注释上来看报错的信息就是 "Network is unreachable"。

这个 ip_route_output_flow 主要是执行路由选路。所以我们推断可能是路由出问题了,看一下这个命名空间的路由表。

<code style='padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px'><span style='color: #5c6370; font-style: italic; line-height: 26px'># ip netns exec net1 route -n</span><br/>Kernel IP routing table<br/>Destination     Gateway         Genmask         Flags Metric Ref    Use Iface<br/>192.168.0.0     0.0.0.0         255.255.255.0   U     0      0        0 veth1</code>

怪不得,原来 net1 这个 namespace 下默认只有 192.168.0.* 这个网段的路由规则。我们 ping 的 IP 是 10.153.*.*,根据这个路由表里找不到出口。自然就发送失败了。

我们来给 net 添加上默认路由规则,只要匹配不到其它规则就默认送到 veth1 上,同时指定下一条是它所连接的 bridge(192.168.0.1)。

<code style='padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px'><span style='color: #5c6370; font-style: italic; line-height: 26px'># ip netns exec net1 route add default gw 192.168.0.1 veth1  </span></code>

再 ping 一下试试。

<code style='padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px'><span style='color: #5c6370; font-style: italic; line-height: 26px'># ip netns exec net1 ping 10.153.*.* -c 2</span><br/>PING 10.153.*.* (10.153.*.*) 56(84) bytes of data.<br/><br/>--- 10.153.*.* ping statistics ---<br/>2 packets transmitted, 0 received, 100% packet loss, time 999ms</code>

额好吧,仍然不通。上面路由帮我们把数据包从 veth 正确送到了 bridge 这个网桥上。接下来网桥还需要 bridge 转发到 eth0 网卡上。所以我们得打开下面这两个转发相关的配置

<code style='padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px'><span style='color: #5c6370; font-style: italic; line-height: 26px'># sysctl net.ipv4.conf.all.forwarding=1</span><br/><span style='color: #5c6370; font-style: italic; line-height: 26px'># iptables -P FORWARD ACCEPT</span></code>

不过这个时候,还存在一个问题。那就是外部的机器并不认识 192.168.0.* 这个网段的 ip。它们之间都是通过 10.153.*.* 来进行通信的。设想下我们工作中的电脑上没有外网 IP 的时候是如何正常上网的呢?外部的网络只认识外网 IP。没错,那就是我们上面说的 NAT 技术。

我们这次的需求是实现内部虚拟网络访问外网,所以需要使用的是 SNAT。它将 namespace 请求中的 IP(192.168.0.2)换成外部网络认识的 10.153.*.*,进而达到正常访问外部网络的效果。

<code style='padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px'><span style='color: #5c6370; font-style: italic; line-height: 26px'># iptables -t nat -A POSTROUTING -s 192.168.0.0/24 ! -o br0 -j MASQUERADE</span></code>

来再 ping 一下试试,欧耶,通了!

<code style='padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px'><span style='color: #5c6370; font-style: italic; line-height: 26px'># ip netns exec net1 ping 10.153.*.* -c 2</span><br/>PING 10.153.*.* (10.153.*.*) 56(84) bytes of data.<br/>64 bytes from 10.153.*.*: icmp_seq=1 ttl=57 time=1.70 ms<br/>64 bytes from 10.153.*.*: icmp_seq=2 ttl=57 time=1.68 ms</code>

这时候我们可以开启 tcpdump 抓包查看一下,在 bridge 上抓到的包我们能看到还是原始的源 IP 和 目的 IP。

再到 eth0 上查看的话,源 IP 已经被替换成可和外网通信的 eth0 上的 IP 了。

至此,容器就可以通过宿主机的网卡来访问外部网络上的资源了。我们来总结一下这个发送过程

3. 开放容器端口

我们再考虑另外一个需求,那就是把在这个命名空间内的服务提供给外部网络来使用。

和上面的问题一样,我们的虚拟网络环境中 192.168.0.2 这个 IP 外界是不认识它的。只有这个宿主机知道它是谁。所以我们同样还需要 NAT 功能。

这次我们是要实现外部网络访问内部地址,所以需要的是 DNAT 配置。DNAT 和 SNAT 配置中有一个不一样的地方就是需要明确指定容器中的端口在宿主机上是对应哪个。比如在 docker 的使用中,是通过 -p 来指定端口的对应关系。

<code style='padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px'><span style='color: #5c6370; font-style: italic; line-height: 26px'># docker run -p 8000:80 ...</span></code>

我们通过如下这个命令来配置 DNAT 规则

<code style='padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px'><span style='color: #5c6370; font-style: italic; line-height: 26px'># iptables -t nat -A PREROUTING  ! -i br0 -p tcp -m tcp --dport 8088 -j DNAT --to-destination 192.168.0.2:80</span></code>

这里表示的是宿主机在路由之前判断一下如果流量不是来自 br0,并且是访问 tcp 的 8088 的话,那就转发到 192.168.0.2:80 。

在 net1 环境中启动一个 Server

<code style='padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px'><span style='color: #5c6370; font-style: italic; line-height: 26px'># ip netns exec net1 nc -lp 80</span></code>

外部选一个ip,比如 10.143.*.*, telnet 连一下 10.162.*.* 8088 试试,通了!

<code style='padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px'><span style='color: #5c6370; font-style: italic; line-height: 26px'># telnet 10.162.*.* 8088</span><br/>Trying 10.162.*.*...<br/>Connected to 10.162.*.*.<br/>Escape character is '^]'.<br/></code>

开启抓包,

Refining Core Development Skills
Written by

Refining Core Development Skills

Fei has over 10 years of development experience at Tencent and Sogou. Through this account, he shares his deep insights on performance.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.