NAT

路由过程

image.png
一个ICMP的以太网格式报文:
|目的MAC|源MAC|目的IP|源IP|ICMP请求包|
主机A发送一个ICMP包到主机B, 此时目的IP为10.10.0.2,源IP为192.168.0.2。 主机A发现,这个IP和我不在一个网段,那就需要进行IP选路了。此时它就只能把包发往直接的网关了(192.168.0.1) ,主机A和网关在同一个网段,通过MAC地址进行交换,所以目的MAC就是MACRA, 源Mac就是MACHOSTA。 因此从主机A发出的一个以太网报文格式为:
|MACRA|MACHOSTA|10.10.0.2|192.168.0.2|ICMP请求包
路由器在接口A处收到以太网包进行分析,选出目的IP,根据自身的路由表发现,这个数据包要经过接口B发出啊。于是就在路由器接口B封装数据包
|MACHOSTB|MACRB|10.10.0.2|192.168.0.2|ICMP请求包
主机B在二层收到从路由器接口B发送的以太网报文进行解析发现目的IP是自己的,然后开始生成回复。首先发现目的IP是192.168.0.2.这货和我不是一个网段的啊,那我就把数据包先发给网关,让网关帮我邮递。于是封装出下面的以太网包:
|MACRB|MACHOSTB|192.168.0.2|10.10.0.2|ICMP回复包
路由器从接口B收到数据包之后,同样进行分析之后知道自己需要通过接口A发出。于是封装的以太网包:
|MACHOSA|MACRA|192.168.0.2|10.10.0.2|ICMP回复包
终于回复的数据包被主机A收到,整个ICMP过程完成。
有一个非常重要的原则要记住:在数据跳转的过程中,改变的是MAC,IP是始终是没有改变的。

SNAT

image.png
上图是我们家里常用的路由器连接示例,HOSTA和HOSTB连接到路由器的LAN口,当主机A访问8.8.8.8时,根据上面讲的路由过程,我们知道到达路由器的以太网包为:
|ROUTE_LAN_MAC|HOSTA|8.8.8.8|192.168.1.2:12345|TCP请求包
此时路由器通过WAN口将报文发送到公网上去,按这常理它的数据包格式应该是:
|下一跳路由MAC|ROUTE_WAN_MAC|8.8.8.8|192.168.1.2:12345|TCP请求包
我们在深入思考一下,假设这样的一个数据包发送到公网上之后,确定还能按原路找回来吗? 大家的路由器网关都是192.168.1.1 ,数据如果回来之后根本就不知道发送到哪一个路由器了。
这个时候就出现了NAT的技术,如果想让回复的数据包能够返回,那么我就需要更改源IP地址啊,于是路由器在从WAN口路由出去之后更改源IP为WAN口的IP。
|下一跳路由MAC|ROUTE_WAN_MAC|8.8.8.8|35.21.2.128:12345|TCP请求包
同时记录NAT转换的信息,保证回复的数据能够投递到指定的目标。这种技术叫做SNAT。
SNAT一般包含的信息有。

source destination protocal action
192.168.1.2:12345 8.8.8.8 tcp 192.168.1.2:12345 -> 35.21.2.128:12345

DNAT

从8.8.8.8返回到路由器A的包格式是:
|ROUTE_WAN_MAC|下一跳路由MAC|35.21.2.128:12345|8.8.8.8|TCP回复包
这个包很明显不会发送给HOSTA。
但是在之前记录SNAT的时候,一般会记录一条DNAT。

source destination action
8.8.8.8 35.21.2.128:12345 35.21.2.128:12345 -> 192.168.1.2:12345

