解决eCos系统的lwIP存在的bug,该bug导致TCP重发失效,最终导致TCP拒绝服务

时间:2022-10-22 00:36:08


问题描述

跑 eCos + lwIP 的设备作为 TCP 服务器,在网络不繁忙不丢包的情况下,一切正常,在网络繁忙会出现丢包的情况下,重试几次后 TCP 拒绝服务(对 SYN 包都不会有任何响应, ping 功能可能正常也可能无响应),其它任务正常。能够承受的重试次数和选项 ​​lwIP networking stack >> Protocols >> TCP support >> Sender pbufs (CYGNUM_LWIP_TCP_SND_QUEUELEN)​​ 有关(据我推测,没有实测)。

抓到的数据包如下:

解决eCos系统的lwIP存在的bug,该bug导致TCP重发失效,最终导致TCP拒绝服务

  • Time 栏是两个数据包的间隔。
  • 835 号数据包是客户端向服务器发起的请求,跑 eCos + lwIP 的设备作为服务器端。
  • 836 号数据包是服务器在接收到请求后给出的响应。836 号同时还携带了 835 号包的 ACK。
  • 837 号数据包是 835 号数据包的重发包,显然客户端没有收到 836 号数据包,836 号数据包丢失了。客户端既没有收到响应数据,也没有收到 835 号包的 ACK,经过 250 ms 后重发请求数据包,即 837 号数据包。
  • 838 号数据包是服务器告诉客户端已经接收到 837 号包了,不用再重发了,注意这个包只有 ACK 没有响应数据。
  • 10 秒后,客户端没有收到响应,关闭了连接,839 号包 ~ 841 号包是关闭连接三次握手。

看出来了吗?

客户端没有收到服务器的响应,服务器也没有重发!这不是 TCP 啊!TCP 是会重发的,对方没有收到就会重发,但是这里就是没有重发。

解决办法

原因是定时器资源不够,TCP 没有申请到定时器,没法处理任何和超时有关的事物。先给出解决办法,问题分析在后面。有两个办法可以解决这个问题,二选一,第二方法更彻底。

一、在配置工具中增加一个用户定时器

解决eCos系统的lwIP存在的bug,该bug导致TCP重发失效,最终导致TCP拒绝服务

将上图所示的 ​​Simultaneous active timeouts by user modules​​ 选项值加1。这样 lwIP 就有足够的定时器可用了,TCP 所需要的定时器也能正确分配到。

二、修改 lwip_net.cdl

这个办法更加地彻底,从根本上解决了缺少定时器的问题。

关于超时定时器的配置部分(lwip_net.cdl:1342):

cdl_option CYGNUM_LWIP_MEMP_NUM_CORE_SYS_TIMEOUT {
display "Simultaneous active timeouts by core modules"
flavor data
calculated { CYGPKG_LWIP_TCP + CYGFUN_LWIP_IP_REASSEMBLY +
CYGPKG_LWIP_ARP + (CYGPKG_LWIP_DHCP * 2) +
CYGPKG_LWIP_AUTOIP + CYGPKG_LWIP_IGMP +
CYGPKG_LWIP_DNS + CYGPKG_LWIP_PPP }
description "
The number of simulateously active timeouts used by the lwIP
core modules."
}

从这个选项的脚本可以看出,lwIP 所需要的定时器数量是根据所选择的功能自动算出来的,但是少了 ​​lwip_select​​ 所需要的那个超时定时器。

将该脚本改成如下:

cdl_option CYGNUM_LWIP_MEMP_NUM_CORE_SYS_TIMEOUT {
display "Simultaneous active timeouts by core modules"
flavor data
calculated { CYGPKG_LWIP_TCP + CYGFUN_LWIP_IP_REASSEMBLY +
CYGPKG_LWIP_ARP + (CYGPKG_LWIP_DHCP * 2) +
CYGPKG_LWIP_AUTOIP + CYGPKG_LWIP_IGMP +
CYGPKG_LWIP_DNS + CYGPKG_LWIP_PPP +
CYGPKG_LWIP_SOCKET_API }
description "
The number of simulateously active timeouts used by the lwIP
core modules."
}

即在选项的计算式中加入 ​​+ CYGPKG_LWIP_SOCKET_API​​​ 。​​lwip_select​​​ 是 ​​CYGPKG_LWIP_SOCKET_API​​ 组件提供的一个函数。

修复后抓取的包:

