searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

容器网络基础原理

2023-04-27 09:05:58
19
0

      在上一篇《容器技术入门》中介绍的 8 种 NameSpace 中,有一个 Network NameSpace ,我们可以借助这个来给容器配置独立的网络视图。

我们先看宿主机所处的 Default Network NameSpace :

[root@host ~]# readlink /proc/$$/ns/net
net:[4026531956]
[root@host ~]#

      以net:[4026531956] 为例,net 代表了 NameSpace 的类型,4026531956 则是 NameSpace 的 inode number。

      经过之前的文章介绍,我们已经深知容器的本质就是进程,本文也不再浪费笔墨,接下来的操作将直接透过上层直达本质,借助 Linux 底层提供的能力来探索和还原容器网络的实现。(后续本文创建出的进程请直接理解为容器)

      首先通过 ip netns 工具创建两个网络命名空间 netns1 和 netns2 :

[root@host ~]# ip netns add netns1
[root@host ~]# ip netns add netns2
[root@host ~]# ip netns list
netns2
netns1
[root@host ~]#

       在这两个网络命名空间之上分别创建两个 bash 进程容器,其中 container1 :

[root@host ~]# ip netns exec netns1 /bin/bash --rcfile <(echo "PS1=\"container1> \"")
container1> readlink /proc/$$/ns/net
net:[4026532165]
container1> ip link # 查看网络设备列表
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
container1> route -n # 查看路由表
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
container1> iptables -L # 查看 iptables 规则
Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
container1>

       同样的,container2 :

[root@host ~]# ip netns exec netns2 /bin/bash --rcfile <(echo "PS1=\"container2> \"")
container2> readlink /proc/$$/ns/net
net:[4026532219]
container2> ip link # 查看网络设备列表
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
container2> route -n # 查看路由表
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
container2> iptables -L # 查看 iptables 规则
Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
container2>

      可以看出,由于 Network NameSpace 隔离的作用,不同的容器( container1 和 container2 )拥有自己独立的网络协议栈,包括网络设备、路由表、ARP 表、iptables 规则、socket 等,所有的容器都会以为自己运行在独立的网络环境中。

       现在准备一个简单的 go web 服务,并分别在 container1 和 container2 的后台运行:

package main

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

func main() {
 name := os.Args[1]
 http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  fmt.Println("req")
  w.Write([]byte(name + "\n"))
 })
 fmt.Println(name, "listen :8080")
 panic(http.ListenAndServe(":8080", nil))
}

        container1 :

container1> go run main.go container1 > container1.log &
[1] 2866
container1> tail container1.log
container1 listen :8080
container1>

         container2 :

container2> go run main.go container2 > container2.log &
[1] 2955
container2> tail container2.log
container2 listen :8080
container2>

        当前在同一主机下,即使 container1 和 container2 都监听着 8080 端口,也并不会发生端口冲突。

        我们测试一下刚才所启动的服务的可用性,以 container1 为例:

container1> curl localhost:8080
curl: (7) Failed to connect to ::1: Network is unreachable
container1>

        此时访问不通是因为我们根本还没有启用任何网络设备,包括我们基本的 lo 回路设备。直接将其启用即可:

container1> ifconfig
container1> ifup lo
container1> ifconfig
lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

container1> curl localhost:8080
container1
container1>

container2 同理。

 