这样回复过来的包从路由器A的LAN口发出去就是:
|HOSTA|ROUTE_LAN_MAC|192.168.1.2:12345|8.8.8.8|TCP回复包
但这个时候有一个问题,假如HOSTB也发送了一个
|ROUTE_LAN_MAC|HOSTB|8.8.8.8|192.168.1.3:12345|TCP请求包
那路由器收到的回复包就是
|ROUTE_WAN_MAC|下一跳路由MAC|35.21.2.128:12345|8.8.8.8|TCP回复包
此时8.8.8.8给HOSTA和HOSTB的回复包一样。路由器就不知道这个包发给谁了。
这个问题的一个解决方案是路由器保证从路由器发出去的包使用了不同的端口。
比如当HOSTA在和8.8.8.8建立连接之后,路由器上的12345这个端口就被暂用了。
当HOSTB使用192.168.1.3:12345向路由器发送目的地址是8.8.8.8的包时。路由器建立一个映射,192.168.1.3:12345 -> 192.168.1.3:12346,然后路由器WAN发送时的包就是:
|下一跳路由MAC|ROUTE_WAN_MAC|8.8.8.8|35.21.2.128:12346|TCP请求包
路由器记录的DNAT是

source destination action
8.8.8.8 35.21.2.128:12346 35.21.2.128:12346 -> 192.168.1.3:12345

此时,从8.8.8.8返回的包就是:
|ROUTE_WAN_MAC|下一跳路由MAC|35.21.2.128:12346|8.8.8.8|TCP回复包
这种区分同一网络里主机访问同一地址的NAT被叫做NAPT(Network Address Port Translation)。但是它存在限制,即其通信仅限于TCP或UDP。当然还有很多其他方式…

Docker网络

打包一个测试镜像

部署一个docker镜像。此镜像由Java编写,可以输出源主机和目的主机的IP。

  1. @RestController
  2. public class UaController {
  3. @GetMapping(value = "/hello")
  4. public String hello(HttpServletRequest request) {
  5. InetAddress ia = null;
  6. try {
  7. ia = InetAddress.getLocalHost();
  8. } catch (UnknownHostException e) {
  9. return "无法获取本机网络地址信息";
  10. }
  11. String info = "ClientIp : " + request.getRemoteAddr() + ", ServerName : " + ia.getHostName() + ", ServerIp : " + ia.getHostAddress() + "\n";
  12. System.out.println(info);
  13. return info;
  14. }
  15. }

部署镜像之后。
docker0的IP:172.17.0.1/16。
容器的IP:172.17.0.6/16。
宿主机内网的IP:10.0.8.9。
宿主机公网的IP:81.71.14.12。
shell终端的IP:197.168.6.78

查看系统基本信息

