简单介绍IP、端口、防火墙、Iptables及Docker中的网络实现

写在前面

通过上篇《Docker基本原理简析》知道,Docker通过Linux中的Namespace、CGroups及AFUS(联合文件系统)实现了容器与宿主机的隔离;但是同时也把容器与宿主机的网络隔离了(Docker通过命名空间为容器创建了一个隔离的网络环境),即此时的容器是无法与外界相连的。

那么,Docker是如何让容器与外界相连的呢?或者说,容器使用了哪种技术通过宿主机向互联网发起网络请求的?

什么是IP,如何共享IP

IP地址

试想当深圳的电脑J想要访问上海的电脑K上的服务,那么J该如何才能找到K(而不是电脑A、B、C、D……)呢?

编个号试试啊!

K进行唯一性编号5.20.13.14,这样只要J告诉网络自己想连接5.20.13.14这个编号上的电脑,那么J就能自然而然连接到K去了。

这里的5.20.13.14就类似于我们平时所说的IP地址

端口号

假如电脑J每次只发出一个请求,等收到响应才发出第二个请求……,这种模式下只用IP地址就好了。但是这种模式肯定不满足实际需求,比如我可能在电脑J上用微信给电脑K发信息,同时用浏览器访问baidu.com查明天去香港的船票;这时候只有IP就不够了,还需要端口号(Port)来区分到底是谁发起的请求。

只要请求里包含了两对IP:Port(一个发起者的,一个目的地),就能准确定义是哪台电脑的哪个应用访问了哪台电脑的哪个服务。

下面的golang代码能启动一个服务,并把请求中的IP:Port打印出来:

// golang语言
package main

import (
	"fmt"
	"log"
	"net/http"
)

func sayhelloName(w http.ResponseWriter, r *http.Request) {
	fmt.Println(r.RemoteAddr)
}

func main() {
	http.HandleFunc("/", sayhelloName)       //设置访问的路由
	err := http.ListenAndServe(":9090", nil) //设置监听的端口
	if err != nil {
		log.Fatal("ListenAndServe: ", err)
	}
}
# 如果利用curl访问上面的服务
# curl -v localhost:9090
# 可以发现console中会输出类似下面的内容
[::1]:53354
[::1]:53356

IPV4与终端大爆炸

理想情况下只要给每个终端(电脑、手机、智能手环等)各自分配一个IP地址,世界上的任何一个终端都可以唯一定位到另一个终端。奈何IP地址是有限的,尤其是当前通用的IPV4(4个byte,32个二进制位),最多也只能容下大概43亿(2的32次方)个终端,那么终端多于43亿个以后怎么办?

有人说既然32位不够用,128位总够用了吧,即IPV6(据说IPV6能够给地球上的每一粒沙子编个号)。但是因为IPV4的历史包袱太重,大部分设备都是按照IPV4设计的,更新换代成本太高;且出现了NAT(Network Address Translation,网络地址转换)技术,一定程度上缓解了IP不够的情况,因此升级到IPV6并不那么急迫了。接下来看看NAT是个啥。

大家共享一个IP地址

现在我住的地方安装了宽带,被运营商分配了一个公网IP地址(假设为5.21.13.14),我的两个平板(M和N)、两部手机(P和Q)和一台电脑(K)都能上网,怎么做到的呢?

不妨给 M、N、P、Q和K再编个号,比如给M分配个192.168.0.3,给N分配个192.168.0.4,给P分配个192.168.0.5……

M要访问www.baidu.com时,可能会遵循下面的链路:

# baidu.com不知道内网地址:192.168.0.3
# 但是知道公网IP:5.21.13.14
# 知道请求是从 5.21.13.14:60630 发来的
192.168.0.3:60624 -> 5.21.13.14:60630 -> ... -> baidu.com

这里需要注意,当请求从192.168.0.3发出,经过5.21.13.14的时候,为了让baidu.com知道把响应传到哪里,因此会把请求里的sourceIP修改成公网IP5.21.13.14(因为baidu.com不知道私有IP192.168.0.3的存在),然后请求会一直顺着链路到baidu.com的服务器。

现在我所有的设备都可以通过内部地址192.168.0.*连接到共享的IP5.21.13.14并访问baidu.com了。

响应链路

为了让baidu.com知道把响应传回哪里,请求里的sourceIP被改成了公网IP地址,响应传回平板M的链路如下:

# baidu.com知道请求是从 5.21.13.14:60630 发来的
# 因此把请求发送给了 5.21.13.14:60630
# 但是请求到了 5.21.13.14:60630 以后
# 再怎么到 192.168.0.3:60624 呢
192.168.0.3:60624 <<- 5.21.13.14:60630 <- ... <- baidu.com