1.容器点对点通信:Veth

      当前两个容器处于不同的 Network NameSpace 中,它们的网络环境是互相隔离的,你不认识我,我也不认识你,它们之间自然也无法进行网络通信。

      在 Linux 网络虚拟化技术中为我们提供了一种用软件来模拟硬件网卡的方式:Veth(Virtual Ethernet devices)。和一根网线有两端一样,Veth 也是成对出现的,所以也被称为 veth pair 。假设 veth1 和 veth2 是一对设备,那么从 veth1 进入的数据包将会在 veth2 收到,反过来也一样。所以只要将一对 Veth 分别放入两个 Network Namespace 中,这两个 Network Namespace 就会像连接了网线一样,可以互相通信了。

      创建一个 veth pair (包含 veth1 和 veth2 两个虚拟网络设备):

      直接将一端的虚拟网卡 veth1 放入 netns1 命名空间,另一端的 veth2 放入 netns2 命名空间,这样就相当于使用网线将两个命名空间连接起来了:

      连接起来后就可以在 container1 和 container2 容器中查看到各自对应的网络设备:

      分别为这两个网卡设置 IP 地址,使其位于同一个子网 172.17.0.0/24 中,然后启用网卡:

       测试 container1 和 container2 容器互相访问对方的服务:

       到这里,只需依靠 veth ,我们就得到了一个点对点的二层网络拓扑,容器点对点通信的问题也得以成功解决。

2. 容器间互相通信:Bridge

      我们知道在现实世界中,不可能只有两台计算机,当有第三台、第四台,乃至无数台计算机加入到网络环境中的时候,我们不可能有这么多网口可以彼此两两互相连接。为了解决这个问题,便发明了二层交换机(或网桥)。

      对于容器也是如此,如果我们有 3 个或以上的 Namespace 需要接入同一个二层网络,就不能简单的只使用 Veth 了。不过和使用 Veth 作为虚拟网卡的方式一样,贴心的 Linux 同样为我们提供了网桥(交换机)的虚拟实现方式:Bridge 。

      在 netns1 和 netns2 命名空间的基础上再创建一个 netns3 命名空间:

 

      重复之前的操作,创建 container3 容器:

      在为这三个容器互相通网之前,我们先把之前给 container1 和 container2 容器之间连接的“网线”(veth1 和 veth2 )拔了(只需在其中一个容器中操作):

       现在三个容器之间就谁也不认识谁了。

       回归实践,创建一个 Bridge ,并将其启用:

      准备三条“网线”(三对 veth):

        将 veth1 插入 netns1 、veth1-br 插入 br0 、 veth2 插入 netns2 、veth2-br 插入 br0 、 veth3 插入 netns3 、veth3-br 插入 br0 (记得启用 veth*-br ):

        分别在三个容器中,为各自的网卡设置 IP 地址,并使其位于同一个子网 172.17.0.0/24 中,设置完后同样需要进行启用操作:

      测试 container1 、 container2 、container3 容器互相访问对方的服务:

        至此,在 Veth 的基础上,引入 Bridge 功能,我们就将多个 Namespace 连接到了同一个二层网络中,容器间互相通信的问题得以成功解决。

3. 容器与外部网络通信:route 和 iptables

      目前为止,我们的实验都是处于同一子网中。但实际的应用场景,更多的是需要容器可以与外部进行互通。

      在现实世界中,二层交换机只能解决同一子网内的数据流向,对于不同子网,就需要使用三层路由器(或网关)来转发。

      不过和之前 Linux 提供了交换机的虚拟化实现 Bridge 不同,Linux 并没有提供一个虚拟的路由器设备。因为 Linux 其自身就已经具备了路由器的功能,可以直接用来充当路由器,更准确地说,在 Linux 中,一个 Network Namespace 就可以承担一个路由器的功能。

       在 Linux Network Namespace 中,路由功能的定义其实很简单:直接通过定义路由表规则就可以决定将请求的数据包流向到指定的网络设备上。

       路由的规则都定义在了路由表,对于路由表,我们最常用的是 local 和 main ,当然也可以另外配置其他表,其中 local 的优先级要高于 main ,我们平时访问本机(localhost)的请求,都会直接在 local 表中找到规则,不会再经过 main 。

       可以查看所有的路由表:

      如果要查看指定的路由表中的规则可以使用 ip route list table 表名 :

       我们平时使用的 route -n 命令实际就是在查看 main 路由表。

 