解决eCos系统的lwIP存在的bug,该bug导致TCP重发失效,最终导致TCP拒绝服务

  • 204 号包是客户端发起的请求。
  • 205 号包是我们的设备返回的响应,这个包没有到达客户端,中途丢失了。
  • 206 号包是 204 号包的重发。
  • 207 号包是对 206 号包的确认,客户端接收到这个 ACK 包后,知道服务器已经接收到请求包了,不再重发请求包。
  • 208 号包是对 205 号的重发,我们的设备一直没有收到客户端对 205 号包的 ACK ,因此重发此包。
  • 209 号包是客户端确认接收到了我们的设备返回的响应。
  • 这个处理丢包的流程是正确的。

原因分析

lwIP 有个很好的特性,那就是 ​​Traffic statistics​​​,打开这个特性,lwIP 可以统计数据包的收发以及内存的使用情况。在资源足够的情况下,强烈建议打开该选项。在 eCos 中,这个选项是 ​​lwIP networking stack >> Traffic statistics (CYGPKG_LWIP_STATS)​​。

调试是查找问题的好帮手,一定要留调试接口,一定要掌握调试这么手艺。

将设备连接调试器,连续运行直到故障重现,暂停程序执行,这个时候就可以检查 statistics 了,查看 lwip_tcpip >> current >> src >> core >> stats.c 文件中的 ​​lwip_stats​​ 结构。

检查的结果就是 SYS_TIMEOUT 类型的 LWIP_MEMPOOL 发生了错误,只分配了 6 个,但是实际最多需要 7 个。

给 lwIP 多增加几个定时器资源,再执行就不会出现故障了。

因此可以肯定 TCP 处理超时重发的定时器分配失败了,也就没有 TCP 的超时处理了,也就不会重发了。lwIP 所需要的定时器,大部分都在 ​​tcpip_thread​​​ 的开始处申请完了,TCP 的定时器是在有需要的时候调用 ​​tcp_timer_needed​​ 函数申请的。在 eCos 中,lwIP 所需要的定时器个数是自动计算的:

CYGNUM_LWIP_MEMP_NUM_CORE_SYS_TIMEOUT = 
CYGPKG_LWIP_TCP + CYGFUN_LWIP_IP_REASSEMBLY +
CYGPKG_LWIP_ARP + (CYGPKG_LWIP_DHCP * 2) +
CYGPKG_LWIP_AUTOIP + CYGPKG_LWIP_IGMP +
CYGPKG_LWIP_DNS + CYGPKG_LWIP_PPP

经检查发现,除了上面引用到的特性会使用定时器外,socket 的 ​​lwip_select​​ 函数也会使用到定时器,因此这里算式少算了一个定时器,问题就出在这。

​lwip_select​​​ 函数通过调用 ​​sys_sem_wait_timeout​​ 函数间接地使用到了定时器。

我们的程序使用了 ​​lwip_select​​ 函数,而且还带超时,所以触发了这个bug。

缺少 TCP 定时器引起TCP拒绝服务的原因:

  • TCP 发送一个数据包后不会立即释放该数据包的内存资源,因为该数据包可能还需要重发。
  • 网络不丢包的情况下,对方能正确地接收数据包,并返回 ACK 包,lwip 在接收到 ACK 包后释放发送包的内存资源。所以在不丢包的情况下,不会有问题。
  • 网络丢包的情况下,超时定时器会重发没有被 ACK 的数据包,直到接收到 ACK 或发送超时再释放发送包的内存资源,超时定时器未被开启的情况下,发送的数据包丢失以后,因为没有了超时重发,对方永远都接收不到数据包,lwip 也不会接收到该数据包的ACK而释放资源,同样也不会因为发送超时而释放资源(因为定时器没开启,所有超时机制都失效了),因此这个数据包将永远占据着资源而不释放。发生多次这样的情况以后,内存资源被耗尽,lwip 已经申请不到内存来处理新的数据包了,开始出现拒绝服务。
  • lwip 使用的内存资源有多种,看哪种资源首先被耗尽,可能会导致ping不通的情况。

还有更快捷地发现问题的途径:启用断言!

lwIP 的断言设计也是很完善的,完全可以使用断言来捕获资源不够的问题,查看定时器申请的代码(​​sys_timeout​​函数):

timeout = memp_malloc(MEMP_SYS_TIMEOUT);
if (timeout == NULL) {
LWIP_ASSERT("sys_timeout: timeout != NULL", timeout != NULL);
return;
}

如果启用了断言,当资源分配失败,这里的 ​​LWIP_ASSERT​​ 就被触发了!

但是断言有一点不好,非调试状态下它会引起系统死机或复位,而不仅仅是网络功能失效,对产品的口碑而言,网络功能失效比死机或复位要好一些。

要打开 eCos 中 lwIP 的断言,那么就要打开整个 eCos 的断言。

嗯 …… 下次找问题,先打开断言!