这里有一个问题,即响应到达5.21.13.14的时候,它怎么知道这个响应是平板M发出而不是N发出的呢?

我们能注意到,从192.168.0.3:60624->5.21.13.14:60630的请求,端口号由60624变成了60630(当然也可以是其他值),baidu.com知道我们请求是从5.21.13.14:60630过来的,因此把响应发送到了5.21.13.14:60630。那么接下来再怎么根据这个地址把响应转发到192.168.0.3:60624呢?

其实这件事情也不难。当请求从5.21.13.14:60630发出以后,网络设备(比如路由器)会记一件事情,即从60630发出的请求是192.168.0.3:60624发出的,等到有响应发送到60630端口时,设备就立即把这个响应再转发给192.168.0.3:60624。如此,M收到了应有的响应,链路就通了。

防火墙与iptables

记得十年前刚读大学时,那时候流行给电脑安装杀毒软件,诚惶诚恐地配置各种奇怪项,生怕自己中了病毒。对于我们使用的笔记本电脑来说,防火墙到底是什么呢?详细描述大家可以查看参考列表中的文章,简单讲防火墙是一套连接的规则,比如J想要连接M,但是M在那边设置了一条规则,”凡是来自J的请求都拒绝”,这条规则就可以认为是一个私有的小防火墙了。

需要说明的一点是,防火墙可以从很多层面进行设置,比如链路层、连接层、应用层等等,可以在硬件层面做设置,更可以在软件层面做限制。不同的操作系统的默认防火墙也是不一样的,windows有一套,Linux有一套,MacOS也有一套。因为开源和应用广泛,这里只简单说一下Linux系统中的Iptables(这也是我在让人爱上Coding的坚持与实践这篇博文里推荐Linux的原因)。

netfilter/iptables

Linux内核天生具有路由功能,其中包含netfilter模块,可以实现丰富的路由功能。比如设置拒绝接受任何来自5.21.13.14的请求;再比如,实现NAT功能,把内网的IP:Port映射成为公网的IP:Port并记录公网Port到内网IP:Port的映射。

上面这些都可以统称为路由规则,而这些路由规则需要通过iptables写入内核。

Docker中的网络

既然通过Namespace把容器的网络与宿主机完全隔离开了,如果想通过宿主机的网络访问外网,还需要两类虚拟技术。

linux中的Veth

在linux中,Veth是成对出现的虚拟网络设备(可以视为虚拟的网卡),发送到Veth一端虚拟设备的请求会从另一端的虚拟设备中发出。

|---------|----------|
|   ns1   |    ns2   |
| veth0---|---veth1  |
|         |          |
|---------|----------|

比如上图的veth0veth1是一对veth,其中veth0绑定到了命名空间ns1,veth1绑定到了ns2。那么在ns1中发送到veth0的请求就会自然而然地被ns2在veth1上面获取到。

Linux中的Bridge

虽然有了veth,但是想把流量发送到宿主机所在的公网,依然不够。Linux中的Bridge是一种虚拟的网络桥接设备,相当于现实世界中的交换机(或路由器),可以连接不同的网络设备。

|---------|----------|
|   ns1   |          |
|  veth1--|---veth0  |
|---------|     |    |
          |    br0   |
          |     |    |
   主机----|---eth0   |
          |----------|

如上图,命名空间ns1通过br0连接起来了,ns1可以通过veth1发送请求,顺着veth1->veth0->br0->eth0的链路连通到主机。

Docker中的网络链路

那么,网络如何通过veth1->veth0->br0->eth0访问外部网络的呢?这里需要借助Iptables:

  1. 设置命名空间ns1中所有流量都经过veth1的网络设备流出;如此,在ns1中可以访问宿主机的网络地址。
  2. 在宿主机上将ns1所在网络段的请求路由到br0的网桥;如此,在宿主机可以访问Namespace中的网络地址。
  3. 在宿主机上打开IP转发;如此,宿主机在收到ns1传过来的请求时,将进行转发操作。
  4. 在宿主机上设定把ns1中发出的包添加NAT(网络地址转换);如此,从ns1中发出的请求,有了公网的IP(宿主机IP),就能收到响应了。
  5. 收到响应后,再路由回ns1,请求链路就闭合了。

小结

本文简单介绍了IP、端口、linux中的路由,最后简单梳理了docker中的网络链路。这里面忽略了很多的细节,但是基本说明了容器通过宿主机访问外网的原理。大家可以阅读参考文献,自行探索更详细的内容。

参考