4. 容器和宿主机互通

      我们现在有 container1 、 container2 和 container3 三个容器,其 IP 分别为:172.17.0.101 、172.17.0.102 和 172.17.0.103 ,它们都处于同一子网中。宿主机的 IP 是 10.0.12.15 :

      查看容器(以 container1 为例)的路由规则:

       可以发现,当前容器的路由表中只有一条规则:当访问 172.17.0.0/24 子网内的 IP 时,例如 172.17.0.102 ,数据包直接转发到 veth1 设备。根据 Veth 的性质,对应 veth1 的另一端 veth1-br 设备就会接收到数据包,又由于 veth1-br 设备是连接在 br0 二层交换机上的,所以 veth2-br 设备也会接收到该数据包,最后就到达了对应的 veth2 目标设备。

       我们现在的目的就是要让容器访问除了 172.17.0.0/24 子网内的 IP 的其它 IP 时(比如宿主机 IP 10.0.12.15),也可以把数据包转发出去,也就是需要给容器增加路由规则。回想我们之前的 Bridge ,从网络角度上看,我们是把它当作一个二层交换机,所以不需要为它设置 IP 地址,但现在从宿主机的角度上看,br0 同时也是 Default Network Namespace 里面的一张网卡,我们可以直接为这张网卡设置 IP 后,来充当三层路由器(网关),参与主机的路由转发。

       我们在宿主机上给 br0 设备设置 IP 地址为 172.17.0.1 (同样位于子网 172.17.0.0/24 内):

       此时,宿主机会自动增加一条 Destination 为 172.17.0.0 的路由规则:

      按照这条路由规则(访问目标主机为 172.17.0.0/24 的数据包会转发到 br0 设备),我们现在的宿主机就可以直接访问容器了:

      现在宿主机可以访问容器了,回到容器访问宿主机的问题上,我们给容器增加路由转发规则,使默认所有路由(0.0.0.0 ,除了 172.17.0.0/24 子网内的 IP )都需要透过作用于三层的 br0 网关(172.17.0.1)来转递封包:

      这样一来,当容器访问宿主机 IP 时,数据包就会直接转发到 Default Network Namespace 的 br0 网卡上,宿主机发现收到的 IP 数据包是属于自己的,就会对其进行处理。

       也就是说现在容器也可以访问宿主机了:

 

5. 容器访问其它主机(外网)

      上面我们给容器配置了路由规则,使其访问宿主机 IP 时,数据包就会转发到 Default Network Namespace 的 br0 网卡上,宿主机发现收到的 IP 数据包是属于自己的,就会对其进行处理。

      但在 Linux 中,如果发现收到的 IP 数据包并不是属于自己的,是会将其直接丢弃的。这里我们准备了一个和宿主机(10.0.12.15)同一子网的另一台 IP 地址为 10.0.12.11 的主机 host2 来测试:

 

        在 container1 内 ping host2 ,以及访问外网,和我们预期一致,无法访问:

 

      如果我们想要在容器中可以访问其他主机,或者外网,我们就不能让 Linux 丢弃不属于自己的数据包,而是继续转发出去。要做到这一点,只需要打开 Linux 的 IP Forward 功能:

      但是,这里还是会有一个问题,当容器访问其他主机或者外网时(veth1->br0),Linux 也帮我们转发了出去(br0->eth0),但其他主机或者外网在响应了我们容器的请求时,它并不认识我们所配置的 172.17.0.0/24 这个网段,还是无法成功处理请求。所以我们还需要使用 NAT( Network Address Translation )技术:把 IP 数据报文头中的 IP 地址和端口转换成另一个 IP 地址和端口(NAT 技术也被用于 IPv4 地址枯竭问题)。

      在这里我们需要修改的是源 IP 地址,所以是 SNAT (Source NAT),把 IP 转换成宿主机出口网卡的 IP 。

      在 Linux 中,我们可以通过 iptables 中的 MASQUERADE 策略来实现这个 SNAT :

      这条规则的意思是将不是从网卡 br0 发出的且源地址为 172.17.0.0/24 的数据包(即容器中发出的数据包)做 SNAT 。

      此时,我们的容器就可以访问其它主机(外网)了。

 

