不论是Kubernetes还是Docker本身,我们在一个节点上暴露端口对外提供服务的时候,总是少不了端口转发。例如以下方式:
docker run -d -p 8080:80 nginx:latest
就是用最简单的方法,在本机暴露8080端口,转发到容器内的80端口,这样一来外界可以通过本机IP+8080端口就可以访问容器内提供的nginx服务了。
大家有没有想过,这是怎么做到的呢?
其实不难,Docker实现本身就是高度依赖Linux内核的若干模块,这里的端口转发也不例外,也是用了Linux iptables的小把戏,接下来我们就来分析下这个小把戏。
一. 从log入手
既是知道Docker的端口转发与iptables有关,那么究竟有什么样的关系呢?我觉得从log入手,尝试记录下数据包通过iptables的轨迹,然后再作分析吧。
在不确定具体的涉及哪些iptables表的情况下,我想最好的方法是把所有的可能的表都加上适当的log,从而记录数据包在各个表和链之间的流转。
接下来,我们就来照此思路去探究iptables如何做到端口转发。
1. 开启iptables的log
在CentOS7中打开 /etc/rsyslog.conf ,并添加如下配置:
kern.* /var/log/iptables.log
并重启rsyslog:
systemctl restart rsyslog.service
这样,就可以在 /var/log/iptables.log 中看到iptables的log了,当然,前提是你得有log输出。
2. 在nat表中增加log规则
在操作以前,首先保存原先的iptables配置,以免后续操作带有破坏性,也便于还原:
iptables-save > iptb_origin
然后,分别在所有可能的表的所有的可能涉及的链中增加对于源目端口为80或者8080的DEBUG log,可以用如下脚本方便进行:
#!/bin/bash for table in raw filter nat mangle do for chain in PREROUTING OUTPUT FORWARD INPUT POSTROUTING do for port in 8080 80 do iptables -t ${table} -A ${chain} -p tcp -m tcp --dport $port -j LOG --log-prefix "[debug]-${table}-${chain}:" --log-level 7 || true iptables -t ${table} -A ${chain} -p tcp -m tcp --sport $port -j LOG --log-prefix "[debug]-${table}-${chain}:" --log-level 7 || true done done done
可能某些表无法加入某些链,不过,我们暂且忽略,为了简化逻辑,直接等以上脚本执行完,而忽略其中的错误。
3. 发起请求
从外部地址 1.2.3.4 发起请求:
curl 172.20.242.183:8080
我们再看
1 Jan 21 18:09:52 centos7 kernel: [debug]-raw-PREROUTING:IN=eth0 OUT= MAC=00:16:3e:01:ae:15:ee:ff:ff:ff:ff:ff:08:00 SRC=1.2.3.4 DST=172.20.242.183 LEN=64 TOS=0x14 PREC=0x00 TTL=48 ID=0 DF PROTO=TCP SPT=10656 DPT=8080 WINDOW=65535 RES=0x00 SYN URGP=0 2 Jan 21 18:09:52 centos7 kernel: [debug]-mangle-PREROUTING:IN=eth0 OUT= MAC=00:16:3e:01:ae:15:ee:ff:ff:ff:ff:ff:08:00 SRC=1.2.3.4 DST=172.20.242.183 LEN=64 TOS=0x14 PREC=0x00 TTL=48 ID=0 DF PROTO=TCP SPT=10656 DPT=8080 WINDOW=65535 RES=0x00 SYN URGP=0 3 Jan 21 18:09:52 centos7 kernel: [debug]-nat-PREROUTING:IN=eth0 OUT= MAC=00:16:3e:01:ae:15:ee:ff:ff:ff:ff:ff:08:00 SRC=1.2.3.4 DST=172.20.242.183 LEN=64 TOS=0x14 PREC=0x00 TTL=48 ID=0 DF PROTO=TCP SPT=10656 DPT=8080 WINDOW=65535 RES=0x00 SYN URGP=0 4 Jan 21 18:09:52 centos7 kernel: [debug]-mangle-FORWARD:IN=eth0 OUT=docker0 MAC=00:16:3e:01:ae:15:ee:ff:ff:ff:ff:ff:08:00 SRC=1.2.3.4 DST=172.17.0.2 LEN=64 TOS=0x14 PREC=0x00 TTL=47 ID=0 DF PROTO=TCP SPT=10656 DPT=80 WINDOW=65535 RES=0x00 SYN URGP=0 5 Jan 21 18:09:52 centos7 kernel: [debug]-mangle-POSTROUTING:IN= OUT=docker0 SRC=1.2.3.4 DST=172.17.0.2 LEN=64 TOS=0x14 PREC=0x00 TTL=47 ID=0 DF PROTO=TCP SPT=10656 DPT=80 WINDOW=65535 RES=0x00 SYN URGP=0 6 Jan 21 18:09:52 centos7 kernel: [debug]-nat-POSTROUTING:IN= OUT=docker0 SRC=1.2.3.4 DST=172.17.0.2 LEN=64 TOS=0x14 PREC=0x00 TTL=47 ID=0 DF PROTO=TCP SPT=10656 DPT=80 WINDOW=65535 RES=0x00 SYN URGP=0 7 Jan 21 18:09:52 centos7 kernel: [debug]-raw-PREROUTING:IN=docker0 OUT= PHYSIN=veth57b521b MAC=02:42:96:cd:15:f7:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=1.2.3.4 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=80 DPT=10656 WINDOW=28960 RES=0x00 ACK SYN URGP=0 8 Jan 21 18:09:52 centos7 kernel: [debug]-mangle-PREROUTING:IN=docker0 OUT= PHYSIN=veth57b521b MAC=02:42:96:cd:15:f7:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=1.2.3.4 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=80 DPT=10656 WINDOW=28960 RES=0x00 ACK SYN URGP=0 9 Jan 21 18:09:52 centos7 kernel: [debug]-mangle-FORWARD:IN=docker0 OUT=eth0 PHYSIN=veth57b521b MAC=02:42:96:cd:15:f7:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=1.2.3.4 LEN=60 TOS=0x00 PREC=0x00 TTL=63 ID=0 DF PROTO=TCP SPT=80 DPT=10656 WINDOW=28960 RES=0x00 ACK SYN URGP=0 10 Jan 21 18:09:52 centos7 kernel: [debug]-mangle-POSTROUTING:IN= OUT=eth0 PHYSIN=veth57b521b SRC=172.17.0.2 DST=1.2.3.4 LEN=60 TOS=0x00 PREC=0x00 TTL=63 ID=0 DF PROTO=TCP SPT=80 DPT=10656 WINDOW=28960 RES=0x00 ACK SYN URGP=0 11 Jan 21 18:09:52 centos7 kernel: [debug]-raw-PREROUTING:IN=eth0 OUT= MAC=00:16:3e:01:ae:15:ee:ff:ff:ff:ff:ff:08:00 SRC=1.2.3.4 DST=172.20.242.183 LEN=52 TOS=0x14 PREC=0x00 TTL=48 ID=0 DF PROTO=TCP SPT=10656 DPT=8080 WINDOW=2061 RES=0x00 ACK URGP=0 12 Jan 21 18:09:52 centos7 kernel: [debug]-mangle-PREROUTING:IN=eth0 OUT= MAC=00:16:3e:01:ae:15:ee:ff:ff:ff:ff:ff:08:00 SRC=1.2.3.4 DST=172.20.242.183 LEN=52 TOS=0x14 PREC=0x00 TTL=48 ID=0 DF PROTO=TCP SPT=10656 DPT=8080 WINDOW=2061 RES=0x00 ACK URGP=0 13 Jan 21 18:09:52 centos7 kernel: [debug]-mangle-FORWARD:IN=eth0 OUT=docker0 MAC=00:16:3e:01:ae:15:ee:ff:ff:ff:ff:ff:08:00 SRC=1.2.3.4 DST=172.17.0.2 LEN=52 TOS=0x14 PREC=0x00 TTL=47 ID=0 DF PROTO=TCP SPT=10656 DPT=80 WINDOW=2061 RES=0x00 ACK URGP=0 14 Jan 21 18:09:52 centos7 kernel: [debug]-mangle-POSTROUTING:IN= OUT=docker0 SRC=1.2.3.4 DST=172.17.0.2 LEN=52 TOS=0x14 PREC=0x00 TTL=47 ID=0 DF PROTO=TCP SPT=10656 DPT=80 WINDOW=2061 RES=0x00 ACK URGP=0 15 Jan 21 18:09:52 centos7 kernel: [debug]-raw-PREROUTING:IN=eth0 OUT= MAC=00:16:3e:01:ae:15:ee:ff:ff:ff:ff:ff:08:00 SRC=1.2.3.4 DST=172.20.242.183 LEN=134 TOS=0x14 PREC=0x00 TTL=48 ID=0 DF PROTO=TCP SPT=10656 DPT=8080 WINDOW=2061 RES=0x00 ACK PSH URGP=0 16 Jan 21 18:09:52 centos7 kernel: [debug]-mangle-PREROUTING:IN=eth0 OUT= MAC=00:16:3e:01:ae:15:ee:ff:ff:ff:ff:ff:08:00 SRC=1.2.3.4 DST=172.20.242.183 LEN=134 TOS=0x14 PREC=0x00 TTL=48 ID=0 DF PROTO=TCP SPT=10656 DPT=8080 WINDOW=2061 RES=0x00 ACK PSH URGP=0 17 Jan 21 18:09:52 centos7 kernel: [debug]-mangle-FORWARD:IN=eth0 OUT=docker0 MAC=00:16:3e:01:ae:15:ee:ff:ff:ff:ff:ff:08:00 SRC=1.2.3.4 DST=172.17.0.2 LEN=134 TOS=0x14 PREC=0x00 TTL=47 ID=0 DF PROTO=TCP SPT=10656 DPT=80 WINDOW=2061 RES=0x00 ACK PSH URGP=0 18 Jan 21 18:09:52 centos7 kernel: [debug]-mangle-POSTROUTING:IN= OUT=docker0 SRC=1.2.3.4 DST=172.17.0.2 LEN=134 TOS=0x14 PREC=0x00 TTL=47 ID=0 DF PROTO=TCP SPT=10656 DPT=80 WINDOW=2061 RES=0x00 ACK PSH URGP=0 19 Jan 21 18:09:52 centos7 kernel: [debug]-raw-PREROUTING:IN=docker0 OUT= PHYSIN=veth57b521b MAC=02:42:96:cd:15:f7:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=1.2.3.4 LEN=52 TOS=0x00 PREC=0x00 TTL=64 ID=22142 DF PROTO=TCP SPT=80 DPT=10656 WINDOW=227 RES=0x00 ACK URGP=0 20 Jan 21 18:09:52 centos7 kernel: [debug]-mangle-PREROUTING:IN=docker0 OUT= PHYSIN=veth57b521b MAC=02:42:96:cd:15:f7:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=1.2.3.4 LEN=52 TOS=0x00 PREC=0x00 TTL=64 ID=22142 DF PROTO=TCP SPT=80 DPT=10656 WINDOW=227 RES=0x00 ACK URGP=0 21 Jan 21 18:09:52 centos7 kernel: [debug]-mangle-FORWARD:IN=docker0 OUT=eth0 PHYSIN=veth57b521b MAC=02:42:96:cd:15:f7:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=1.2.3.4 LEN=52 TOS=0x00 PREC=0x00 TTL=63 ID=22142 DF PROTO=TCP SPT=80 DPT=10656 WINDOW=227 RES=0x00 ACK URGP=0 22 Jan 21 18:09:52 centos7 kernel: [debug]-mangle-POSTROUTING:IN= OUT=eth0 PHYSIN=veth57b521b SRC=172.17.0.2 DST=1.2.3.4 LEN=52 TOS=0x00 PREC=0x00 TTL=63 ID=22142 DF PROTO=TCP SPT=80 DPT=10656 WINDOW=227 RES=0x00 ACK URGP=0 23 Jan 21 18:09:52 centos7 kernel: [debug]-raw-PREROUTING:IN=docker0 OUT= PHYSIN=veth57b521b MAC=02:42:96:cd:15:f7:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=1.2.3.4 LEN=290 TOS=0x00 PREC=0x00 TTL=64 ID=22143 DF PROTO=TCP SPT=80 DPT=10656 WINDOW=227 RES=0x00 ACK PSH URGP=0 24 Jan 21 18:09:52 centos7 kernel: [debug]-mangle-PREROUTING:IN=docker0 OUT= PHYSIN=veth57b521b MAC=02:42:96:cd:15:f7:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=1.2.3.4 LEN=290 TOS=0x00 PREC=0x00 TTL=64 ID=22143 DF PROTO=TCP SPT=80 DPT=10656 WINDOW=227 RES=0x00 ACK PSH URGP=0 25 Jan 21 18:09:52 centos7 kernel: [debug]-mangle-FORWARD:IN=docker0 OUT=eth0 PHYSIN=veth57b521b MAC=02:42:96:cd:15:f7:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=1.2.3.4 LEN=290 TOS=0x00 PREC=0x00 TTL=63 ID=22143 DF PROTO=TCP SPT=80 DPT=10656 WINDOW=227 RES=0x00 ACK PSH URGP=0 26 Jan 21 18:09:52 centos7 kernel: [debug]-mangle-POSTROUTING:IN= OUT=eth0 PHYSIN=veth57b521b SRC=172.17.0.2 DST=1.2.3.4 LEN=290 TOS=0x00 PREC=0x00 TTL=63 ID=22143 DF PROTO=TCP SPT=80 DPT=10656 WINDOW=227 RES=0x00 ACK PSH URGP=0 27 Jan 21 18:09:52 centos7 kernel: [debug]-raw-PREROUTING:IN=docker0 OUT= PHYSIN=veth57b521b MAC=02:42:96:cd:15:f7:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=1.2.3.4 LEN=664 TOS=0x00 PREC=0x00 TTL=64 ID=22144 DF PROTO=TCP SPT=80 DPT=10656 WINDOW=227 RES=0x00 ACK PSH URGP=0 28 Jan 21 18:09:52 centos7 kernel: [debug]-mangle-PREROUTING:IN=docker0 OUT= PHYSIN=veth57b521b MAC=02:42:96:cd:15:f7:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=1.2.3.4 LEN=664 TOS=0x00 PREC=0x00 TTL=64 ID=22144 DF PROTO=TCP SPT=80 DPT=10656 WINDOW=227 RES=0x00 ACK PSH URGP=0 29 Jan 21 18:09:52 centos7 kernel: [debug]-mangle-FORWARD:IN=docker0 OUT=eth0 PHYSIN=veth57b521b MAC=02:42:96:cd:15:f7:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=1.2.3.4 LEN=664 TOS=0x00 PREC=0x00 TTL=63 ID=22144 DF PROTO=TCP SPT=80 DPT=10656 WINDOW=227 RES=0x00 ACK PSH URGP=0 30 Jan 21 18:09:52 centos7 kernel: [debug]-mangle-POSTROUTING:IN= OUT=eth0 PHYSIN=veth57b521b SRC=172.17.0.2 DST=1.2.3.4 LEN=664 TOS=0x00 PREC=0x00 TTL=63 ID=22144 DF PROTO=TCP SPT=80 DPT=10656 WINDOW=227 RES=0x00 ACK PSH URGP=0 31 Jan 21 18:09:52 centos7 kernel: [debug]-raw-PREROUTING:IN=eth0 OUT= MAC=00:16:3e:01:ae:15:ee:ff:ff:ff:ff:ff:08:00 SRC=1.2.3.4 DST=172.20.242.183 LEN=52 TOS=0x14 PREC=0x00 TTL=48 ID=0 DF PROTO=TCP SPT=10656 DPT=8080 WINDOW=2047 RES=0x00 ACK URGP=0 32 Jan 21 18:09:52 centos7 kernel: [debug]-mangle-PREROUTING:IN=eth0 OUT= MAC=00:16:3e:01:ae:15:ee:ff:ff:ff:ff:ff:08:00 SRC=1.2.3.4 DST=172.20.242.183 LEN=52 TOS=0x14 PREC=0x00 TTL=48 ID=0 DF PROTO=TCP SPT=10656 DPT=8080 WINDOW=2047 RES=0x00 ACK URGP=0 33 Jan 21 18:09:52 centos7 kernel: [debug]-mangle-FORWARD:IN=eth0 OUT=docker0 MAC=00:16:3e:01:ae:15:ee:ff:ff:ff:ff:ff:08:00 SRC=1.2.3.4 DST=172.17.0.2 LEN=52 TOS=0x14 PREC=0x00 TTL=47 ID=0 DF PROTO=TCP SPT=10656 DPT=80 WINDOW=2047 RES=0x00 ACK URGP=0 34 Jan 21 18:09:52 centos7 kernel: [debug]-mangle-POSTROUTING:IN= OUT=docker0 SRC=1.2.3.4 DST=172.17.0.2 LEN=52 TOS=0x14 PREC=0x00 TTL=47 ID=0 DF PROTO=TCP SPT=10656 DPT=80 WINDOW=2047 RES=0x00 ACK URGP=0 35 Jan 21 18:09:52 centos7 kernel: [debug]-raw-PREROUTING:IN=eth0 OUT= MAC=00:16:3e:01:ae:15:ee:ff:ff:ff:ff:ff:08:00 SRC=1.2.3.4 DST=172.20.242.183 LEN=52 TOS=0x14 PREC=0x00 TTL=48 ID=0 DF PROTO=TCP SPT=10656 DPT=8080 WINDOW=2048 RES=0x00 ACK FIN URGP=0 36 Jan 21 18:09:52 centos7 kernel: [debug]-mangle-PREROUTING:IN=eth0 OUT= MAC=00:16:3e:01:ae:15:ee:ff:ff:ff:ff:ff:08:00 SRC=1.2.3.4 DST=172.20.242.183 LEN=52 TOS=0x14 PREC=0x00 TTL=48 ID=0 DF PROTO=TCP SPT=10656 DPT=8080 WINDOW=2048 RES=0x00 ACK FIN URGP=0 37 Jan 21 18:09:52 centos7 kernel: [debug]-mangle-FORWARD:IN=eth0 OUT=docker0 MAC=00:16:3e:01:ae:15:ee:ff:ff:ff:ff:ff:08:00 SRC=1.2.3.4 DST=172.17.0.2 LEN=52 TOS=0x14 PREC=0x00 TTL=47 ID=0 DF PROTO=TCP SPT=10656 DPT=80 WINDOW=2048 RES=0x00 ACK FIN URGP=0 38 Jan 21 18:09:52 centos7 kernel: [debug]-mangle-POSTROUTING:IN= OUT=docker0 SRC=1.2.3.4 DST=172.17.0.2 LEN=52 TOS=0x14 PREC=0x00 TTL=47 ID=0 DF PROTO=TCP SPT=10656 DPT=80 WINDOW=2048 RES=0x00 ACK FIN URGP=0 39 Jan 21 18:09:52 centos7 kernel: [debug]-raw-PREROUTING:IN=docker0 OUT= PHYSIN=veth57b521b MAC=02:42:96:cd:15:f7:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=1.2.3.4 LEN=52 TOS=0x00 PREC=0x00 TTL=64 ID=22145 DF PROTO=TCP SPT=80 DPT=10656 WINDOW=227 RES=0x00 ACK FIN URGP=0 40 Jan 21 18:09:52 centos7 kernel: [debug]-mangle-PREROUTING:IN=docker0 OUT= PHYSIN=veth57b521b MAC=02:42:96:cd:15:f7:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=1.2.3.4 LEN=52 TOS=0x00 PREC=0x00 TTL=64 ID=22145 DF PROTO=TCP SPT=80 DPT=10656 WINDOW=227 RES=0x00 ACK FIN URGP=0 41 Jan 21 18:09:52 centos7 kernel: [debug]-mangle-FORWARD:IN=docker0 OUT=eth0 PHYSIN=veth57b521b MAC=02:42:96:cd:15:f7:02:42:ac:11:00:02:08:00 SRC=172.17.0.2 DST=1.2.3.4 LEN=52 TOS=0x00 PREC=0x00 TTL=63 ID=22145 DF PROTO=TCP SPT=80 DPT=10656 WINDOW=227 RES=0x00 ACK FIN URGP=0 42 Jan 21 18:09:52 centos7 kernel: [debug]-mangle-POSTROUTING:IN= OUT=eth0 PHYSIN=veth57b521b SRC=172.17.0.2 DST=1.2.3.4 LEN=52 TOS=0x00 PREC=0x00 TTL=63 ID=22145 DF PROTO=TCP SPT=80 DPT=10656 WINDOW=227 RES=0x00 ACK FIN URGP=0 43 Jan 21 18:09:52 centos7 kernel: [debug]-raw-PREROUTING:IN=eth0 OUT= MAC=00:16:3e:01:ae:15:ee:ff:ff:ff:ff:ff:08:00 SRC=1.2.3.4 DST=172.20.242.183 LEN=52 TOS=0x14 PREC=0x00 TTL=48 ID=0 DF PROTO=TCP SPT=10656 DPT=8080 WINDOW=2048 RES=0x00 ACK URGP=0 44 Jan 21 18:09:52 centos7 kernel: [debug]-mangle-PREROUTING:IN=eth0 OUT= MAC=00:16:3e:01:ae:15:ee:ff:ff:ff:ff:ff:08:00 SRC=1.2.3.4 DST=172.20.242.183 LEN=52 TOS=0x14 PREC=0x00 TTL=48 ID=0 DF PROTO=TCP SPT=10656 DPT=8080 WINDOW=2048 RES=0x00 ACK URGP=0 45 Jan 21 18:09:52 centos7 kernel: [debug]-mangle-FORWARD:IN=eth0 OUT=docker0 MAC=00:16:3e:01:ae:15:ee:ff:ff:ff:ff:ff:08:00 SRC=1.2.3.4 DST=172.17.0.2 LEN=52 TOS=0x14 PREC=0x00 TTL=47 ID=0 DF PROTO=TCP SPT=10656 DPT=80 WINDOW=2048 RES=0x00 ACK URGP=0 46 Jan 21 18:09:52 centos7 kernel: [debug]-mangle-POSTROUTING:IN= OUT=docker0 SRC=1.2.3.4 DST=172.17.0.2 LEN=52 TOS=0x14 PREC=0x00 TTL=47 ID=0 DF PROTO=TCP SPT=10656 DPT=80 WINDOW=2048 RES=0x00 ACK URGP=0
以上涉及raw,mangle和nat几个iptables表,选取第1行到第22行的log,数据包的基本轨迹可以归纳为如下
- [1] raw-PREROUTING: IN=eth0 (1.2.3.4:10656 -> 172.20.242.183:8080)
- [2] mangle-PREROUTING: IN=eth0 (1.2.3.4:10656 -> 172.20.242.183:8080)
- [3] nat-PREROUTING: IN=eth0 (1.2.3.4:10656 -> 172.20.242.183:8080)
- [4] mangle-FORWARD: IN=eth0 OUT=docker0 (1.2.3.4:10656 -> 172.17.0.2:80)
- [5] mangle-POSTROUTING: OUT=docker0 (1.2.3.4:10656 -> 172.17.0.2:80)
- [6] nat-POSTROUTING: OUT=docker0 (1.2.3.4:10656 -> 172.17.0.2:80)
- [7] raw-PREROUTING: IN=docker0 (172.17.0.2:80 -> 1.2.3.4:10656)
- [8] mangle-PREROUTING: IN=docker0 (172.17.0.2:80 -> 1.2.3.4:10656)
- [9] mangle-FORWARD: IN=docker0 OUT=eth0 (172.17.0.2:80 -> 1.2.3.4:10656)
- [10] mangle-POSTROUTING: OUT=eth0 (172.17.0.2:80 -> 1.2.3.4:10656)
- [11] raw-PREROUTING: IN=eth0 (1.2.3.4:10656 -> 172.20.242.183:8080)
- [12] mangle-PREROUTING: IN=eth0 (1.2.3.4:10656 -> 172.20.242.183:8080)
- [13] mangle-FORWARD: IN=eth0 OUT=docker0 (1.2.3.4:10656 -> 172.17.0.2:80)
- [14] mangle-POSTROUTING: OUT=docker0 (1.2.3.4:10656 -> 172.17.0.2:80)
- [15] raw-PREROUTING: IN=eth0 (1.2.3.4:10656 -> 172.20.242.183:8080)
- [16] mangle-PREROUTING: IN=eth0 (1.2.3.4:10656 -> 172.20.242.183:8080)
- [17] mangle-FORWARD: IN=eth0 OUT=docker0 (1.2.3.4:10656 -> 172.17.0.2:80)
- [18] mangle-POSTROUTING: OUT=docker0 (1.2.3.4:10656 -> 172.17.0.2:80)
- [19] raw-PREROUTING: IN=docker0 (172.17.0.2:80 -> 1.2.3.4:10656)
- [20] mangle-PREROUTING: IN=docker0 (172.17.0.2:80 -> 1.2.3.4:10656)
- [21] mangle-FORWARD: IN=docker0 OUT=eth0 (172.17.0.2:80 -> 1.2.3.4:10656)
- [22] mangle-POSTROUTING: OUT=eth0 (172.17.0.2:80 -> 1.2.3.4:10656)
以上分成5个色块,分别为如下表示:
- 粉色:1.2.3.4发送SYN包
- 橘色:172.17.0.2发送SYN + ACK包
- 黄色:1.2.3.4发送ACK包
- 绿色:1.2.3.4发送ACK + PSH包
- 蓝色:172.17.0.2发送ACK包
从以上的log可以看到,从nat表的PREROUTING链开始后,目的端口就从8080变成了80,那这个会不会就是其端口转发的根源呢?我们接下来再分析下。
二. NAT表的魔法
1. 初始的Docker的NAT表
上面我们说到,数据包从NAT表出来后,目的端口发生了变化。我们将iptables回退到增加log之前的状态,看看这几个iptables的表都有什么变化。
与安装docker前相比,其他几个表都没有什么相关的变化,唯独nat表,看上去有些不一样:
[root@centos7 ~]# iptables -t nat -nvL Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination 60 3132 DOCKER all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL Chain INPUT (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination 0 0 DOCKER all -- * * 0.0.0.0/0 !127.0.0.0/8 ADDRTYPE match dst-type LOCAL Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination 0 0 MASQUERADE all -- * !docker0 172.17.0.0/16 0.0.0.0/0 Chain DOCKER (2 references) pkts bytes target prot opt in out source destination 0 0 RETURN all -- docker0 * 0.0.0.0/0 0.0.0.0/0
我们可以看到,它在nat表中增加了一个自定义链DOCKER,而这个链被两处引用,一处是在PREROUTING,另一处在OUTPUT,为什么在这两处呢?
为了回答这个问题,我们有必要先谈谈iptables的基本知识。
2. iptables的处理端口转发的过程
简单地讲,Linux iptables一共有5条内置链,分别为PREROUTING,FORWARDING,POSTROUTING,INPUT和OUTPUT。
此外,iptables还有5个表,分别为filter,nat,mangle,raw和security。
数据包经过这些链是有顺序的,经过某个链时,如果链上有对应的表在作用,则去执行该表中对应的链里的rule,大致如下所示:
即数据进入网卡接口后,到达协议栈,首先经过的iptables链是PREROUTING,这里给数据包一些预处理的机会,譬如修改目的地址等,把所有的表都撸一遍,如果看到某个表有PREROUTING链的规则,则进去执行,如果没有就跳过,多说一句,不是每个表都有PREROUTING链,只有raw、mangle和nat表才有的,以下链也是的,后续也就不一一而足了。
数据包经过PREROUTING链的处理后,然后交给Routing决策,结合路由表看看该数据包是继续在协议栈中往下走进入本地进程,还是直接把本网卡所在的机器当路由,直接通过FOWARD出该网卡去往其他网卡,甚至其他机器。
到达了INPUT就表示肯定是去往本地的某个进程了,不过再给一次处理的机会再进入进程,于是乎,再查看下具有INPUT链的几张表(即mangle、nat和filter),看看其中有没有能够匹配的规则,有就执行,对数据包作送入进程socket前的最后一次处理。
一旦到达了进程socket,进入进程用户空间,这个数据包的生命就该到达终点了。
本来到了这里,故事也就结束了,然而实际使用的协议都是有来有回的,回包的起点就是本地进程了,因而出现了上图中的右边部分,即当本地进程发起一个数据包(这里可能是一个回包)时,首先就要交给OUTPUT链,在该链上先给个机会对进程发出的数据包做个处理,然后再经过路由决策决定发往哪张网卡。
经由上面的分析,Docker对于入向包在PREROUTING链中处理,而出向包在OUTPUT链中处理,也就顺理成章了吧。也就是说,要赶在让路由策略决定往哪里发前,先处理下,这样保证如向包能够顺利进入相应的进程,而出向包能够达到相应的网卡接口。
我们先来解读下以上的nat表rule的作用:
- 在PREROUTING中,对于所有源地址、目的地址、网络协议的包进行地址匹配,如果匹配本地地址类的,就进入自定义的DOCKER链,注意,本地地址类可以不局限于127.0.0.0/8,而是包括所有本地监听的、分配的地址,例如docker容器中常用的172开头的那些地址,具体可以通过 ip route show table local type local 看到
- 在OUTPUT链中,对所有源地址、网络协议、目的地址不是127.0.0.0/8的包进行匹配,如果属于本地地址类的,进入自定义的DOCKER链
- 在自定义的DOCKER链中,对于docker0网桥进入的包(即容器中发出的),啥也不做,直接返回到刚刚跳转到DOCKER链的上级链
- 在MASQUERADE链中对于所有源自172.17.0.0/16、但不是由docker0发出的包(由容器发往本机之外的)都进行地址伪装,即将源地址替换为本地地址,也就是做SNAT
以上就是docker安装好的nat表,启动了以上nginx的端口转发后,
3. 启动端口转发后NAT表的变化
既然我们谈的是Docker的端口转发,那我们真的来一个转发看看,瞧瞧iptables中有什么变化:
docker run -d -p 8080:80 nginx:latest
看iptables变化:
[root@centos7 ~]# iptables -t nat -nvL Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination 60 3132 DOCKER all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL Chain INPUT (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination 0 0 DOCKER all -- * * 0.0.0.0/0 !127.0.0.0/8 ADDRTYPE match dst-type LOCAL Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes) pkts bytes target prot opt in out source destination 0 0 MASQUERADE all -- * !docker0 172.17.0.0/16 0.0.0.0/0 0 0 MASQUERADE tcp -- * * 172.17.0.2 172.17.0.2 tcp dpt:80 Chain DOCKER (2 references) pkts bytes target prot opt in out source destination 0 0 RETURN all -- docker0 * 0.0.0.0/0 0.0.0.0/0 0 0 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 to:172.17.0.2:80
好家伙,偷偷在POSTROUTING和DOCKER中增加了两条rule:
- 在自定义的DOCKER链中,对于不是由docker0接受到的tcp包,进行目的地址转换,转换的规则为: 如果目的端口是8080的tcp包,则修改为172.17.0.2:80(172.17.0.2为刚生成的container的地址)
- 在MASQUERADE链中,增加了了一条地址伪装规则,对于所有的来自172.17.0.2和发往172.17.0.2且目的端口为80的TCP包,都进行源地址伪装(SNAT),改为该网络接口对外地址,如果是eth0,那我们这里就是172.20.242.183
4. 数据包的基本流向
看到这里,我们或许已经大体上有了一个包的基本流向的概念了,结合已经的iptables的log,我们来分析下具体的流向。
首先,查看了本地的路由:
[root@centos7 ~]# ip route default via 172.20.255.253 dev eth0 169.254.0.0/16 dev eth0 scope link metric 1002 172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 172.20.240.0/20 dev eth0 proto kernel scope link src 172.20.242.183
我们从这台机器去inspect这个nginx container,拿到部分IP信息如下:
"Networks": { "bridge": { "IPAMConfig": null, "Links": null, "Aliases": null, "NetworkID": "a2d0aec96e15c5cb8ead04d0d95fd00d71b72b17a8214ce78b4af9a4c71b6248", "EndpointID": "c1faabc8c50e3a703219900e029ebf813cfcd619030865c2d8991ac966f56b95", "Gateway": "172.17.0.1", "IPAddress": "172.17.0.2", "IPPrefixLen": 16, "IPv6Gateway": "", "GlobalIPv6Address": "", "GlobalIPv6PrefixLen": 0, "MacAddress": "02:42:ac:11:00:02" } }
并查看本地的所有的网卡接口的地址:
[root@centos7 ~]# ip a 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 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 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000 link/ether 00:16:3e:01:ae:15 brd ff:ff:ff:ff:ff:ff inet 172.20.242.183/20 brd 172.20.255.255 scope global dynamic eth0 valid_lft 315273420sec preferred_lft 315273420sec inet6 fe80::216:3eff:fe01:ae15/64 scope link valid_lft forever preferred_lft forever 3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default link/ether 02:42:c0:8e:53:0e brd ff:ff:ff:ff:ff:ff inet 172.17.0.1/16 scope global docker0 valid_lft forever preferred_lft forever inet6 fe80::42:c0ff:fe8e:530e/64 scope link valid_lft forever preferred_lft forever 11: veth02d2a0d@if10: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default link/ether da:75:64:82:95:10 brd ff:ff:ff:ff:ff:ff link-netnsid 0 inet6 fe80::d875:64ff:fe82:9510/64 scope link valid_lft forever preferred_lft forever
那么从外网1.2.3.4发出的第一个SYN包(log总结中粉色部分)的走向是这样的:
- TCP包(1.2.3.4:10656 -> 172.20.242.183:8080)由eth0接口进入TCP协议栈
- 进入PREROUTING链,分别过滤raw、mangle及nat表,发现只有nat表有rule存在,则进入逐条过滤
- 发现其目的地址属于本地地址类,跳转到自定义DOCKER链
- 在DOCKER链中发现第一条规则不匹配,因为其不是docker0接收到的包;第二条匹配,非docker0接收,且目的端口为8080,则通过DNAT转换为(1.2.3.4:10656 -> 172.17.0.2:80)
- PREROUTING链中过滤完成后,进入路由决策,路由决策根据路由 172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 决定该包发往docker0,如果没有源地址则填充源地址为172.17.0.1(即本机发起的包,我们这里显然不是),则该包维持(1.2.3.4:10656 -> 172.17.0.2:80),发往docker0,走向FORWARRD链,相当于转发到另外一个网卡接口了,这里得益于打开了上面提及的ip_forward内核参数,eth0和docker0之间可以互相转发包
- 于是经过FORWARD链,数据包由eth0直接转发到docker0,数据包维持(1.2.3.4:10656 -> 172.17.0.2:80)
- (1.2.3.4:10656 -> 172.17.0.2:80)到达POSTROUTING链,经过nat表过滤,因为它的源地址不属于172.17.0.0/16,因而不匹配第一条rule;到第二条rule,源地址目的地址都不是172.17.0.2,因而也不匹配,从docker0出协议栈,由于docker0是Linux网桥,该网桥与容器内的"eth0"(容器自己看,它自己有个eth0,实际是veth pair的一端)通过一对veth pair直连,这样一来,容器内的nginx进程的socket就收到该SYN包了
那从容器里返回的SYN+ACK包该怎么办呢?结合上面的log总结中的橘色部分,我们可以作如下推演:
- TCP包(172.17.0.2:80 -> 1.2.3.4:10656)从容器内的nginx进程的socket通过docker0接口发出返回包SYN+ACK,进入TCP协议栈
- 首先到达PREROUTING链,这里比较奇怪,只经过了raw和mangle表的过滤,却没有经过nat,为什么呢?因为iptables对于数据包也是记录状态的,如果前面有了一个ACK了,那么从iptables看来,同样四元组的SYN+ACK包就是ESTABLISHED状态了,因此不需要经过nat表去浪费时间了,需要怎么转换iptables已经知道了,直接做掉就好了,于是进入路由决策
- 在路由决策阶段,由于是外部地址,匹配 default via 172.20.255.253 dev eth0路由规则,通过eth0发送,当前是在docker0,所以需要从FORWARD链出去
- TCP包(172.17.0.2:80 -> 1.2.3.4:10656)包到达POSTROUTING链,这时需要在mangle表处理下,因为iptables的记忆功能,它知道这个四元组曾经从eth0来的时候是从172.20.242.183:8080转换来的,这里回去的话还要转换为本地地址和端口即数据包变成(172.20.242.183:8080 -> 1.2.3.4:10656)发出,至此,一个回包就好了。
接下来的部分,就显得雷同了,大差不差,只是少了进入nat表了,原因之前说了,iptables是有状态的。那我们就不在这里赘述了。
参考:
[1] https://www.cnblogs.com/yum777/articles/8514636.html
[2] https://tonybai.com/2016/01/15/understanding-container-networking-on-single-host/
[3] https://www.rigacci.org/wiki/lib/exe/fetch.php/doc/appunti/linux/sa/iptables/conntrack.html