先把后面会使用到的系统信息都展示出来。

  1. [root@tengxunyun1412 docker_build]# route -n
  2. Kernel IP routing table
  3. Destination Gateway Genmask Flags Metric Ref Use Iface
  4. 0.0.0.0 10.0.8.1 0.0.0.0 UG 100 0 0 eth0
  5. 10.0.8.0 0.0.0.0 255.255.252.0 U 100 0 0 eth0
  6. 172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
  7. [root@tengxunyun1412 docker_build]# ip addr
  8. 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
  9. link/ether 52:54:00:d4:80:dd brd ff:ff:ff:ff:ff:ff
  10. inet 10.0.8.9/22 brd 10.0.11.255 scope global noprefixroute eth0
  11. valid_lft forever preferred_lft forever
  12. inet6 fe80::5054:ff:fed4:80dd/64 scope link noprefixroute
  13. valid_lft forever preferred_lft forever
  14. 3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
  15. link/ether 02:42:d2:84:78:c6 brd ff:ff:ff:ff:ff:ff
  16. inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
  17. valid_lft forever preferred_lft forever
  18. inet6 fe80::42:d2ff:fe84:78c6/64 scope link
  19. valid_lft forever preferred_lft forever
  20. 63: vethf17af16@if62: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
  21. link/ether 4a:d6:4e:06:6f:70 brd ff:ff:ff:ff:ff:ff link-netnsid 0
  22. inet6 fe80::48d6:4eff:fe06:6f70/64 scope link
  23. valid_lft forever preferred_lft forever
  24. [root@tengxunyun1412 docker_build]# docker exec -it 95ce2bd ip addr
  25. 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
  26. link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
  27. inet 127.0.0.1/8 scope host lo
  28. valid_lft forever preferred_lft forever
  29. 62: eth0@if63: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
  30. link/ether 02:42:ac:11:00:06 brd ff:ff:ff:ff:ff:ff
  31. inet 172.17.0.6/16 brd 172.17.255.255 scope global eth0
  32. valid_lft forever preferred_lft forever
  33. [root@tengxunyun1412 ~]# docker exec -it 95ce2bd ip route list
  34. default via 172.17.0.1 dev eth0
  35. 172.17.0.0/16 dev eth0 proto kernel scope link src 172.17.0.6
  36. [root@tengxunyun1412 docker_build]# iptables -t nat -L -n
  37. Chain PREROUTING (policy ACCEPT)
  38. target prot opt source destination
  39. DOCKER all -- 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL
  40. Chain INPUT (policy ACCEPT)
  41. target prot opt source destination
  42. Chain POSTROUTING (policy ACCEPT)
  43. target prot opt source destination
  44. MASQUERADE all -- 172.17.0.0/16 0.0.0.0/0
  45. MASQUERADE tcp -- 172.17.0.6 172.17.0.6 tcp dpt:56010
  46. Chain OUTPUT (policy ACCEPT)
  47. target prot opt source destination
  48. DOCKER all -- 0.0.0.0/0 !127.0.0.0/8 ADDRTYPE match dst-type LOCAL
  49. Chain DOCKER (2 references)
  50. target prot opt source destination
  51. RETURN all -- 0.0.0.0/0 0.0.0.0/0
  52. DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:56010 to:172.17.0.6:56010

开启了ip_forward

  1. [root@tengxunyun1412 ~]# cat /etc/sysctl.conf
  2. # Controls IP packet forwarding
  3. # 改为0就禁用了,sysctl -p保存
  4. net.ipv4.ip_forward = 1

Docker网络模型

image.png

外部数据包运输到容器

从shell所在的机子通过公网IP访问。
image.png
shell终端的发出的数据包,由于需要访问公网,所以其所在的网络的路由器给它做了SNAT,即看见的是58.251.36.2(中间可能做了多次NAT,为了方便描述,这里就假设只有一层了)。
云服务器是不绑定具有公网IP的网卡的,所以这个包会发送到云服务器所在的网络的外网网关上,
网关有一条DNAT。

source destination action
0.0.0.0 81.71.14.12 81.71.14.12 -> 10.0.8.9

我们发送的数据包这样就能找到服务器10.0.8.9了。用tcpdump抓个包看看:

  1. [root@tengxunyun1412 ~]# tcpdump tcp -i eth0 -t -s 0 -n and dst port 56010 and src net 58.251.36.2
  2. dropped privs to tcpdump
  3. tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
  4. listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
  5. IP 58.251.36.2.20357 > 10.0.8.9.56010: Flags [S], seq 2519377287, win 64240, options [mss 1424,nop,wscale 8,nop,nop,sackOK], length 0
  6. IP 58.251.36.2.40748 > 10.0.8.9.56010: Flags [S], seq 578652413, win 64240, options [mss 1424,nop,wscale 8,nop,nop,sackOK], length 0
  7. IP 58.251.36.2.40748 > 10.0.8.9.56010: Flags [.], ack 1809884930, win 517, length 0
  8. IP 58.251.36.2.20357 > 10.0.8.9.56010: Flags [.], ack 2626063158, win 517, length 0
  9. IP 58.251.36.2.40748 > 10.0.8.9.56010: Flags [P.], seq 0:505, ack 1, win 517, length 505
  10. IP 58.251.36.2.40748 > 10.0.8.9.56010: Flags [.], ack 235, win 516, length 0
  • tcp: ip icmp arp rarp 和 tcp、udp、icmp这些选项等都要放到第一个参数的位置,用来过滤数据报的类型
  • -i eth0 : 只抓经过接口eth1的包
  • -t : 不显示时间戳
  • -s 0 : 抓取数据包时默认抓取长度为68字节。加上-S 0 后可以抓到完整的数据包
  • dst port 56010: 抓取目标端口是56010的数据包
  • src net 58.251.36.2 : 数据包的源网络地址为58.251.36.2