6. 外部访问容器(容器端口映射)

       容器与外部网络通信,我们已经解决了容器和宿主机间的通信,容器访问其他主机(外网),还剩最后一个,外部来访问容器。

       在 Docker 中,要让外部可以访问容器服务,我们会做一次容器端口映射,如:

      通过 -p 参数就可以将容器内的 8080 端口映射到宿主机的 8000 端口上,这样外部就可以通过访问宿主机 IP + 8000 端口访问到我们的容器服务了。

       这一技术实现其实同样是使用到了 NAT 技术,只不过和上面的 SNAT 不同的是,这里我们需要修改的是目的 IP 地址,即 DNAT (Destination NAT),把宿主机上 8000 端口的流量请求转发到容器中的地址 172.17.0.101:8080 中。

       在 Linux 中,我们可以通过 iptables 中的 DNAT 策略来实现这个 DNAT :

[root@host ~]# iptables -t nat -A PREROUTING  ! -i br0 -p tcp -m tcp --dport 8000 -j DNAT --to-destination 172.17.0.101:8080
[root@host ~]# iptables -t nat -nL
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:8000 to:172.17.0.101:8080

Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination
MASQUERADE  all  --  172.17.0.0/24        0.0.0.0/0
[root@host ~]#

 

      这条规则的意思是将访问宿主机 8000 端口的请求转发到 172.17.0.101 的 8080 端口上,也就是说我们现在可以在 host2 主机(10.0.12.11)上通过访问 10.0.12.15:8000 来访问实际的 container1 服务了。

 

       到这里,我们就已经实现了一个和 Docker 默认网络模式一样的拓扑关系:

0条评论
作者已关闭评论
刘****宽
2文章数
0粉丝数
刘****宽
2 文章 | 0 粉丝
刘****宽
2文章数
0粉丝数
刘****宽
2 文章 | 0 粉丝
原创

容器网络基础原理

2023-04-27 09:05:58
19
0

      在上一篇《容器技术入门》中介绍的 8 种 NameSpace 中,有一个 Network NameSpace ,我们可以借助这个来给容器配置独立的网络视图。

我们先看宿主机所处的 Default Network NameSpace :

[root@host ~]# readlink /proc/$$/ns/net
net:[4026531956]
[root@host ~]#

      以net:[4026531956] 为例,net 代表了 NameSpace 的类型,4026531956 则是 NameSpace 的 inode number。

      经过之前的文章介绍,我们已经深知容器的本质就是进程,本文也不再浪费笔墨,接下来的操作将直接透过上层直达本质,借助 Linux 底层提供的能力来探索和还原容器网络的实现。(后续本文创建出的进程请直接理解为容器)

      首先通过 ip netns 工具创建两个网络命名空间 netns1 和 netns2 :

[root@host ~]# ip netns add netns1
[root@host ~]# ip netns add netns2
[root@host ~]# ip netns list
netns2
netns1
[root@host ~]#

       在这两个网络命名空间之上分别创建两个 bash 进程容器,其中 container1 :

[root@host ~]# ip netns exec netns1 /bin/bash --rcfile <(echo "PS1=\"container1> \"")
container1> readlink /proc/$$/ns/net
net:[4026532165]
container1> ip link # 查看网络设备列表
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
container1> route -n # 查看路由表
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
container1> iptables -L # 查看 iptables 规则
Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
container1>

       同样的,container2 :

[root@host ~]# ip netns exec netns2 /bin/bash --rcfile <(echo "PS1=\"container2> \"")
container2> readlink /proc/$$/ns/net
net:[4026532219]
container2> ip link # 查看网络设备列表
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
container2> route -n # 查看路由表
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
container2> iptables -L # 查看 iptables 规则
Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
container2>

      可以看出,由于 Network NameSpace 隔离的作用,不同的容器( container1 和 container2 )拥有自己独立的网络协议栈,包括网络设备、路由表、ARP 表、iptables 规则、socket 等,所有的容器都会以为自己运行在独立的网络环境中。

       现在准备一个简单的 go web 服务,并分别在 container1 和 container2 的后台运行:

