首页 > *nix技术, TCP/IP, 内核技术, 网络协议 > Socket选项系列之TCP_DEFER_ACCEPT

Socket选项系列之TCP_DEFER_ACCEPT

2013年2月24日 发表评论 阅读评论 7,121 次浏览

学过计算机网络课程的人,应该都知道TCP协议有个连接状态转换图,也许对其整体详细不甚清楚,但至少对TCP握手协议有些印象。标准的TCP三次握手(本节仅考虑这种情况,对于四次握手等其他情况,可以参考RFC 793和http://lenky.info/?p=1921)的一般图示如下:

上面图示中的服务器端并不那么容易让人理解,因为我们知道监听套接口仅做请求接收操作,其创建后就永远都处在LISTEN状态(除非服务进程关闭),因此转换为SYN_RCVD、ESTABLISHED状态的必定是服务端根据客户端请求而新建立的连接套接口:

上面的图示只是一种理想状况,而Linux TCP协议栈的实际实现却并不一定就是这样,性能优化是一方面,比如Linux在收到客户端的SYN包时,并不创建实际的连接套接口,而只是创建一个request_sock结构体来代表这个连接套接口,但很显然,相比完全的套接口sock结构体,request_sock结构体要小得多,相关图示如下(为了后面对比,加上accept()的调用时机):

而另一方面则是因为在现实网络中,有一种专门针对TCP协议的攻击:TCP半连接攻击。这种攻击的原理很容易理解,既然服务器要在收到客户端的请求(即SYN数据包到达)后创建相关结构体等而需要消耗系统资源(比如内存,即便是像Linux这种做了优化处理,一次消耗的资源比较少),因此,如果客户端持续发送大量的SYN包,那么就会不断的消耗服务器端的可用资源,如果针对服务器发回的SYN+ACK包,客户端也不做出ACK回复,从而使得服务器反复发送SYN+ACK包导致情况进一步恶化,每个请求连接都持续如此直到连接的最终超时,而在这个持续的过程中,可能有些正常的客户端请求,服务器端因为系统资源不足而无法创建连接、提供服务。
Syn_cookies是针对TCP半连接攻击的重要防范方法之一,原理也很简单,既然TCP半连接攻击的基本原理是服务器端会在收到客户端的SYN包时消耗资源,那么最简单的防范也就是服务器端在收到客户端的SYN包时尽量不做资源分配,把这个动作延后处理(可以看到,连接套接口处于SYN_RCVD状态的时间非常的短):

而必要的信息(也就是客户端的验证信息)保存在网络上。“把信息保存在网络上”,这听起来有点不可思议,但它的依据却也十分简单,因为在正常的情况下,对于服务器发送的SYN+ACK包,假定其SEQ为N,正常客户端回复的ACK包的ACK值将是N+1,所以,服务器端只需把客户端的相关信息转换为一个数值N(也就是cookie值)并发送给客户端,而本身无需保存任何信息,当在收到客户端的回复ACK包时,再根据客户端的相关信息得到一个值M,比较M和ACK包里的ACK-1值,在正常的情况下,它们必定匹配(匹配并不是指两个值完全相等,如何判断是否匹配与具体的转换算法相关)。当然,这只是保证了不消耗内存等存储资源,对于服务器端网络带宽、CPU计算资源(如果计算cookie值的算法已经复杂,可能反而消耗更多)等依旧无法避免消耗,所以Syn_cookies也并非无懈可击。另外,值得说明一点的是,并非开启Linux系统的Syn_cookies选项(即:echo 1 > /proc/sys/net/ipv4/tcp_syncookies)就会立即让Linux TCP协议栈对每一个客户端请求都走发送Syn Cookies的流程,而只有在系统判断当前请求达到一定数量且满足相关条件时才会这么做:

1258:	Filename : \linux-3.4.4\net\ipv4\tcp_ipv4.c
1259:	int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
1260:	{
1261:	…
1281:		if (inet_csk_reqsk_queue_is_full(sk) && !isn) {
1282:			want_cookie = tcp_syn_flood_action(sk, skb, "TCP");
1283:			if (!want_cookie)
1284:				goto drop;
1285:		}

上面所有描述的这些与本节将介绍的TCP_DEFER_ACCEPT选项有何关系?这在于选项TCP_DEFER_ACCEPT本身的目标功能:它将服务器端连接套接口转变成ESTABLISHED状态的时机后移。具体来讲,就是只有当服务器端收到客户端发送的实际请求数据后,才建立其对应的连接套接口(如果同时开启Syn_cookies,那么对于需要走发送Syn Cookies流程的客户端请求就按照前面介绍的Syn_cookies图示,即此时Syn_cookies起主导作用,但这并不是说Syn_cookies与选项TCP_DEFER_ACCEPT不能共用,前面说过,即便是启动Syn_cookies,也并不是所有的客户端请求都会走发送Syn Cookies的流程,所以对于那些不走Syn Cookies流程并且开启选项TCP_DEFER_ACCEPT的监听套接口,就是下面图示这样):

从上图中看到,连接套接口处于SYN_RCVD状态的时间大大的延长,所以要模拟出SYN_RCVD状态的连接套接口非常的容易,对nginx的监听套接口开启选项TCP_DEFER_ACCEPT选项,比如配置为这样:“listen 80 deferred;”,再写一个测试程序(前面有类似的完整代码,这里仅给出关键部分):

46:	Filename : tcp_defer_test.c
47:	    if (connect(sockfd, (struct sockaddr *)(&server_addr),
48:	            sizeof(struct sockaddr)) == -1) {
49:	…
68:	    getchar();
69:	
70:	    write(sockfd, req_header, strlen(req_header));

编译并执行该程序,它会立即connect()到nginx服务程序,但是如果在write()实际数据前让它主动停顿(即getchar();)下来,那么此时利用netstat查看系统的套接口状态(因为都在同一台机器上,三个套接口都显示出来了):

[root@localhost ~]# netstat -natp | grep 80
tcp 0 0 0.0.0.0:80     0.0.0.0:*      LISTEN      21922/nginx         
tcp 0 0 10.0.0.1:80    10.0.0.1:56073 SYN_RECV    -                   
tcp 0 0 10.0.0.1:56073 10.0.0.1:80    ESTABLISHED 3537/./tcp_defer_te

服务器端nginx的监听套接口处于LISTEN状态,其针对客户端请求而建立的连接套接口处于SYN_RECV状态,而客户端tcp_defer_test进程的套接口处于ESTABLISHED状态。
从这个示例可以看到,在客户端发送实际的请求数据前,服务器端的对应连接套接口一直(理论情况,在大多数情况下,Linux内核里的等待时间段都不可能无限的长,因为系统的定时器和超时机制无处不在,我说“一直”一般也就是指直到超时或连接断开,比如收到rst包等,所以请理解字词句里要表达的主要意思,而不要抠字眼,否则夹杂各种例外情况的说明反而忽略的重点,当有必要说明时,比如后续分析到选项TCP_DEFER_ACCEPT在Linux内核里实现时,再根据实际源码把握细节)都处于SYN_RECV状态,即便TCP三次握手已经成功完成。
前面讲过,Syn_cookies使得服务器对系统资源的分配做了一次延后处理,也就是拖延了Linux TCP协议栈(内核态)对资源进行分配的时机,从而在一定程度上防御TCP半连接攻击;但如果客户端发起的是TCP全连接攻击,即客户端发出正常的TCP三次握手流程把连接建立起来(此种情况,Syn_cookies当然是通过的),但不发送任何实际请求数据(因为一旦发送实际的请求数据,服务器端就能进行下一步处理,可能提前判断出请求有错或处理完成而结束,从而达不到让服务器长时间等待消耗的目的),达到消耗服务器应用层(用户态)资源的目的。以nginx为例,当其accept()接受客户端连接请求后,就会创建相关数据结构,比如ngx_connection_t,而我们知道,整个nginx服务进程能分配的ngx_connection_t是非常有限的,如果这个请求迟迟不结束,数据结构ngx_connection_t不能及时释放,后续再来正常的客户端请求,nginx就很有可能提供正常服务:

46:	Filename : ngx_event_accept.c
47:	        c = ngx_get_connection(s, ev->log);
48:	
49:	        if (c == NULL) {
50:	            if (ngx_close_socket(s) == -1) {
51:	                ngx_log_error(NGX_LOG_ALERT, ev->log, ngx_socket_errno,
52:	                              ngx_close_socket_n " failed");

针对如此,根据选项TCP_DEFER_ACCEPT的特性,它在一定程度上缓解了TCP全连接攻击;相比在应用层处理这些非法的客户端请求(即TCP全连接攻击)所带来的消耗,把它们直接交给内核去做处理所消耗的系统资源会更少一点。
下面开始看Linux内核里对选项TCP_DEFER_ACCEPT的具体实现(下面的分析都是针对选项TCP_DEFER_ACCEPT的相关流程),在应用层给套接口设置该选项的常规用法是这样:

46:	Filename : ngx_connection.c
47:	            if (setsockopt(ls[i].fd, IPPROTO_TCP, TCP_DEFER_ACCEPT,
48:	                           &timeout, sizeof(int))
49:	                == -1)

有一个参数timeout很重要,很明显,它指定的是等待客户端发送实际数据的超时时间(nginx默认的就是60秒);这个系统调用对应到内核里就是执行这些代码:

109:	Filename : \linux-3.4.4\net\ipv4\tcp.c
110:		case TCP_DEFER_ACCEPT:
111:			/* Translate value in seconds to number of retransmits */
112:			icsk->icsk_accept_queue.rskq_defer_accept =
113:				secs_to_retrans(val, TCP_TIMEOUT_INIT / HZ,
114:						TCP_RTO_MAX / HZ);
115:			break;

通过函数secs_to_retrans()将应用层传下来的超时时间转换为了对应的重传次数,即服务器是通过统计重传次数(具体重传次数由三个值共同决定:TCP_DEFER_ACCEPT选项、TCP_SYNCNT选项、proc文件系统的tcp_synack_retrie)来判断是否超时的。具体来看,在服务器收到三次握手流程里的第三个数据包(即ACK数据包)时,关键代码在函数tcp_check_req()内:

567:	Filename : \linux-3.4.4\net\ipv4\tcp_minisocks.c
568:	struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb,
569:				   struct request_sock *req,
570:				   struct request_sock **prev)
571:	{
572:	…
720:		/* While TCP_DEFER_ACCEPT is active, drop bare ACK. */
721:		if (req->retrans < inet_csk(sk)->icsk_accept_queue.rskq_defer_accept &&
722:		    TCP_SKB_CB(skb)->end_seq == tcp_rsk(req)->rcv_isn + 1) {
723:			inet_rsk(req)->acked = 1;
724:			NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPDEFERACCEPTDROP);
725:			return NULL;
726:		}

第721行的前半句判断,意味着如果服务器恰好在发送完最后一个(此时TCP_DEFER_ACCEPT选项占主导,那此时发送完后,重传次数刚好超限)或几个(此时是TCP_SYNCNT选项或proc文件系统设置占主导,对应的设置值比TCP_DEFER_ACCEPT选项设置值要大,如果只大1,那么也就是最后一个包)重传SYN+ACK包时收到客户端的ACK数据包,那么不走TCP_DEFER_ACCEPT流程,从而连接将建立并通知应用层的accept()函数。
这就是TCP_DEFER_ACCEPT选项的一种例外情况,虽然客户端还没有发送实际数据,但最终连接也建立起来了,为什么要这样做?这是因为要考虑,有可能客户端待发送的实际数据本身就没准备好而需要一定的等待时间(因而持续回复ACK),也有可能网络环境导致SYN+ACK包与ACK包的确发生了丢失;反正不管怎样,对于出现这样的例外情况时,Linux系统就认为它属于正常的客户端请求,需建立最终连接。
第721行的后半句判断不难理解,它判断当前收到的ACK包是否携带有实际数据,如果是,那么就无需走TCP_DEFER_ACCEPT流程;对于走TCP_DEFER_ACCEPT流程的ACK数据包会被直接丢掉,Mini Socket仍然是Mini Socket(即没有转换为Connect Socket),但其状态被更改为已确认(即inet_rsk(req)->acked = 1;);第724行是系统做信息统计,与此处逻辑无关。
如果客户端一直没有发送实际的请求数据,那么服务器就会反复的重传三次握手流程里的第二个数据包(即SYN+ACK数据包),重传的次数由三个数值决定:1,sysctl_tcp_synack_retries,它可以通过proc文件系统的/proc/sys/net/ipv4/tcp_synack_retries节点来控制;2,icsk_syn_retries,它可以通过TCP_SYNCNT选项来设置;3,rskq_defer_accept,它可以通过TCP_DEFER_ACCEPT选项来设置。关于SYN+ACK定时器的启用与取消不多详说,我们直接看其对应的主要执行函数inet_csk_reqsk_queue_prune()。首先从下面代码可以看到重传计数取值的优先顺序为TCP_DEFER_ACCEPT选项 > TCP_SYNCNT选项 > proc文件系统设置(注意变量max_retries和thresh的取值,后面会谈到):

655:	Filename : \linux-3.4.4\net\ipv4\inet_connection_sock.c
656:	void inet_csk_reqsk_queue_prune(struct sock *parent,
657:	…
655:		int max_retries = icsk->icsk_syn_retries ? : sysctl_tcp_synack_retries;
656:		int thresh = max_retries;
657:	…
655:		if (queue->rskq_defer_accept)
656:			max_retries = queue->rskq_defer_accept;

接着是对Mini Socket重传YN+ACK数据包的超时判断,逻辑很简单,如果已超时,那么将丢弃该请求(虽然当前还只是半连接,代码575-576行);如果需要重传SYN+ACK数据包,那么就将执行rtx_syn_ack()回调函数(对应函数tcp_v4_rtx_synack()):

2365:	Filename : \linux-3.4.4\net\ipv4\inet_connection_sock.c
2366:					syn_ack_recalc(req, thresh, max_retries,
2367:						       queue->rskq_defer_accept,
2368:						       &expire, &resend);
2369:	…
2365:					if (!expire &&
2366:					    (!resend ||
2367:					     !req->rsk_ops->rtx_syn_ack(parent, req, NULL) ||
2368:					     inet_rsk(req)->acked)) {
2369:	…
2365:						continue;
2366:					}
2367:	
2368:					/* Drop this request */
2369:					inet_csk_reqsk_queue_unlink(parent, req, reqp);
2370:					reqsk_queue_removed(queue, req);

重要的是第555-557行代码,其计算当前Mini Socket是否已经超时(参数expire)以及是否需要再重传SYN+ACK数据包(参数resend);除了前面提到的重传SYN+ACK数据包的最大次数,还要结合其它条件才能判断当前Mini Socket是否超时,比如,如果当前Accept半连接队列里存在太多的陈旧连接(即有重传过SYN+ACK数据包的连接),那么就要将超时的阈值降低(参数thresh值更小),让这些陈旧连接提前结束以腾出空间接收后续新来的连接请求。看函数syn_ack_recalc()相关代码:

567:	 Filename : \linux-3.4.4\net\ipv4\inet_connection_sock.c
568:		*expire = req->retrans >= thresh &&
569:			  (!inet_rsk(req)->acked || req->retrans >= max_retries);
570:		/*
571:		 * Do not resend while waiting for data after ACK,
572:		 * start to resend on end of deferring period to give
573:		 * last chance for data or ACK to create established socket.
574:		 */
575:		*resend = !inet_rsk(req)->acked ||
576:			  req->retrans >= rskq_defer_accept - 1;

代码487-488行做超时判断,条件有多个,可以看到如果thresh(依赖TCP_SYNCNT选项和proc文件系统设置)大于max_retries(依赖TCP_DEFER_ACCEPT选项),那么即便是重传次数大于TCP_DEFER_ACCEPT选项设定值,仍可能不被判断为超时。再结合代码494-495行来看,此时也可能需要重传SYN+ACK数据包,也就是说,最终起决定作用的反而是TCP_SYNCNT选项和proc文件系统设置值,所以总体来讲,重传次数基本等于TCP_DEFER_ACCEPT选项(如果未设置就当0看待)和TCP_SYNCNT选项(如果未设置就取proc文件系统设置值,但还受当前网络环境影响,比如前面提到的陈旧连接太多)设置的最大值。

Socket选项系列完整Word文档:tcp socket option.rar

转载请保留地址:http://www.lenky.info/archives/2013/02/2219http://lenky.info/?p=2219


备注:如无特殊说明,文章内容均出自Lenky个人的真实理解而并非存心妄自揣测来故意愚人耳目。由于个人水平有限,虽力求内容正确无误,但仍然难免出错,请勿见怪,如果可以则请留言告之,并欢迎来讨论。另外值得说明的是,Lenky的部分文章以及部分内容参考借鉴了网络上各位网友的热心分享,特别是一些带有完全参考的文章,其后附带的链接内容也许更直接、更丰富,而我只是做了一下归纳&转述,在此也一并表示感谢。关于本站的所有技术文章,欢迎转载,但请遵从CC创作共享协议,而一些私人性质较强的心情随笔,建议不要转载。

法律:根据最新颁布的《信息网络传播权保护条例》,如果您认为本文章的任何内容侵犯了您的权利,请以Email或书面等方式告知,本站将及时删除相关内容或链接。

  1. kai
    2013年4月11日16:52 | #1

    分析的很不错,超过一定数目客户端才会被拒绝

  1. 本文目前尚无任何 trackbacks 和 pingbacks.