在前文《Linux 路由三大件》中,我们提到了 iptables 可以修改数据包的特征从而影响其路由。这个功能无论是传统场景下的 防火墙,还是云原生场景下的 服务路由(k8s service)、网络策略(calico network policy) 等都有依赖。
虽然业界在积极地推进 ipvs、ebpf 等更新、更高效的技术落地,但是出于各种各样的原因(技术、成本、管理等),iptables 仍然有着非常广泛的使用 ,所以我们有必要了解 iptables 的方方面面。今天我们就来了解其中的重要一环:调试和追踪 iptables。更具体一点:一个数据包是怎么在 iptables 的各个 chain/table/rule 中游走的。
注:不了解 iptables 的同学可以先看看这篇 wiki https://wiki.archlinux.org/title/Iptables
trace
我们知道,iptables 的 rule 最后是一个 target,这个 target 可以设置为 TRACE。这样,匹配这个 rule 的包经过的所有 chain/table/rule 都会被记录下来。
这个 rule 本身只能被放在 raw
表里,可以存在于 PREROUTING
和 OUTPUT
两个 chain 中。其匹配规则和其他一般的 rule 没有什么差别。
实践
我们采用一个 docker 场景来进行实践,如下图:
亦即宿主机的 10000 端口映射给了容器的 80 端口。图中有两种测试场景,分别是:
- 本节点访问容器 IP
- 跨节点访问宿主机 IP
为了让分析更直观/简单,我们用 telnet 测试即可(curl 会产生很多后续 http 包,不够简洁)。
OK,开始我们的测试!
首先在宿主机上开启 iptables log
# check first
# cat /proc/net/netfilter/nf_log
modprobe nf_log_ipv4
sysctl net.netfilter.nf_log.2=nf_log_ipv4
然后增加 trace 配置
# 访问容器 IP 使用
iptables -t raw -A OUTPUT -p tcp --destination 172.19.0.0/16 --dport 80 -j TRACE
# 访问节点 IP 使用
iptables -t raw -A PREROUTING -p tcp --destination 192.168.64.4 --dport 10000 -j TRACE
查看结果可以用这个命令:cat /var/log/messages | grep "TRACE:"
本节点访问:telnet 172.19.0.9 80
,结果:
fedora kernel: [ 2350.411619] TRACE: raw:OUTPUT:policy:2 IN= OUT=br-00ea7870520a SRC=172.19.0.1 DST=172.19.0.9 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=11432 DF PROTO=TCP SPT=43256 DPT=80 SEQ=2316959420 ACK=0 WINDOW=64240 RES=0x00 SYN URGP=0 OPT (020405B40402080AAF8EE4320000000001030307) UID=0 GID=0
fedora kernel: [ 2350.411634] TRACE: nat:OUTPUT:policy:2 IN= OUT=br-00ea7870520a SRC=172.19.0.1 DST=172.19.0.9 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=11432 DF PROTO=TCP SPT=43256 DPT=80 SEQ=2316959420 ACK=0 WINDOW=64240 RES=0x00 SYN URGP=0 OPT (020405B40402080AAF8EE4320000000001030307) UID=0 GID=0
fedora kernel: [ 2350.411639] TRACE: filter:OUTPUT:policy:1 IN= OUT=br-00ea7870520a SRC=172.19.0.1 DST=172.19.0.9 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=11432 DF PROTO=TCP SPT=43256 DPT=80 SEQ=2316959420 ACK=0 WINDOW=64240 RES=0x00 SYN URGP=0 OPT (020405B40402080AAF8EE4320000000001030307) UID=0 GID=0
fedora kernel: [ 2350.411644] TRACE: nat:POSTROUTING:policy:4 IN= OUT=br-00ea7870520a SRC=172.19.0.1 DST=172.19.0.9 LEN=60 TOS=0x10 PREC=0x00 TTL=64 ID=11432 DF PROTO=TCP SPT=43256 DPT=80 SEQ=2316959420 ACK=0 WINDOW=64240 RES=0x00 SYN URGP=0 OPT (020405B40402080AAF8EE4320000000001030307) UID=0 GID=0
fedora kernel: [ 2350.411778] TRACE: raw:OUTPUT:policy:2 IN= OUT=br-00ea7870520a SRC=172.19.0.1 DST=172.19.0.9 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=11433 DF PROTO=TCP SPT=43256 DPT=80 SEQ=2316959421 ACK=357062108 WINDOW=502 RES=0x00 ACK URGP=0 OPT (0101080AAF8EE4320C2ACEC2) UID=0 GID=0
fedora kernel: [ 2350.411784] TRACE: filter:OUTPUT:policy:1 IN= OUT=br-00ea7870520a SRC=172.19.0.1 DST=172.19.0.9 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=11433 DF PROTO=TCP SPT=43256 DPT=80 SEQ=2316959421 ACK=357062108 WINDOW=502 RES=0x00 ACK URGP=0 OPT (0101080AAF8EE4320C2ACEC2) UID=0 GID=0
跨节点访问:telnet 192.168.64.4 10000
,结果:
fedora kernel: [ 2631.978281] TRACE: raw:PREROUTING:policy:2 IN=enp0s1 OUT= MAC=fa:f3:dc:b4:4e:ef:3a:f9:d3:9e:aa:64:08:00 SRC=192.168.64.1 DST=192.168.64.4 LEN=64 TOS=0x10 PREC=0x00 TTL=64 ID=0 PROTO=TCP SPT=52842 DPT=10000 SEQ=1184533180 ACK=0 WINDOW=65535 RES=0x00 CWR ECE SYN URGP=0 OPT (020405B4010303060101080A6EF8750A0000000004020000)
fedora kernel: [ 2631.978585] TRACE: nat:PREROUTING:rule:1 IN=enp0s1 OUT= MAC=fa:f3:dc:b4:4e:ef:3a:f9:d3:9e:aa:64:08:00 SRC=192.168.64.1 DST=192.168.64.4 LEN=64 TOS=0x10 PREC=0x00 TTL=64 ID=0 PROTO=TCP SPT=52842 DPT=10000 SEQ=1184533180 ACK=0 WINDOW=65535 RES=0x00 CWR ECE SYN URGP=0 OPT (020405B4010303060101080A6EF8750A0000000004020000)
fedora kernel: [ 2631.979055] TRACE: nat:DOCKER:rule:3 IN=enp0s1 OUT= MAC=fa:f3:dc:b4:4e:ef:3a:f9:d3:9e:aa:64:08:00 SRC=192.168.64.1 DST=192.168.64.4 LEN=64 TOS=0x10 PREC=0x00 TTL=64 ID=0 PROTO=TCP SPT=52842 DPT=10000 SEQ=1184533180 ACK=0 WINDOW=65535 RES=0x00 CWR ECE SYN URGP=0 OPT (020405B4010303060101080A6EF8750A0000000004020000)
fedora kernel: [ 2631.979077] TRACE: filter:FORWARD:rule:1 IN=enp0s1 OUT=br-00ea7870520a MAC=fa:f3:dc:b4:4e:ef:3a:f9:d3:9e:aa:64:08:00 SRC=192.168.64.1 DST=172.19.0.9 LEN=64 TOS=0x10 PREC=0x00 TTL=63 ID=0 PROTO=TCP SPT=52842 DPT=80 SEQ=1184533180 ACK=0 WINDOW=65535 RES=0x00 CWR ECE SYN URGP=0 OPT (020405B4010303060101080A6EF8750A0000000004020000)
fedora kernel: [ 2631.979585] TRACE: filter:DOCKER-USER:return:1 IN=enp0s1 OUT=br-00ea7870520a MAC=fa:f3:dc:b4:4e:ef:3a:f9:d3:9e:aa:64:08:00 SRC=192.168.64.1 DST=172.19.0.9 LEN=64 TOS=0x10 PREC=0x00 TTL=63 ID=0 PROTO=TCP SPT=52842 DPT=80 SEQ=1184533180 ACK=0 WINDOW=65535 RES=0x00 CWR ECE SYN URGP=0 OPT (020405B4010303060101080A6EF8750A0000000004020000)
fedora kernel: [ 2631.979585] TRACE: filter:FORWARD:rule:2 IN=enp0s1 OUT=br-00ea7870520a MAC=fa:f3:dc:b4:4e:ef:3a:f9:d3:9e:aa:64:08:00 SRC=192.168.64.1 DST=172.19.0.9 LEN=64 TOS=0x10 PREC=0x00 TTL=63 ID=0 PROTO=TCP SPT=52842 DPT=80 SEQ=1184533180 ACK=0 WINDOW=65535 RES=0x00 CWR ECE SYN URGP=0 OPT (020405B4010303060101080A6EF8750A0000000004020000)
fedora kernel: [ 2631.979585] TRACE: filter:DOCKER-ISOLATION-STAGE-1:return:3 IN=enp0s1 OUT=br-00ea7870520a MAC=fa:f3:dc:b4:4e:ef:3a:f9:d3:9e:aa:64:08:00 SRC=192.168.64.1 DST=172.19.0.9 LEN=64 TOS=0x10 PREC=0x00 TTL=63 ID=0 PROTO=TCP SPT=52842 DPT=80 SEQ=1184533180 ACK=0 WINDOW=65535 RES=0x00 CWR ECE SYN URGP=0 OPT (020405B4010303060101080A6EF8750A0000000004020000)
fedora kernel: [ 2631.980070] TRACE: filter:FORWARD:rule:8 IN=enp0s1 OUT=br-00ea7870520a MAC=fa:f3:dc:b4:4e:ef:3a:f9:d3:9e:aa:64:08:00 SRC=192.168.64.1 DST=172.19.0.9 LEN=64 TOS=0x10 PREC=0x00 TTL=63 ID=0 PROTO=TCP SPT=52842 DPT=80 SEQ=1184533180 ACK=0 WINDOW=65535 RES=0x00 CWR ECE SYN URGP=0 OPT (020405B4010303060101080A6EF8750A0000000004020000)
fedora kernel: [ 2631.980070] TRACE: filter:DOCKER:rule:1 IN=enp0s1 OUT=br-00ea7870520a MAC=fa:f3:dc:b4:4e:ef:3a:f9:d3:9e:aa:64:08:00 SRC=192.168.64.1 DST=172.19.0.9 LEN=64 TOS=0x10 PREC=0x00 TTL=63 ID=0 PROTO=TCP SPT=52842 DPT=80 SEQ=1184533180 ACK=0 WINDOW=65535 RES=0x00 CWR ECE SYN URGP=0 OPT (020405B4010303060101080A6EF8750A0000000004020000)
fedora kernel: [ 2631.980070] TRACE: nat:POSTROUTING:policy:4 IN=enp0s1 OUT=br-00ea7870520a MAC=fa:f3:dc:b4:4e:ef:3a:f9:d3:9e:aa:64:08:00 SRC=192.168.64.1 DST=172.19.0.9 LEN=64 TOS=0x10 PREC=0x00 TTL=63 ID=0 PROTO=TCP SPT=52842 DPT=80 SEQ=1184533180 ACK=0 WINDOW=65535 RES=0x00 CWR ECE SYN URGP=0 OPT (020405B4010303060101080A6EF8750A0000000004020000)
fedora kernel: [ 2631.981288] TRACE: raw:PREROUTING:policy:2 IN=enp0s1 OUT= MAC=fa:f3:dc:b4:4e:ef:3a:f9:d3:9e:aa:64:08:00 SRC=192.168.64.1 DST=192.168.64.4 LEN=52 TOS=0x10 PREC=0x00 TTL=64 ID=0 PROTO=TCP SPT=52842 DPT=10000 SEQ=1184533181 ACK=3245308350 WINDOW=2058 RES=0x00 ACK URGP=0 OPT (0101080A6EF8750D8E0CA28F)
fedora kernel: [ 2631.981299] TRACE: filter:FORWARD:rule:1 IN=enp0s1 OUT=br-00ea7870520a MAC=fa:f3:dc:b4:4e:ef:3a:f9:d3:9e:aa:64:08:00 SRC=192.168.64.1 DST=172.19.0.9 LEN=52 TOS=0x10 PREC=0x00 TTL=63 ID=0 PROTO=TCP SPT=52842 DPT=80 SEQ=1184533181 ACK=3245308350 WINDOW=2058 RES=0x00 ACK URGP=0 OPT (0101080A6EF8750D8E0CA28F)
fedora kernel: [ 2631.981304] TRACE: filter:DOCKER-USER:return:1 IN=enp0s1 OUT=br-00ea7870520a MAC=fa:f3:dc:b4:4e:ef:3a:f9:d3:9e:aa:64:08:00 SRC=192.168.64.1 DST=172.19.0.9 LEN=52 TOS=0x10 PREC=0x00 TTL=63 ID=0 PROTO=TCP SPT=52842 DPT=80 SEQ=1184533181 ACK=3245308350 WINDOW=2058 RES=0x00 ACK URGP=0 OPT (0101080A6EF8750D8E0CA28F)
fedora kernel: [ 2631.981308] TRACE: filter:FORWARD:rule:2 IN=enp0s1 OUT=br-00ea7870520a MAC=fa:f3:dc:b4:4e:ef:3a:f9:d3:9e:aa:64:08:00 SRC=192.168.64.1 DST=172.19.0.9 LEN=52 TOS=0x10 PREC=0x00 TTL=63 ID=0 PROTO=TCP SPT=52842 DPT=80 SEQ=1184533181 ACK=3245308350 WINDOW=2058 RES=0x00 ACK URGP=0 OPT (0101080A6EF8750D8E0CA28F)
fedora kernel: [ 2631.981312] TRACE: filter:DOCKER-ISOLATION-STAGE-1:return:3 IN=enp0s1 OUT=br-00ea7870520a MAC=fa:f3:dc:b4:4e:ef:3a:f9:d3:9e:aa:64:08:00 SRC=192.168.64.1 DST=172.19.0.9 LEN=52 TOS=0x10 PREC=0x00 TTL=63 ID=0 PROTO=TCP SPT=52842 DPT=80 SEQ=1184533181 ACK=3245308350 WINDOW=2058 RES=0x00 ACK URGP=0 OPT (0101080A6EF8750D8E0CA28F)
fedora kernel: [ 2631.981316] TRACE: filter:FORWARD:rule:7 IN=enp0s1 OUT=br-00ea7870520a MAC=fa:f3:dc:b4:4e:ef:3a:f9:d3:9e:aa:64:08:00 SRC=192.168.64.1 DST=172.19.0.9 LEN=52 TOS=0x10 PREC=0x00 TTL=63 ID=0 PROTO=TCP SPT=52842 DPT=80 SEQ=1184533181 ACK=3245308350 WINDOW=2058 RES=0x00 ACK URGP=0 OPT (0101080A6EF8750D8E0CA28F)
对比这张图
以及宿主机节点上具体的规则(删除了 docker0 相关)
*raw
:PREROUTING ACCEPT [312:33617]
:OUTPUT ACCEPT [335:24610]
-A PREROUTING -d 192.168.64.4/32 -p tcp -m tcp --dport 10000 -j TRACE
-A OUTPUT -d 172.19.0.0/16 -p tcp -m tcp --dport 80 -j TRACE
COMMIT
*nat
:PREROUTING ACCEPT [4:903]
:INPUT ACCEPT [4:903]
:OUTPUT ACCEPT [222:16027]
:POSTROUTING ACCEPT [224:16155]
:DOCKER - [0:0]
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.19.0.0/16 ! -o br-00ea7870520a -j MASQUERADE
-A POSTROUTING -s 172.19.0.9/32 -d 172.19.0.9/32 -p tcp -m tcp --dport 80 -j MASQUERADE
-A DOCKER -i br-00ea7870520a -j RETURN
-A DOCKER ! -i br-00ea7870520a -p tcp -m tcp --dport 10000 -j DNAT --to-destination 172.19.0.9:80
COMMIT
*filter
:INPUT ACCEPT [308:34040]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [351:26291]
:DOCKER - [0:0]
:DOCKER-ISOLATION-STAGE-1 - [0:0]
:DOCKER-ISOLATION-STAGE-2 - [0:0]
:DOCKER-USER - [0:0]
-A FORWARD -j DOCKER-USER
-A FORWARD -j DOCKER-ISOLATION-STAGE-1
-A FORWARD -o br-00ea7870520a -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -o br-00ea7870520a -j DOCKER
-A FORWARD -i br-00ea7870520a ! -o br-00ea7870520a -j ACCEPT
-A FORWARD -i br-00ea7870520a -o br-00ea7870520a -j ACCEPT
-A DOCKER -d 172.19.0.9/32 ! -i br-00ea7870520a -o br-00ea7870520a -p tcp -m tcp --dport 80 -j ACCEPT
-A DOCKER-ISOLATION-STAGE-1 -i br-00ea7870520a ! -o br-00ea7870520a -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -j RETURN
-A DOCKER-ISOLATION-STAGE-2 -o br-00ea7870520a -j DROP
-A DOCKER-ISOLATION-STAGE-2 -j RETURN
-A DOCKER-USER -j RETURN
COMMIT
分析内容可以知:
- 两个测试都是 TCP 三步握手的第一、三步的两个包,从包的 SEQ 字段可以验证,也可以用 iptables rule 的 pkts 字段的差异来进行验证
iptables -v -t raw -L PREROUTING --line-number
Chain PREROUTING (policy ACCEPT 239 packets, 24919 bytes)
num pkts bytes target prot opt in out source destination
1 8 445 TRACE tcp -- any any anywhere fedora tcp dpt:ndmp
- 对于 TCP 链接,只有第一个包才会经过 nat,后续包属于
RELATED,ESTABLISHED
,直接放过,不需要重新 nat - 对于跨节点访问,第一个包首先来到
PREROUTING
链, 命中了 docker 添加的DNAT
规则:nat:DOCKER
(rule 3
),效果就是第三行到第四行DST
从192.168.64.4
变成了172.19.0.9
iptables -v -t nat -L DOCKER --line-number
Chain DOCKER (2 references)
num pkts bytes target prot opt in out source destination
1 0 0 RETURN all -- docker0 any anywhere anywhere
2 0 0 RETURN all -- br-00ea7870520a any anywhere anywhere
3 2 128 DNAT tcp -- !br-00ea7870520a any anywhere anywhere tcp dpt:ndmp to:172.19.0.9:80
DNAT
后走到了Forward
链,DOCKER-USER
和DOCKER-ISOLATION-STAGE-1
都return
了。最后通过filter:FORWARD:rule:8
来到filter:DOCKER
上,accept
将包送到了docker bridge: br-00ea7870520a
上,从而送进了容器 172.19.0.9。当然,第二个包依然属于RELATED,ESTABLISHED
,所以在filter:FORWARD:rule:7
上直接ACCEPT
,不需要再走filter:DOCKER
了。
iptables -v -t filter -L DOCKER --line-number
Chain DOCKER (2 references)
num pkts bytes target prot opt in out source destination
1 2 128 ACCEPT tcp -- !br-00ea7870520a br-00ea7870520a anywhere 172.19.0.9 tcp dpt:http
iptables -v -t filter -L FORWARD --line-number
Chain FORWARD (policy DROP 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
1 18 1290 DOCKER-USER all -- any any anywhere anywhere
2 18 1290 DOCKER-ISOLATION-STAGE-1 all -- any any anywhere anywhere
3 0 0 ACCEPT all -- any docker0 anywhere anywhere ctstate RELATED,ESTABLISHED
4 0 0 DOCKER all -- any docker0 anywhere anywhere
5 0 0 ACCEPT all -- docker0 !docker0 anywhere anywhere
6 0 0 ACCEPT all -- docker0 docker0 anywhere anywhere
7 8 421 ACCEPT all -- any br-00ea7870520a anywhere anywhere ctstate RELATED,ESTABLISHED
8 2 128 DOCKER all -- any br-00ea7870520a anywhere anywhere
9 8 741 ACCEPT all -- br-00ea7870520a !br-00ea7870520a anywhere anywhere
10 0 0 ACCEPT all -- br-00ea7870520a br-00ea7870520a anywhere anywhere
补充
最后补充一下,对于较新的内核,iptables 是 nf_tables
而不是 iptables-legacy
的场景下:
# 较旧的
iptables -V
iptables v1.8.8 (legacy)
# 较新的
iptables -V
iptables v1.8.8 (nf_tables)
用 nft monitor trace # xtables-monitor --trace
命令来观察结果(这个方式比 messages 更友好:每个包都有一个单独的 id,更直观),当然,trace 方式也有差别,此处不展开,感兴趣读者可以自行到参考链接中进行研究。
参考
- https://serverfault.com/questions/122157/debugger-for-iptable...
- https://man7.org/linux/man-pages/man8/iptables.8.html#TARGETS
- https://man7.org/linux/man-pages/man8/iptables-extensions.8.html
- https://stackoverflow.com/questions/41748330/how-to-read-iptables-trace-logs-policy-numbers#
- https://wiki.nftables.org/wiki-nftables/index.php/Ruleset_deb...
- https://unix.stackexchange.com/questions/614413/how-to-proper...
在前文《数据包如何游走于 Iptables 规则之间》发布后,有几个读者留言,说看了文章还是觉得不容易调试 Iptables,今天就来把几个常见的问题补充一下,希望能帮到大家。
TRACE 过滤
第一个就是 TRACE 不够精准,产生了很多无关的 log,导致无法阅读和分析 TRACE 的结果。
通常我们会这样写 TRACE:iptables -t raw -A PREROUTING -p tcp --destination 192.168.64.4 --dport 10000 -j TRACE
但是如果有很多 client 访问这个 destination,就会产生很多 log,影响我们调试。
为此,我们可以再加一个 src port 限制:
iptables -t raw -A PREROUTING -p tcp --sport 40001 --destination 192.168.64.4 --dport 10000 -j TRACE
然后我们测试时,可以用 netcat 指定源端口,如: nc -p 40001 192.168.64.4 10000
或者 curl 指定源端口,如: curl --local-port 40001-40001 192.168.64.4:10000
这样就能比较精准地得到我们希望看到的流量的 Iptables 路径了。
TRACE 清理
当我们需要更换 TRACE 策略,或者结束调试时,需要清理之前加进去的规则。如果只是测试环境,那么可以简单粗暴地执行iptables -t raw -F
;
如果是重要的环境(比如生产环境)有两种做法
- 把上面添加规则的
-A
变成-D
即可iptables -t raw -D PREROUTING -p tcp --sport 40001 --destination 192.168.64.4 --dport 10000 -j TRACE
- 首先执行
iptables -v --line-number -L PREROUTING -t raw
,得到 TRACE 规则的 rulenum,然后iptables -t raw -D PREROUTING rulenum
即可删除
Linux 发行版问题
一般我们采用 tail -f /var/log/messages | grep "TRACE"
即可得到 log 输出,但是在 centos7
中则不然,因为 centos7
使用 systemd
的 journald
作为默认的日志系统,这种情况下,内核日志不会被重定向到 /var/log/messages
,而是到了 journald
,我们使用 journalctl -k | grep TRACE
即可看到 iptables TRACE log。其他发行版也可能有这个问题,我们调试时注意下就好了。
iptables 版本问题
有时候我们会发现 tail -f /var/log/messages | grep "TRACE"
什么输出都没有,这时候我们可以运行 iptables -V
检查版本,如果发现是 nf_tables
,则需要执行 xtables-monitor -t
来获取日志。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。