package main

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

func main() {
 name := os.Args[1]
 http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  fmt.Println("req")
  w.Write([]byte(name + "\n"))
 })
 fmt.Println(name, "listen :8080")
 panic(http.ListenAndServe(":8080", nil))
}

        container1 :

container1> go run main.go container1 > container1.log &
[1] 2866
container1> tail container1.log
container1 listen :8080
container1>

         container2 :

container2> go run main.go container2 > container2.log &
[1] 2955
container2> tail container2.log
container2 listen :8080
container2>

        当前在同一主机下,即使 container1 和 container2 都监听着 8080 端口,也并不会发生端口冲突。

        我们测试一下刚才所启动的服务的可用性,以 container1 为例:

container1> curl localhost:8080
curl: (7) Failed to connect to ::1: Network is unreachable
container1>

        此时访问不通是因为我们根本还没有启用任何网络设备,包括我们基本的 lo 回路设备。直接将其启用即可:

container1> ifconfig
container1> ifup lo
container1> ifconfig
lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

container1> curl localhost:8080
container1
container1>

container2 同理。

 

1.容器点对点通信:Veth

      当前两个容器处于不同的 Network NameSpace 中,它们的网络环境是互相隔离的,你不认识我,我也不认识你,它们之间自然也无法进行网络通信。

      在 Linux 网络虚拟化技术中为我们提供了一种用软件来模拟硬件网卡的方式:Veth(Virtual Ethernet devices)。和一根网线有两端一样,Veth 也是成对出现的,所以也被称为 veth pair 。假设 veth1 和 veth2 是一对设备,那么从 veth1 进入的数据包将会在 veth2 收到,反过来也一样。所以只要将一对 Veth 分别放入两个 Network Namespace 中,这两个 Network Namespace 就会像连接了网线一样,可以互相通信了。

      创建一个 veth pair (包含 veth1 和 veth2 两个虚拟网络设备):

      直接将一端的虚拟网卡 veth1 放入 netns1 命名空间,另一端的 veth2 放入 netns2 命名空间,这样就相当于使用网线将两个命名空间连接起来了:

      连接起来后就可以在 container1 和 container2 容器中查看到各自对应的网络设备:

      分别为这两个网卡设置 IP 地址,使其位于同一个子网 172.17.0.0/24 中,然后启用网卡:

       测试 container1 和 container2 容器互相访问对方的服务:

       到这里,只需依靠 veth ,我们就得到了一个点对点的二层网络拓扑,容器点对点通信的问题也得以成功解决。

2. 容器间互相通信:Bridge

      我们知道在现实世界中,不可能只有两台计算机,当有第三台、第四台,乃至无数台计算机加入到网络环境中的时候,我们不可能有这么多网口可以彼此两两互相连接。为了解决这个问题,便发明了二层交换机(或网桥)。

      对于容器也是如此,如果我们有 3 个或以上的 Namespace 需要接入同一个二层网络,就不能简单的只使用 Veth 了。不过和使用 Veth 作为虚拟网卡的方式一样,贴心的 Linux 同样为我们提供了网桥(交换机)的虚拟实现方式:Bridge 。

      在 netns1 和 netns2 命名空间的基础上再创建一个 netns3 命名空间:

 

      重复之前的操作,创建 container3 容器:

      在为这三个容器互相通网之前,我们先把之前给 container1 和 container2 容器之间连接的“网线”(veth1 和 veth2 )拔了(只需在其中一个容器中操作):

       现在三个容器之间就谁也不认识谁了。

       回归实践,创建一个 Bridge ,并将其启用:

      准备三条“网线”(三对 veth):

        将 veth1 插入 netns1 、veth1-br 插入 br0 、 veth2 插入 netns2 、veth2-br 插入 br0 、 veth3 插入 netns3 、veth3-br 插入 br0 (记得启用 veth*-br ):

        分别在三个容器中,为各自的网卡设置 IP 地址,并使其位于同一个子网 172.17.0.0/24 中,设置完后同样需要进行启用操作:

      测试 container1 、 container2 、container3 容器互相访问对方的服务:

        至此,在 Veth 的基础上,引入 Bridge 功能,我们就将多个 Namespace 连接到了同一个二层网络中,容器间互相通信的问题得以成功解决。

