Ovear's Blog

I'am Ovear,Ovear is me.

Theme Refrain made by Eiko

Proudly powered by WordPress

记一次因CONNTRACK和ICMP导致的奇怪问题

起因

策略路由是个好东西,就是用起来特别麻烦,经常产生一些很奇怪的问题,这次遇到的问题也是特别迷惑。

有一条规则会使用CONNTRACK对链接进行分类,然后把整个链接打上标记,如果不符合任何规则就打一个默认标记。

这条规则看起来一点问题都没有,使用起来也非常正常。但是如果使用MTR对负责这条规则的链接做路由追踪的话,就会出现第一次路由追踪完毕后,从第二跳开始就会丢包。

排查

遇到这个问题之后,第一时间就怀疑是标记发生了错误。检查CONNTRACK之后,发现标记果然被打上了默认的标记

但是仔细查找后发现,并未发现规则有明显的问题,且丢包只会发生在第2轮开始后第2个hop开始丢包;而手动使用ping和traceroute则是全程没有问题的。

用iptables检查日志后,发现在第一轮的一个包时,mark被更改为默认的标记,但是后续的包又变回正常的。而在第二轮的第一个包时,mark又被更改为默认的标记,此时后续的包都变为默认的标记的了。

那么就有下面两个疑问

  1. 第一轮的第一个包后,CONNMARK被置为默认,但是后续的包又恢复正常的了。而第二轮的第一个包后,后续包为什么又不正常了。
  2. 为什么第一二轮的第一个包,会被设置为默认标记

原因

又经过一番研究,通过iptables日志中发现了问题的关键所在:

第一个包的TTL都是为1,即如果不是本地的IP,则会被本机直接返回TTL超时,那么就会经过netfilter的OUTPUT chain。而OUTPUT chain中有一条,如果DST是本地网络就设置为默认标记的规则。

这就回答了第2个问题:因为TTL exceed包是由本机发出的,导致被包被标记为默认标记并存入CONNTRACK。

但是这让问题更加迷惑了:第一轮的一个包被标记后,链接中后续的包怎么没有被设置为默认标记反而使用了正常的标记?按照CONNTRACK的描述来看,应该是对整条链接打标记才对,这是一条链接,应该已经绑起来了才对呀?

未解之谜

在网上搜寻了半天,也没有找到任何线索之后,决定还是使用老办法:检查nf_conntrack和iptables日志,最终发发现了conntrack的一个奇怪行为:

  1. conntrack对于ICMP链接也会进行追踪,猜测应该是通过type/code/seq来实现的
  2. conntrack追踪ICMP链接时,只会对与TTL>1的非本地连接进行追踪;而对于本地IP即使TTL=1时,也会进行追踪

这个行为我猜测可能是因为,TTL=1的非本地地址ICMP链接没有追踪的意义,所以直接就不追踪了。因为对于非本地地址来说,TTL=1一定是不可达的,终止在本地的OUTPUT就行了。

知道这个行为之后,之前提到的问题2,似乎也就清晰了。

对于mtr来说,他目前的行为是从TTL=1开始,一直到目的地(即不出现TTL exceed,同时出现ICMP echo-reply)的TTL,按照从小到大顺序进行轮询;到达目的地之后,在从TTL=1开始继续递增,重复进行轮询

因为其type/code都是一致的,且seq是规律递增的,按理来说conntrack会将其归为一条连接。但是因为存在上面不追踪非本地TTL=1的ICMP链接行为的存在,导致conntrack会将其视为两个链接。

即:conntrack会忽略第一个TTL=1的MTR ICMP包,从TTL=2的第二个MTR ICMP包才开始追踪链接。

所以对于conntrack来说,我们实际的一个MTR的链接,会被视为两个链接。所以就出现了MTR中第1轮TTL=1的包和第2轮TTL=1的包不在一个链接中的情况:

