一次linux虚拟网络模型的试验——探究docker网络模型之路

251 阅读9分钟

Linux 虚拟网络模型

为了支持网络协议栈的多个实例,linux在网络协议栈引入了网络命名空间,这些独立的协议栈被隔离到不同 的命名空间中,处于不同的命名空间的网络协议栈事完全隔离的,彼此之间无法通信。docker 就是通过这种实现了不同容器之间的隔离。Veth这个设备对可以联通两个不同的命名空间,使得两个命名空间可以通信,在多个命名空间时则多会使用bridge,Netfilter和iptables则实现了数据包的过滤、修改和丢弃,本文将会从这几点讲述如何实现linux虚拟网络模型。

从netns和veth说起

netns是linux的网络命名空间,它们之间的网络是隔离的,veth则可以联通两个不同的命名空间。

    1  ip netns add ns1
    2  ip netns ls
    3  ip netns add ns2
    #创建了两个命名空间
    4  ip link add veth0 type veth peer name veth1
    #创建了一对veth,veth总是成对出现的
    5  ip link show

到这里命名空间和veth都创建完毕,可以通过ip netns lsip link show分别看到。 ip netns ls的结果

ns2
ns1 (id: 0)

ip link show的结果

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
    link/ether 06:99:32:53:81:1e brd ff:ff:ff:ff:ff:ff
3: veth1@veth0: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 7e:8d:5f:08:37:21 brd ff:ff:ff:ff:ff:ff
4: veth0@veth1: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 4e:22:2b:5e:8e:1f brd ff:ff:ff:ff:ff:ff

接下来我们把veth绑在netns上

    6  ip link set veth1 netns ns1
    8  ip link set veth0 netns ns2

再执行ip link show

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
    link/ether 06:99:32:53:81:1e brd ff:ff:ff:ff:ff:ff

可以看到已经看不到之前的veth,他们已经被绑在了ns1和ns2上了,此时可以通过ip netns exec ns1 ip link show来看 执行ip netns exec ns1 ip link show

1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
3: veth1@if4: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 7e:8d:5f:08:37:21 brd ff:ff:ff:ff:ff:ff link-netnsid 1

同时,已被绑定的veth也可以通过ip netns exec ns1执行ip link命令进行换绑

   11  ip netns exec ns1 ip link set veth1 netns ns2
   13  ip netns exec ns2 ip link set veth0 netns ns1

此时他们的veth交换了位置,现在两个veth绑定到了两个netns里,但是他们还不可以通信,因为没有绑定ip,现在我们来绑定ip

   14  ip netns exec ns1 ip addr add 10.1.1.1/24 dev veth0
   15  ip netns exec ns2 ip addr add 10.1.1.2/24 dev veth1

启动两个ip

   16  ip netns exec ns1 ip link set dev veth0 up
   17  ip netns exec ns2 ip link set dev veth1 up

尝试ping一下ip netns exec ns1 ping 10.1.1.2

PING 10.1.1.2 (10.1.1.2) 56(84) bytes of data.
64 bytes from 10.1.1.2: icmp_seq=1 ttl=64 time=0.035 ms
64 bytes from 10.1.1.2: icmp_seq=2 ttl=64 time=0.028 ms
64 bytes from 10.1.1.2: icmp_seq=3 ttl=64 time=0.028 ms
64 bytes from 10.1.1.2: icmp_seq=4 ttl=64 time=0.030 ms
^C
--- 10.1.1.2 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 2997ms
rtt min/avg/max/mdev = 0.028/0.030/0.035/0.004 ms

Ohhhhhhhhhh,成功了。

Bridge使通讯更丰富

Linux内核是通过一个虚拟的网桥设备来实现桥接的,这个虚拟设备可以绑定若干个接口设备,从将他们桥接起来,作为关键的是,它还可以有一个ip地址。

ip netns delete ns1
ip netns delete ns2

先把之前的两个网络空间删掉,然后开始下面的配置 先配置ns1

# 创建 Network Namespace 1
sudo ip netns add ns1