可以看到我们抓的包的目的IP都是10.0.8.9。
当数据包到达eth0网卡的时候,Linux会发现这个包能被DNAT命中

  1. Chain DOCKER (2 references)
  2. target prot opt source destination
  3. RETURN all -- 0.0.0.0/0 0.0.0.0/0
  4. DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:56010 to:172.17.0.6:56010

于是这个包的目的IP就变成了172.17.0.6。
Linux发现这个包的目的不是eth0的网段,正常情况是要丢弃的,但是由于我们开起了ip_forward,这个参数可以让数据包在网卡之间转发。

IP forwarding is the ability for an operating system to accept incoming network packets on one interface, recognize that it is not meant for the system itself, but that it should be passed on to another network, and then forwards it accordingly. This is what you need when you have for example a system setup that is sitting between two different networks and needs to pass traffic between them.

当安装了Docker之后,ip_forward是自动开启的。所以此时Linux一查路由表发现,172.17.0.6是docker0所在的网段,就会将这个包转发至docker0网卡。
此时docker0网卡就会发现这个IP是172.17.0.6,就会将包发送到vethf17af16@if62这张网卡上(docker0内部维护了一个映射?),veth-pair有一个特点,从其中一个“网卡”发出的数据包,可以直接出现在与它对应的另一张“网卡”上,也就是发送到172.17.0.6这个容器内。
包就到达了容器。由于从58.251.36.2出来后,就没在做SNAT了,所以容器内看见的源IP是58.251.36.2。目的IP是172.17.0.6。
注意一下这个DNAT。

  1. Chain DOCKER (2 references)
  2. target prot opt source destination
  3. RETURN all -- 0.0.0.0/0 0.0.0.0/0
  4. DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:56010 to:172.17.0.6:56010

它的意思是网卡收到的所以目的端口为56010的包都转发至172.17.0.6:56010。所以实际上可以得到下面的结果。

  1. [root@tengxunyun1412 ~]# curl 172.17.0.1:56010/hello
  2. ClientIp : 172.17.0.1, ServerName : 4d919c144a8d, ServerIp : 172.17.0.6

但是下面的命令却不行,我本来的猜测是这个命令也可以的,为什么?

  1. [root@tengxunyun1412 ~]# curl 172.17.0.3:56010/hello
  2. curl: (7) Failed to connect to 172.17.0.3 port 56010: Connection refused

curl 10.0.8.8:56010/hello自然不行,因为目的地址不是docker0的网段,所以由eth0无法ip_forward到docker0。

容器数据包运输到外部

