首页 | 新闻 | 新品 | 文库 | 方案 | 视频 | 下载 | 商城 | 开发板 | 数据中心 | 座谈新版 | 培训 | 工具 | 博客 | 论坛 | 百科 | GEC | 活动 | 主题月 | 电子展
返回列表 回复 发帖

TCP 选择性应答的性能权衡(1)

TCP 选择性应答的性能权衡(1)

在最近几个月 Internet 上对 Linux® 开发的日常讨论中,一个重要的话题就是 Linux 的 TCP SACK(Selective Acknowledgment)实现。讨论主要集中在处理某些 SACK 事件时 TCP 协议栈的性能方面,有一些人认为这会带来安全隐患。
我对这些讨论非常感兴趣,但是这些讨论似乎缺乏有力的数据支持。他们讨论的是哪些特定情形?这只是一个小的性能缺陷,还是完全有可能使服务器受到拒绝服务攻击?
我收集了一些有关此主题的引述(参见  中这些引述的链接):
David Miller:“现在,基本上每个 TCP 协议栈都存在此问题,处理无效或恶意创建的 SACK 块会消耗大量 CPU 资源。”
Ilpo Jarvinen [1]:“但是,只要在 sacktag 中存在对 skb 的 fack_count 的依赖,即使在 RB-tree 的情况下,CPU 也存在处理攻击的空间,因为在慢速道上只能步行”。
NC State University:“我们在这个试验中展示了 SACK 处理的效率。正如我们看到的许多情形,TCP 不能很好地处理大型窗口拖放,尤其是大量数据的缓冲。”
CHC IT: “最后,对 2.4 和 2.6 版提出一个警告:对于 TCP 窗口大于 20 MB 的大型 BDP 路径,可能会遇到 Linux SACK 实现问题。如果 Linux 收到一个 SACK 事件时有大量包在传递,它会花费很长的时间定位经过 SACK 处理的包,而且将会导致 TCP 超时,CWND 将返回到第一个包。”
本文将观察 SACK 实现及其在非理想条件下的性能,我们将从 Linux 2.6.22 入手,它目前是 Ubuntu 7.10 的普通发行版内核。该内核在几个月之前发行,自从它发布以来没有出现什么问题,开发人员一直在为其编写代码。当前的开发内核为 2.6.25,其中包含 Ilpo Jarvinen 提供的一系列补丁,用于处理 SACK 性能。本文最后将查看这些代码如何发挥作用,并简略讨论进一步的更改。
SACK 简介SACK 由 RFC 2018、2883 和 3517 定义(参见  中这些 RFC 的链接)。普通 TCP(即未提供 SACK 特性)应答是严格累积的 — 对 N 的应答意味着字节 N 和所有之前的字节都已经收到。SACK 要解决的问题普通累积式应答的 “全有或全无” 性质。
例如,即使包 2(假设从 0 到 9 的序列)是在传送过程中惟一丢失的包,接收方也只能对包 1 发出一个普通的 ACK,因为这是连续接收到的包中的最后一个。另一方面,SACK 接收方可以发出包 1 的 ACK 和包 3 到包 9 的 SACK 选项。这种附加信息可以帮助发送方确定丢失的包最少,只需重新传送很少的数据。如果没有这种附加信息,它需要重新传送大量的数据,这样会降低传送速率,从而适应高丢包率的网络。
在高延迟的连接中,SACK 对于有效利用所有可用带宽尤其重要。高延迟会导致在任何给定时刻都有大量正在传送的包在等待应答。在 Linux 中,除非得到应答或不再需要,这些包将一直存放在重传队列中。这些包按照序列编号排队,但不存在任何形式的索引。当需要处理一个收到的 SACK 选项时,TCP 协议栈必须在重传队列中找到应用了 SACK 的包。重传队列越长,找到所需的数据就越困难。
每个包中的最多包含 4 个 SACK 选项。
攻击场景 常见的安全漏洞源于这样一个事实:SACK 选项的接收方在收到一个包后可能会被要求做大量工作。这种 N:1 的比例使 SACK 发送方可以打击非常强大的平台上的接收方。
特定于 Linux SACK 处理器的攻击场景要求重传队列中已经持有大量包。然后攻击者发送一个充满 SACK 选项的包,目的是使另一方主机扫描整个队列以处理每个选项。一个指向队列末尾包的选项可能会迫使接收方的 TCP 协议栈遍历整个队列以判断此选项指向哪一个包。显然,攻击客户机可以根据自己的喜好发送任何 SACK 选项,但不太容易看出的一个问题是,它可以轻易控制被攻击方的重传队列的长度。SACK 选项的创建者可以控制接收者对接收到的每个选项执行的工作量,因为它也可以控制队列的大小。
重传队列中包的数目基本上取决于两台主机间的带宽延迟效果(bandwidth delay product,BDP)。一条路径的带宽受到网络的物理属性的限制 — 攻击者在未获得更多基础设施的情况下很难增加带宽。可是,攻击者通过在传输之前使每个应答阻塞一小段时间就可任意增加延迟。为了有效利用高延迟连接的带宽,服务器需要保持大量的包处于传输途中,以便总传输时间等于它得到延迟应答的时间。如果不这样做,则网络在某段时间内不会传输任何包,因此它的带宽未得到充分利用。高延迟路径需要大量在传输中的包以便充分使用网络,而 TCP 发送方会在拥塞控制标准的限制范围内尽可能充分利用带宽。
延迟应答大大增加了重传队列的长度,这为攻击提供了必要条件。例如,如果在相对慢的每秒 10MB 的连接中增加 1750ms 的延迟,在这个期限内会产生超过 12000 个包。稍快的连接会产生更多的包,但这种漏洞部分源于这样一个事实,因为引入了较高的延迟,这种方法只适用于标准家庭宽带连接。
在传输延迟的应答时客户机选择 SACK 选项的值。发送方增加一个指向最新收到的包数据的 SACK 选项(也就是拥有最大序列号的数据)。在这种场景中,此数据开始延迟自己的 ACK,但现在它可以选择性应答,这会造成在已经应答和选择性应答的包之间的距离在重传队列中是最长的。
这种特殊的攻击场景(本文的焦点)被称为 find-first 攻击,因为它迫使 TCP 协议栈花费大量时间查找由 SACK 选项指向的第一个字节。
攻击场景评测
Kode Vicious:“采用特殊评测!”。
了解这些背景知识后,让我们来判断这是否是一个严重的问题。这是一个需要细微优化的领域还是一个严重的危机,或者介于二者之间?为弄清这一点,我们需要用真实的数据描述问题的严重程度。第一项任务就是确定要采集哪些数据以及如果进行评测。
实验设置  需要收集的最重要的数据项是服务器的 CPU 使用率。Oprofile 可通过标准的 CLK_UNHALTED 计数器完成这个棘手的工作。另一感兴趣的数据是在处理 SACK 时扫描的包的数目,以及重传窗口(window)的平均大小。我测试了服务器的源代码以获得扫描包的计数器。在没有注释的情况下我又重新进行了测试以确保我得到的结果与标准服务器看到的结果相同。
  传输中的窗口大小也是一个感兴趣的数据。如果在客户机可以跟踪发出的 SACK 选项的数目,则重传队列的大小可从扫描包计数器中计算得到。对于非 SACK 测试用例,我使用客户机报告的延迟应答队列的典型长度作为发送方窗口大小的近似值。
  全部评测在标准 Linux 服务器上完成。我编写了定制客户机,用于驱动实验并触发服务器上感兴趣的代码路径。客户机实现自己的 TCP 栈并在原始 Socket 内核 API 上运行。客户机当然算不上完整的 TCP 协议栈,但这对测量感兴趣的各种变量而言已经足够了。
  客户机启动实验,首先与服务器连接并发送一个简单的 HTTP 请求,请求目的是获取一个 700MB 的 ISO 文件。然后它消耗由服务器通过普通的 100Mbit/s 的网络发送来的全部数据。服务器发来的每个包在经过 1750ms 的延迟后得到应答。服务器在看到应答之前逐步增加同一时刻发送的包数目以尽力填满 1750ms 的时限。我对超过 14000 个传输中的包的窗口进行了观测。
客户机有一个可配置选项,在每个应答传输前,把与最近到达的数据相关的 SACK 信息增加到每个应答上。如果启用此选项,则产生的应答包括最多四个 SACK 选项。每个选项都指向当前应答延迟了的四个最先到达包中的一个 1 字节的随机范围。如果正等待的包不足四个,则产生相应数目的选项。
为了进行有意义的比较,我从三个不同的位置同时收集此数据:
  • 基准:有首次测试中,我先确定基准评测的方法。没有使用定制的客户机和 TCP 栈,而是使用了标准的 Linux TCP 协议栈和 wget 命令行 HTTP 客户机。
  • 定制,不使用 SACK:第二个数据点需要使用定制的引入大窗口的客户机,但根本没有使用 SACK 选项。这个数据点可以把由大窗口造成的基本影响与处理恶意 SACK 选项造成的影响区分开来。
  • 定制,启用 SACK:最后的数据集使用了引入大窗口的客户机,同时也在每次发送 ACK 时增加四个 SACK 选项。
我使用的服务器比较陈旧,是 1.2GHz Athlon/XP 的服务器。
返回列表