# 创建 veth
sudo ip link add veth0 type veth peer name veth_ns_1

# 将一个veth移至 ns1 中
sudo ip link set veth0 netns ns1

# 配置这个veth的ip,并启用 ns1 中的网络接口
sudo ip netns exec ns1 ifconfig veth0 175.18.0.2/24 up
sudo ip netns exec ns1 ifconfig lo up

# 启用留在默认网络命名空间中的虚拟网卡
sudo ifconfig veth_ns_1 up

查看ns1中的网络情况ip netns exec ns1 ip addr

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1
    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
6: veth0@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether a6:04:d5:bb:ec:30 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 175.18.0.2/24 brd 175.18.0.255 scope global veth0
       valid_lft forever preferred_lft forever
    inet6 fe80::a404:d5ff:febb:ec30/64 scope link
       valid_lft forever preferred_lft forever

对于ns2采取同样的操作

sudo ip netns add ns2
sudo ip link add veth0 type veth peer name veth_ns_2
sudo ip link set veth0 netns ns2
sudo ip netns exec ns2 ifconfig veth0 175.18.0.3/24 up
sudo ip netns exec ns2 ifconfig lo up
sudo ifconfig veth_ns_2 up

接下来配置网桥,主要是把两个留出来的veth端绑在bridge上

# 创建一个网桥
# brctl在 bridge-utils 软件包中,没有的话需要使用 apt-get 下载
sudo brctl addbr ns_br

# 配置网桥的ip,并启用
sudo ifconfig ns_br 175.18.0.1/24 up

# 配置路由
sudo route add -net 175.18.0.0/24 dev ns_br

# 将两个虚拟网卡添加到网桥上
sudo brctl addif ns_br veth_ns_1
sudo brctl addif ns_br veth_ns_2

# 配置两个网络命名空间的默认路由
sudo ip netns exec ns1 ip route add default via 175.18.0.1 dev veth0
sudo ip netns exec ns2 ip route add default via 175.18.0.1 dev veth0

现在bridge和两个netns都可通信了

sudo ip netns exec ns2 ping 175.18.0.1
sudo ip netns exec ns1 ping 175.18.0.1
sudo ping -I ns_br 175.18.0.2
sudo ping -I ns_br 175.18.0.3

但是,它们之间是无法通信的,或许也有可能,这个就牵涉到 iptables 的问题了。

Linux iptables

如果说bridge的作用像一个交换机,那iptable则是负责过滤交换机上转发的数据包

cat /proc/sys/net/ipv4/ip_forward

首先看一下转发功能有没有开启,如果返回为零

sudo sysctl -w net.ipv4.conf.all.forwarding=1

开启转发,再去看的时候就会返回1了 iptables 有五个表(table),这里先查看 FILTER 表,输入如下命令查看

iptables -t filter -n --list

关注Forward,它用于处理转发规则

Chain FORWARD (policy ACCEPT)

如果你的policy 是DROP的话可以执行

iptables -t filter --policy FORWARD ACCEPT

这样就可以对数据包放行,再去ping的时候就可以ping通了

ip netns exec ns1 ping -c 3 175.18.0.3
PING 175.18.0.3 (175.18.0.3) 56(84) bytes of data.
64 bytes from 175.18.0.3: icmp_seq=1 ttl=64 time=0.038 ms
64 bytes from 175.18.0.3: icmp_seq=2 ttl=64 time=0.041 ms
64 bytes from 175.18.0.3: icmp_seq=3 ttl=64 time=0.041 ms

--- 175.18.0.3 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1999ms
rtt min/avg/max/mdev = 0.038/0.040/0.041/0.001 ms

ping本机的ip也没有问题

ip netns exec ns1 ping 172.31.30.66
PING 172.31.30.66 (172.31.30.66) 56(84) bytes of data.
64 bytes from 172.31.30.66: icmp_seq=1 ttl=64 time=0.029 ms
64 bytes from 172.31.30.66: icmp_seq=2 ttl=64 time=0.036 ms
64 bytes from 172.31.30.66: icmp_seq=3 ttl=64 time=0.036 ms
^C
--- 172.31.30.66 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 1998ms
rtt min/avg/max/mdev = 0.029/0.033/0.036/0.007 ms