当我们从容器发送一个包到公网。

  1. [root@tengxunyun1412 ~]# tcpdump tcp -i docker0 -s 0 -n and dst port 80 and dst net 14.215.177.38
  2. dropped privs to tcpdump
  3. tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
  4. listening on docker0, link-type EN10MB (Ethernet), capture size 262144 bytes
  5. 20:18:21.133077 IP 172.17.0.6.59172 > 14.215.177.38.http: Flags [S], seq 926578119, win 29200, options [mss 1460,sackOK,TS val 86389301 ecr 0,nop,wscale 7], length 0
  6. 20:18:21.136244 IP 172.17.0.6.59172 > 14.215.177.38.http: Flags [.], ack 861763704, win 229, length 0
  7. 20:18:21.136300 IP 172.17.0.6.59172 > 14.215.177.38.http: Flags [P.], seq 0:77, ack 1, win 229, length 77: HTTP: GET / HTTP/1.1
  8. 20:18:21.140319 IP 172.17.0.6.59172 > 14.215.177.38.http: Flags [.], ack 1421, win 251, length 0
  9. 20:18:21.140360 IP 172.17.0.6.59172 > 14.215.177.38.http: Flags [.], ack 2782, win 274, length 0
  10. 20:18:21.140497 IP 172.17.0.6.59172 > 14.215.177.38.http: Flags [F.], seq 77, ack 2782, win 274, length 0
  11. 20:18:21.143709 IP 172.17.0.6.59172 > 14.215.177.38.http: Flags [.], ack 2783, win 274, length 0
  12. [root@tengxunyun1412 ~]# tcpdump tcp -i eth0 -s 0 -n and dst port 80 and dst net 14.215.177.38
  13. dropped privs to tcpdump
  14. tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
  15. listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
  16. 20:18:21.133122 IP 10.0.8.9.59172 > 14.215.177.38.http: Flags [S], seq 926578119, win 29200, options [mss 1460,sackOK,TS val 86389301 ecr 0,nop,wscale 7], length 0
  17. 20:18:21.136256 IP 10.0.8.9.59172 > 14.215.177.38.http: Flags [.], ack 861763704, win 229, length 0
  18. 20:18:21.136326 IP 10.0.8.9.59172 > 14.215.177.38.http: Flags [P.], seq 0:77, ack 1, win 229, length 77: HTTP: GET / HTTP/1.1
  19. 20:18:21.140336 IP 10.0.8.9.59172 > 14.215.177.38.http: Flags [.], ack 1421, win 251, length 0
  20. 20:18:21.140366 IP 10.0.8.9.59172 > 14.215.177.38.http: Flags [.], ack 2782, win 274, length 0
  21. 20:18:21.140511 IP 10.0.8.9.59172 > 14.215.177.38.http: Flags [F.], seq 77, ack 2782, win 274, length 0
  22. 20:18:21.143717 IP 10.0.8.9.59172 > 14.215.177.38.http: Flags [.], ack 2783, win 274, length 0

这个包肯定先发给docker0,docker0通过路由表做ip_forward。会将包发送到eth0上,然后会命中SNAT.

  1. Chain POSTROUTING (policy ACCEPT)
  2. target prot opt source destination
  3. MASQUERADE all -- 172.17.0.0/16 0.0.0.0/0

这个SNAT 是自动SNAT,也就是说Linux会挑一个可用的IP设置到当前数据包的源IP中,这样返回来的包就能被收到。

测试Client的网关

从shell所在的机子通过公网IP访问。
image.png
从本机通过容器IP访问:

  1. [root@tengxunyun1412 docker_build]# curl 172.17.0.6:56010/hello
  2. ClientIp : 172.17.0.1, ServerName : 72a8128ede25, ServerIp : 172.17.0.6

从本机通过宿主机内网网卡IP访问:

  1. [root@tengxunyun1412 docker_build]# curl 10.0.8.9:56010/hello
  2. ClientIp : 10.0.8.9, ServerName : 72a8128ede25, ServerIp : 172.17.0.6

同一内网的其他机子(IP:10.0.8.10)访问

  1. [root@tengxunyun1413 docker_build]# curl 10.0.8.9:56010/hello
  2. ClientIp : 10.0.8.10, ServerName : 72a8128ede25, ServerIp : 172.17.0.6

从本机通过宿主机公网网卡IP访问:

  1. [root@tengxunyun1412 docker_build]# curl 81.71.14.12:56010/hello
  2. ClientIp : 81.71.14.12, ServerName : 72a8128ede25, ServerIp : 172.17.0.6

本机回环路径:

  1. [root@tengxunyun1412 docker_build]# curl 127.0.0.1:56010/hello
  2. ClientIp : 172.17.0.1, ServerName : 72a8128ede25, ServerIp : 172.17.0.6

iptables

image.png