3. 容器与外部网络通信:route 和 iptables

      目前为止,我们的实验都是处于同一子网中。但实际的应用场景,更多的是需要容器可以与外部进行互通。

      在现实世界中,二层交换机只能解决同一子网内的数据流向,对于不同子网,就需要使用三层路由器(或网关)来转发。

      不过和之前 Linux 提供了交换机的虚拟化实现 Bridge 不同,Linux 并没有提供一个虚拟的路由器设备。因为 Linux 其自身就已经具备了路由器的功能,可以直接用来充当路由器,更准确地说,在 Linux 中,一个 Network Namespace 就可以承担一个路由器的功能。

       在 Linux Network Namespace 中,路由功能的定义其实很简单:直接通过定义路由表规则就可以决定将请求的数据包流向到指定的网络设备上。

       路由的规则都定义在了路由表,对于路由表,我们最常用的是 local 和 main ,当然也可以另外配置其他表,其中 local 的优先级要高于 main ,我们平时访问本机(localhost)的请求,都会直接在 local 表中找到规则,不会再经过 main 。

       可以查看所有的路由表:

      如果要查看指定的路由表中的规则可以使用 ip route list table 表名 :

       我们平时使用的 route -n 命令实际就是在查看 main 路由表。

 

4. 容器和宿主机互通

      我们现在有 container1 、 container2 和 container3 三个容器,其 IP 分别为:172.17.0.101 、172.17.0.102 和 172.17.0.103 ,它们都处于同一子网中。宿主机的 IP 是 10.0.12.15 :

      查看容器(以 container1 为例)的路由规则:

       可以发现,当前容器的路由表中只有一条规则:当访问 172.17.0.0/24 子网内的 IP 时,例如 172.17.0.102 ,数据包直接转发到 veth1 设备。根据 Veth 的性质,对应 veth1 的另一端 veth1-br 设备就会接收到数据包,又由于 veth1-br 设备是连接在 br0 二层交换机上的,所以 veth2-br 设备也会接收到该数据包,最后就到达了对应的 veth2 目标设备。

       我们现在的目的就是要让容器访问除了 172.17.0.0/24 子网内的 IP 的其它 IP 时(比如宿主机 IP 10.0.12.15),也可以把数据包转发出去,也就是需要给容器增加路由规则。回想我们之前的 Bridge ,从网络角度上看,我们是把它当作一个二层交换机,所以不需要为它设置 IP 地址,但现在从宿主机的角度上看,br0 同时也是 Default Network Namespace 里面的一张网卡,我们可以直接为这张网卡设置 IP 后,来充当三层路由器(网关),参与主机的路由转发。

       我们在宿主机上给 br0 设备设置 IP 地址为 172.17.0.1 (同样位于子网 172.17.0.0/24 内):

       此时,宿主机会自动增加一条 Destination 为 172.17.0.0 的路由规则:

      按照这条路由规则(访问目标主机为 172.17.0.0/24 的数据包会转发到 br0 设备),我们现在的宿主机就可以直接访问容器了:

      现在宿主机可以访问容器了,回到容器访问宿主机的问题上,我们给容器增加路由转发规则,使默认所有路由(0.0.0.0 ,除了 172.17.0.0/24 子网内的 IP )都需要透过作用于三层的 br0 网关(172.17.0.1)来转递封包:

      这样一来,当容器访问宿主机 IP 时,数据包就会直接转发到 Default Network Namespace 的 br0 网卡上,宿主机发现收到的 IP 数据包是属于自己的,就会对其进行处理。

       也就是说现在容器也可以访问宿主机了:

 