但如果你尝试去ping 外部其他ip的时候就会发现无法ping通,以百度为例。

root@ip-172-31-30-66:~# ping www.baidu.com
PING www.a.shifen.com (220.181.38.149) 56(84) bytes of data.
64 bytes from 220.181.38.149: icmp_seq=1 ttl=46 time=21.6 ms
64 bytes from 220.181.38.149: icmp_seq=2 ttl=46 time=21.7 ms
^C
--- www.a.shifen.com ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 21.625/21.670/21.716/0.154 ms

root@ip-172-31-30-66:~# ip netns exec ns1 ping 220.181.38.149
#不ping域名的原因是无法对其解析

PING 220.181.38.149 (220.181.38.149) 56(84) bytes of data.
^C
--- 220.181.38.149 ping statistics ---
4 packets transmitted, 0 received, 100% packet loss, time 2999ms

这是因为从 ns1 发送 ICMP 报文至 220.181.38.149 是可以成功的,此时报文的源地址为 175.18.0.2 。但是在 220.181.38.149 回复报文的时候,ICMP 报文的目的地址为 175.18.0.2,这个是一个内网的IP,所以数据包必然丢失。

所以需要在数据包离开之前将源地址修改为172.31.30.66对应的外网ip,iptables的NET表就是用来做这个的

iptables -t nat -A POSTROUTING -s 175.18.0.0/24 -j MASQUERADE

POSTROUTING 链也为 iptables 的五个链之一,是用来做 SNAT (源地址转换的),而 MASQUERADE 策略是:报文从哪个网卡出就用该网卡上的 IP 地址替换该报文的源地址,而在这里, 175.18.0.2 被 172.31.30.66的外网地址替换。 -A 表示 append,也就是向 NAT 表的 POSTROUTING 链追加设置,如果把 -A 换成 -D 则为删除此规则。-t 用于指定表(table), -s 表示源地址, -j 表示 jump。 再去ping一下baidu

ip netns exec ns1 ping -c 3 www.baidu.com
#现在也可以从DNS解析IP了
PING www.a.shifen.com (220.181.38.150) 56(84) bytes of data.
64 bytes from 220.181.38.150: icmp_seq=1 ttl=45 time=20.6 ms
64 bytes from 220.181.38.150: icmp_seq=2 ttl=45 time=20.7 ms
64 bytes from 220.181.38.150: icmp_seq=3 ttl=45 time=20.7 ms

--- www.a.shifen.com ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 20.630/20.706/20.751/0.174 ms

到现在两个网络空间已经都可以请求外部了,那他们如何被外部访问呢? 先在两个网络空间内启动python的web服务器

ip netns exec ns1 nohup python3 -m http.server 80 &
ip netns exec ns2 nohup python3 -m http.server 80 & 

可以直接curl到两个服务器

root@ip-172-31-30-66:~# curl -I http://175.18.0.2
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.5.2
Date: Fri, 06 Nov 2020 06:30:38 GMT
Content-type: text/html; charset=utf-8
Content-Length: 629

root@ip-172-31-30-66:~# curl -I http://175.18.0.3
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.5.2
Date: Fri, 06 Nov 2020 06:30:38 GMT
Content-type: text/html; charset=utf-8
Content-Length: 629

docker -p/-P的映射是将容器中的端口和物理机端口做了映射,这个映射使用 iptables 实现的,我们也可以用iptables来实现映射。

iptables -t nat -A PREROUTING -p tcp --dport 8088 -j DNAT --to 175.18.0.2:80
iptables -t nat -A PREROUTING -p tcp --dport 8089 -j DNAT --to 175.18.0.3:80

现在两个pthon3打开的文件服务器已经开放在8088和8089啦

结语

本文中讲解的linux虚拟网络模型十分的基础,和docker所实现的网络模型相差很大,但原理有所相通,后续会继续搞docker、kubernetes、istio的网络模型分析实验,欢迎关注👏

参考