连接1:只包括第1轮TTL=1发送以及回复的2个数据包。
连接2:其余剩下所有的数据包,包括第1轮TTL=2的包,和第2轮TTL=1的包,以及对应回复的数据包。

最终导致conntrack的restore-mark没有按照预期执行。

同时对于上述行为,也衍生出下面的问题:对于TTL=1的ICMP包,因为conntrack不会追踪,所以restore-mark会失效,save-mark也有意义。这样会导致这些包无法通过restore-mark保持标记,会重复的进行iptables规则匹配。

需要注意的时,我们的连接是双向的,因此conntrack跟踪时,除了将单个方向的数据包关联为一个连接以外,对应回复的数据包也会关联进来。具体而言:

  1. 对于只有TTL=1的包的连接,比如说ping -t 1 www.qq.com,conntrack只会关联其发送的单个数据包以及回复的数据包。那么在两个发送的TTL=1的数据包之间,restore-mark将会不起作用,如果使用了save-mark那么restore-mark仅会在这个ICMP包对应回复的数据包起作用。
  2. 对于同时有TTL=1、和TTL>1的包的连接,就如同上面我们讨论的一样,conntrack会将数据包关联进入两个连接,对应的restore-marksave-mark也只在对应的两个连接中的作用域中生效。

在本文中,我的OUTPUT chain中未考虑到非目标主机中发出ICMP包也被归入conntrack的同一个连接中,同时也因为一些原因,也未检查对应连接是否已经设定过mark,导致问题的发生。

回溯一下发生的事情:

  1. 本地执行mtr www.qq.com
  2. 第1轮TTL=1的包及其回应包,被正确打上标记【被conntrack视为连接1】
  3. 第1论TTL=2的包及其回应包,被正确打上标记【被conntrack视为连接2】
  4. 第1轮剩余的包及其回应包,被正确打上标记
  5. 第2轮TTL=1的请求包(ICMP Echo Request),被正确打上标记,注意这里是连接2
  6. 第2轮TTL=1的回应包(ICMP TTL time Exceeded)有本地路由器发出,经过OUTPUT Chain,被错误打上默认标记。注意这里还是连接2
  7. 被默认路由策略匹配中,导致路由路径变更。该连接的路由发生意外的路由路径变更,导致后续的MTR数据包路由路径也产生变更。
  8. 第2轮剩余其他的所有数据包,都因为错误的标记被restore-mark恢复,导致错误的路由路径。
  9. 第3轮TTL=1的回应包,再次回到5还是连接2

所以第1轮mtr数据包走向了正确的路由,而之后的数据包都出现了丢包,同时mtr第2跳出现了意外回应的主机。
正是因为conntrack奇怪的行为,反而在有问题的策略路由规则下,保证了第1轮mtr数据包(也包括traceroute)的正常运作;而又是因为conntrack这个奇怪的行为,导致mtr才会出现(目前观察到的)的问题,也是非常神奇了。
不过也多亏上面这段这么绕的话和这个现象,又学到了奇怪的计算机网络和Linux知识,也算是一大意外收获了,总之还是蛮开心的))

// TODO下
上述这两个行为通过iptables的log也可以观察到,比较神奇;但是没有在网路中找到相关的讨论(可能是我英语太菜,没找到);感觉有空还是得翻一翻源码,看看有没有注释啥的。

解决方法

既然知道原因,解决方法也很简单啦;可以直接忽略icmp,或者对icmp做特殊的规则,亦或者是对于icmp的链接不save-mark。

当然最好还是可以参考openwrt中的mwan3包的规则,其中的规则比较完善。

对于本地OUTPUT的包打标记时,也需要小心以免产生预期外的问题。

参考文章

[1] https://www.frozentux.net/iptables-tutorial/chunkyhtml/x1582.html

记一次因CONNTRACK和ICMP导致的奇怪问题有一条评论

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

© 2024 Ovear's Blog All rights reserved.