從iptables看容器端口轉發

不論是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的作用:

  1. 在PREROUTING中,對於所有源地址、目的地址、網絡協議的包進行地址匹配,如果匹配本地地址類的,就進入自定義的DOCKER鏈,注意,本地地址類可以不侷限於127.0.0.0/8,而是包括所有本地監聽的、分配的地址,例如docker容器中常用的172開頭的那些地址,具體可以通過 ip route show table local type local 看到
  2. 在OUTPUT鏈中,對所有源地址、網絡協議、目的地址不是127.0.0.0/8的包進行匹配,如果屬於本地地址類的,進入自定義的DOCKER鏈
  3. 在自定義的DOCKER鏈中,對於docker0網橋進入的包(即容器中發出的),啥也不做,直接返回到剛剛跳轉到DOCKER鏈的上級鏈
  4. 在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:

  1. 在自定義的DOCKER鏈中,對於不是由docker0接受到的tcp包,進行目的地址轉換,轉換的規則爲: 如果目的端口是8080的tcp包,則修改爲172.17.0.2:80(172.17.0.2爲剛生成的container的地址)
  2. 在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總結中粉色部分)的走向是這樣的:

  1. TCP包(1.2.3.4:10656 -> 172.20.242.183:8080)由eth0接口進入TCP協議棧
  2. 進入PREROUTING鏈,分別過濾raw、mangle及nat表,發現只有nat表有rule存在,則進入逐條過濾
  3. 發現其目的地址屬於本地地址類,跳轉到自定義DOCKER鏈
  4. 在DOCKER鏈中發現第一條規則不匹配,因爲其不是docker0接收到的包;第二條匹配,非docker0接收,且目的端口爲8080,則通過DNAT轉換爲(1.2.3.4:10656 -> 172.17.0.2:80)
  5. 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之間可以互相轉發包
  6. 於是經過FORWARD鏈,數據包由eth0直接轉發到docker0,數據包維持(1.2.3.4:10656 -> 172.17.0.2:80)
  7. (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總結中的橘色部分,我們可以作如下推演:

  1. TCP包(172.17.0.2:80 -> 1.2.3.4:10656)從容器內的nginx進程的socket通過docker0接口發出返回包SYN+ACK,進入TCP協議棧
  2. 首先到達PREROUTING鏈,這裏比較奇怪,只經過了raw和mangle表的過濾,卻沒有經過nat,爲什麼呢?因爲iptables對於數據包也是記錄狀態的,如果前面有了一個ACK了,那麼從iptables看來,同樣四元組的SYN+ACK包就是ESTABLISHED狀態了,因此不需要經過nat表去浪費時間了,需要怎麼轉換iptables已經知道了,直接做掉就好了,於是進入路由決策
  3. 在路由決策階段,由於是外部地址,匹配 default via 172.20.255.253 dev eth0路由規則,通過eth0發送,當前是在docker0,所以需要從FORWARD鏈出去
  4. 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

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章