5. 容器访问其它主机(外网)

      上面我们给容器配置了路由规则,使其访问宿主机 IP 时,数据包就会转发到 Default Network Namespace 的 br0 网卡上,宿主机发现收到的 IP 数据包是属于自己的,就会对其进行处理。

      但在 Linux 中,如果发现收到的 IP 数据包并不是属于自己的,是会将其直接丢弃的。这里我们准备了一个和宿主机(10.0.12.15)同一子网的另一台 IP 地址为 10.0.12.11 的主机 host2 来测试:

 

        在 container1 内 ping host2 ,以及访问外网,和我们预期一致,无法访问:

 

      如果我们想要在容器中可以访问其他主机,或者外网,我们就不能让 Linux 丢弃不属于自己的数据包,而是继续转发出去。要做到这一点,只需要打开 Linux 的 IP Forward 功能:

      但是,这里还是会有一个问题,当容器访问其他主机或者外网时(veth1->br0),Linux 也帮我们转发了出去(br0->eth0),但其他主机或者外网在响应了我们容器的请求时,它并不认识我们所配置的 172.17.0.0/24 这个网段,还是无法成功处理请求。所以我们还需要使用 NAT( Network Address Translation )技术:把 IP 数据报文头中的 IP 地址和端口转换成另一个 IP 地址和端口(NAT 技术也被用于 IPv4 地址枯竭问题)。

      在这里我们需要修改的是源 IP 地址,所以是 SNAT (Source NAT),把 IP 转换成宿主机出口网卡的 IP 。

      在 Linux 中,我们可以通过 iptables 中的 MASQUERADE 策略来实现这个 SNAT :

      这条规则的意思是将不是从网卡 br0 发出的且源地址为 172.17.0.0/24 的数据包(即容器中发出的数据包)做 SNAT 。

      此时,我们的容器就可以访问其它主机(外网)了。

 

6. 外部访问容器(容器端口映射)

       容器与外部网络通信,我们已经解决了容器和宿主机间的通信,容器访问其他主机(外网),还剩最后一个,外部来访问容器。

       在 Docker 中,要让外部可以访问容器服务,我们会做一次容器端口映射,如:

      通过 -p 参数就可以将容器内的 8080 端口映射到宿主机的 8000 端口上,这样外部就可以通过访问宿主机 IP + 8000 端口访问到我们的容器服务了。

       这一技术实现其实同样是使用到了 NAT 技术,只不过和上面的 SNAT 不同的是,这里我们需要修改的是目的 IP 地址,即 DNAT (Destination NAT),把宿主机上 8000 端口的流量请求转发到容器中的地址 172.17.0.101:8080 中。

       在 Linux 中,我们可以通过 iptables 中的 DNAT 策略来实现这个 DNAT :

[root@host ~]# iptables -t nat -A PREROUTING  ! -i br0 -p tcp -m tcp --dport 8000 -j DNAT --to-destination 172.17.0.101:8080
[root@host ~]# iptables -t nat -nL
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:8000 to:172.17.0.101:8080

Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination
MASQUERADE  all  --  172.17.0.0/24        0.0.0.0/0
[root@host ~]#

 

      这条规则的意思是将访问宿主机 8000 端口的请求转发到 172.17.0.101 的 8080 端口上,也就是说我们现在可以在 host2 主机(10.0.12.11)上通过访问 10.0.12.15:8000 来访问实际的 container1 服务了。

 

       到这里,我们就已经实现了一个和 Docker 默认网络模式一样的拓扑关系:

文章来自个人专栏
文章 | 订阅
0条评论
作者已关闭评论
作者已关闭评论
0
0