syncookies增强

Patrick McManus, April 9, 2008

    1997年,出现了TCP SYN flood攻击。SYN flood是初始化一个TCP连接但不完成该连接,从而消耗掉服务器资源,因而属于一种拒绝服务攻击。对付SYN flood的一种措施是syncookie. syncookie功能在Linux kernel 2.1.44中加入。

    最近对IPv6增加syncookie功能的补丁引发了一些争论。为了完整描述syncookies,首先了解一下TCP如何通过三次握手来建立一个连接:
    (1) 服务器接收到的TCP会话的第一个报文是SYN报文,该报文中携带了synchronize control flag。该SYN flag表示报文的sender希望open一个新连接,该flag只用于 打开连接阶段。
    (2) 服务器回应的报文也携带该SYN flag,因为连接需要在两个方向上open。回应报文还携带一个ACK flag,因而该报文也被称为SYN-ACK. 其目的有两个,一是从server到client连接已经open, 二是回应接收到了从client来的opening报文。
    (3) 最后,client发送一个ACK报文给server,回应接收到了从server到client的SYN-ACK报文,而且双向连接已经建立。

    当SYN flood发生时,server收到三次握手中的第一个报文并回应一个SYN-ACK报文,但是不会再从initiating client收到任何数据。当生成SYN-ACK报文时,大多数server也将在SYNC队列中创建一个表项,该队列用于半开连接以等待握手完成。攻击者意图就是使这些表项一直不能完成连接,当发送大量的SYN报文时该队列中就要保存更多的表项。server在放弃一个连接并释放连接资源之前需要等待很长的超时时间。在server连接超时之前,攻击者flood将打开更多的半开连接。服务器最终将用尽资源而不再能够接受新连接。简单的解决方案包括:每个peer只能与server建立有限数量的连接(resource quota),或由于SYN报文中通常使用虚假源地址因而使用动态调整的pkt filter.

    syncookie原理是:延缓使用任何资源直到接收到三次握手中的第三个报文。这时peer地址就已经被略微认证了,因为在三次握手中的第三个报文中要包含server发出的第二个报文中的序列号. 在这个条件保障下,以peer地址为key进行packet filter和resource quota将能够有效地抵御资源耗尽攻击。

    syncookie基本原理是:仔细处理连接的初始序列号而不是随机选择一个序列号。一旦server接收到SYN报文,将关键信息仔细编码并作为state存储在SYN队列中。这种经过编码的信息是用一个秘钥进行加密hash,形成SYN-ACK报文中的序列号并发送给client。在合法握手的第三个报文中,即从client返回给server的ACK报文中,在acknowledgment number字段中包含该序列号(加1). 这样,open双向连接所必须的所有信息又返回给server,而server在三次握手完成之前不必维护state。

    syncookies的主要缺点是只对TCP握手选项的最基本信息进行了编码。当syncookie功能首次被部署应用时这还不是一个大问题,因为当时所用的选项主要就是Maximum Segment Size (MSS)选项。该选项的目的是让peer在发送报文时不要发送分片包. 这种信息通常作为state存储在SYN队列中. syncookie设计人员很清楚该选项对性能很重要,因而在编码的syncookie中只取出3个比特位。这些比特位被用于接近选项的实际值(to one of 8 common values).

    这些年又添加了许多新的选项,而这些选项与syncookie又不兼容,其中最重要的是window scaling和Selective Acknowledgment(SACK)选项。这些特性分别是允许TCP拥塞控制窗口可超过64KB,以及在这些大窗口下丢失很少的报文的情况下能够更为有效。要是没有这些特性,就不可能在更大带宽或更大延迟的网络中有很好的传输速度。许多家庭宽带链路要求window scaling选项至少要能够完全利用网络连接。由于这个限制以及加密hash需要一定的计算量,因而在Linux内核中,只有当半开连接数量超过由net.ipv4.tcp_max_syn_backlog sysctl控制的高位值时,才求助于syncookie。These connections are less featureful than normal connections but they are only resorted to when the queue would otherwise require active pruning.

    由于cookie机制以前仅对IPv4实现,最近Glenn Griffin提供了补丁添加对IPv6 syncookies的支持。原来syncookie补丁的作者Andi Kleen希望搞清楚该机制是否应当继续支持IPv6:
    Syncookies are discouraged these days. They disable too many valuable TCP features (window scaling, SACK) and even without them the kernel is usually strong enough to defend against syn floods and systems have much more memory than they used to be. So I don't think it makes much sense to add more code to it, sorry.

    Andi所提出的问题包括3点:第1点在他的文章中已经描述了在初始化连接时cookie的能力已经降低. 随着时间的推移,这些选项的价值已经增长,因而使用syncookies的花费也要增长。第2点是Linux并不会用光双向连接所所有的内存,只有到双向连接open后才用光。这个期间内用的是一个"minisock",该minisock是大小为96字节的struct tcp_request_sock,该结构维护连接双向open所需要的最少的state。而双向连接建立的结构是大小为1616字节的struct tcp_sock. 第3点是对overloaded SYN队列的排队管理程序 要比 syncookie功能首次被部署应用时采用的dumb head drop算法更为复杂。因而Andi的建议是由于这些功能已经使Linux足以健壮,因而这些补丁可以不增加。

    其他人开始进行实际测试:
    Willy Tarreau: My tests on an AMD LX800 with max_syn_backlog at 63000 on an HTTP reverse proxy consisted in injecting 250 hits/s of legitimate traffic with 8000 SYN/s of noise.[..] Without SYN cookies, the average response time was about 1.5 second and unstable (due to retransmits), and the CPU was set to 60%. With SYN cookies enabled, the response time dropped to 12-15ms only, but CPU usage jumped to 70%. The difference appears at a higher legitimate traffic rate.

    Ross Vandegrift: Under no SYN flood, the server handles 750 HTTP requests per second, measured via httping in flood mode. With a default tcp_max_syn_backlog of 1024, I can trivially prevent any inbound client connections with 2 threads of syn flood. Enabling tcp_syncookies brings the connection handling back up to 725 fetches per second.

    这些实测数据明确支持syncookie仍然有价值,而且逐渐赢得了许多人的支持。但这些讨论引发了在握手阶段有一些TCP选项没有被考虑的讨论,Florian Westphal和Glenn Griffin还提供了解决方案。他们的解决方案是:类似于经典syncookies中利用ACK包中回应的SYN-ACK序列号,来利用回应的TCP时戳选项。TCP时戳选项是在RFC 1323中引入,而且在现代Linux, Windows和FreeBSD (including OS X)等操作系统中广泛部署,其主要目的是当存在大的拥塞控制窗口时增加测量round trip time(RTT)的频率。

    使用时戳来保存window scale选项和SACK选项值,就要修改SYN-ACK报文的时戳以包含支持它们的state. 在通常的握手期间,client将在SYN-ACK报文中将修改的时戳值回复给server,作为握手第三阶段的时戳选项,这样就在server与clisnt之间传送了SACK选项和window scale选项信息,而这些信息不用在server上存储任何state.

    为了在时戳中为新信息预留空间,要取出时戳的least significant 9个比特位。window scale选项和SACK选项被编码后将被送回来,其代价是在握手交换期间TCP时戳的精确度降低了。采用这种方法,时戳仅在低位丢失512个jiffies.

    下面是在添加了syncookies和timestamp补丁后两个不同的TCP握手。注意在每个握手中,即使在不同的时间点上,SYN-ACK时戳的lowest比特位都相同,这是因为每个握手使用相同的SACK选项和window scaling选项。结果,在每一个SYN-ACK中的时戳值不同,但是低9个比特位共享相同的值0x166.

    13:51:04.582464 IP 127.0.0.1.57985 > 127.0.0.1.4050: S 1061746051:1061746051(0)
           win 32792 <mss 16396,sackOK,timestamp 0xfffea013 0,nop,wscale 6>
    13:51:04.582478 IP 127.0.0.1.4050 > 127.0.0.1.57985: S 2800702917:2800702917(0)
           ack 1061746052 win 32768 <mss 16396,sackOK,timestamp 0xfffe9f66 0xfffea013,nop,wscale 6>
    13:51:04.582480 IP 127.0.0.1.57985 > 127.0.0.1.4050: .
           ack 1 win 513 <nop,nop,timestamp 0xfffea013 0xfffe9466>

    13:59:19.047306 IP 127.0.0.1.45979 > 127.0.0.1.4050: S 218483035:218483035(0)
           win 32792 <mss 16396,sackOK,timestamp 0x0001bed4 0,nop,wscale 6>
    13:59:19.047320 IP 127.0.0.1.4050 > 127.0.0.1.45979: S 1141094138:1141094138(0)
           ack 218483036 win 32768 <mss 16396,sackOK,timestamp 0x0001bd66 0x0001bed4,nop,wscale 6>
    13:59:19.047322 IP 127.0.0.1.45979 > 127.0.0.1.4050: .
           ack 1 win 513 <nop,nop,timestamp 0x0001bed4 0x0001bd66>

    虽然并不保证每一个TCP peer都支持时戳选项,但部署最广泛的OS中都支持时戳。而且因为timestamps, window scaling和SACK选项都与高延迟和宽带网络相关,因而不可能找到一个实现仅仅支持其中一部分选项。

    这种修改方法的一个缺陷是不够通用,因为在以后新的握手选项会继续被部署。目前,MSS, SACK, window scaling以及timestamp选项是对齐报文中仅有的握手选项,NOP选项仅用于报文对齐. 但是要为将来的提高预留空间以支持扩展选项. IANA registry在2007年2月份将选项code 27预留给试验性的RFC 4782 "Quick Start for TCP and IP"。只有时间才能回答该选项是否将成为syncookie的下一个挑战.

    timestamp补丁也就是最近才开发出来,也仅在小范围的开发人员之间讨论,但是它确实以很小的代价,解决了syncookie中的核心问题。

    随着IPv6和现代TCP选项的更新,在网络安全领域syncookies还是能够发挥应有的作用,可能在接下来的10年内不用再变化。

参考资料:
[1] Patrick McManus, Improving syncookies, April 9, 2008, [url]http://lwn.net/Articles/277146/[/url]

------------代码变化------------
在Linux Kernel 2.6.26中时戳选项功能被增加.
增加了以下新的文件:
    net/ipv6/syncookies.c
增加了以下新的函数:
    __u32 cookie_init_timestamp(struct request_sock *req)
    void cookie_check_timestamp(struct tcp_options_received *tcp_opt)
以下函数被修改:
    struct sock *cookie_v4_check(struct sock *sk, struct sk_buff *skb, struct ip_options *opt)