一、listen调用之backlog参数
这个系统调用中的第一个参数就是侦听的"父套接口",就好像进程fork时候的"父进程"一样,这个参数是必须的,我想大家应该都没有什么意见。但是后面还有一个容易被人们忽略的参数就是backlog,这个单词不是很常见,所以我第一看到它的时候是不明白它的意义和作用的,本着“好读书不求甚解”的原则,暂时把这个参数给放在一边了,但是后来觉得这个参数还是侦听套接口一个比较重要的参数,因为毕竟典型服务器端执行的系统调用流程本身也不是很多,所以这里补充讨论一下这个参数的意义。
牛津字典对这个单词的解释:“积压未办之事,积压的工作;没交付的订货”。
listen的man手册对这个参数的解释
The backlog argument defines the maximum length to which the queue of
pending connections for sockfd may grow. If a connection request
arrives when the queue is full, the client may receive an error with an
indication of ECONNREFUSED or, if the underlying protocol supports
retransmission, the request may be ignored so that a later reattempt at
connection succeeds.
但是毕竟都是官方表述,甚是笼统,还是看代码最为直接和清晰。
二、服务器可能面临的问题
1、三次握手
对于服务器来说,它一般是被动打开模式,也就是listen之后通过accept系统调用来接收一个远方的连接(有连接自远方来,不亦乐乎)。远端客户端发送SYNC之后进入SYNC SEND状态,服务器受到这个SYNC之后进入发送SYNC+ACK进入SYNC RCV状态,客户端回应ACK之后双方功德圆满,同时进入ESTABLISHED状态。
2、可能存在的问题
这是最为常见的,也就是大家都知道的三次握手,如果一切以简单的测试程序来运行,这个协议务实而严谨,但是在工程应用中却可能有问题。由于服务器在明处,而客户端或者可能的攻击者在暗处,所以服务器是一个容易受伤的存在。
①、大量并发客户端连接
假设说大量的客户端同时连接服务器,此时服务器肯定不能全部处理过来。具体有多大呢?可以考虑一下一个游戏的服务器,或者大家比较熟悉的春运时铁道部的订票系统。此时并不是每个客户端都能连接上的,因为带宽和服务器的符合都是有限的。
②、sync flood攻击
另一方面,一些攻击者可能不按套路出牌,在发送了SYN,接收到服务器的响应之后并不回应服务器,从而让服务器为这个连接创建的结构一直存在并等待这个ACK的到来,此时服务器可能就会被这个客户端欺骗,空等这个永远也不会到来的ACK,此时服务器系统资源限制,其它普通用户看到服务器不可用。
③、accept调用不及时
还有就是accept调用的影响。假设说一个客户端和服务器友好的完成了三次握手,那么通讯就会进入建立状态(ESTABLISHED),此时如果上层不调用accept或者来不及调用accept,此时这些底层连接同样会受到影响。因为三次握手是TCP协议栈内部的一个机制,上层API不需要也不能够进行干预。
三、内核三次握手实现正常流程
这个我记得在之前的一篇文章中讨论过,但是当时没有关注到这个backlog所起的作用,所以这里再简单补充一下,这里只讨论服务器端对于这个三次握手的执行流程和套路。
1、接收到客户端的第一个SYN连接
tcp_v4_rcv--->>>tcp_v4_do_rcv--->>>tcp_rcv_state_process--->>tcp_v4_conn_request--->>>tcp_v4_send_synack
当执行到这里之后,服务器就已经响应了客户端的一个SYN连接,并且回应了ACK+SYN,进而等待客户端回应ACK之后进入链路建立状态。但是服务器要记住这个连接,因为可能有多个客户端同时连接这个服务器,所以服务器要对不同的客户端进行区分,区分的办法就是为这个连接分配一个request_sock结构,并且将这个连接请求对应的结构挂入这个侦听套接口的请求队列中,对应代码为tcp_v4_conn_request函数中的inet_csk_reqsk_queue_hash_add(sk, req, TCP_TIMEOUT_INIT)。
2、接收到客户端的ACK
之前的调用链和之前相同,但是在tcp_v4_do_rcv函数中会出现和上次不同的现象,在该函数中:
if (sk->sk_state == TCP_LISTEN) {
struct sock *nsk = tcp_v4_hnd_req(sk, skb);当ACK到来时,该函数返回的nsk(new sk)将会满足下面的 if (nsk != sk)判断。
if (!nsk)
goto discard;
if (nsk != sk) {
if (tcp_child_process(sk, nsk, skb)) {当客户端的ACK到来的时候服务器进入该分支。
rsk = nsk;
goto reset;
}
return 0;
}
}
可以注意到tcp_v4_do_rcv--->>tcp_v4_hnd_req---->>>inet_csk_search_req中使用的icsk->icsk_accept_queue.listen_opt是和第一次SYNC到来是通过inet_csk_reqsk_queue_hash_add--->>>reqsk_queue_hash_req操作的是同一个队列,所以客户端ACK到来的时候能够从这表中找到前一个连接项。
struct request_sock *inet_csk_search_req(const struct sock *sk,
struct request_sock ***prevp,
const __be16 rport, const __be32 raddr,
const __be32 laddr)
{
const struct inet_connection_sock *icsk = inet_csk(sk);
struct listen_sock *lopt = icsk->icsk_accept_queue.listen_opt;
struct request_sock *req, **prev;
for (prev = &lopt->syn_table[inet_synq_hash(raddr, rport, lopt->hash_rnd,
lopt->nr_table_entries)];
真正的子套接口的创建流程为:
tcp_v4_hnd_req--->>>tcp_check_req--->>>tcp_v4_syn_recv_sock--->>tcp_create_openreq_child--->>inet_csk_clone--->>>sk_clone--->>>sk_alloc
四、backlog的作用
前一节都是正常流程,所以backlog的作用并没有体现出来,现在考虑一下客户端并发连接比较多的情况。
1、listen中backlog的作用
sys_listen--->>>sys_listen--->>>inet_listen{sk->sk_max_ack_backlog = backlog}--->>>inet_listen--->>>inet_csk_listen_start--->>reqsk_queue_alloc
int reqsk_queue_alloc(struct request_sock_queue *queue,
unsigned int nr_table_entries)
{
size_t lopt_size = sizeof(struct listen_sock);
struct listen_sock *lopt;
nr_table_entries = min_t(u32, nr_table_entries, sysctl_max_syn_backlog);
nr_table_entries = max_t(u32, nr_table_entries, 8);
nr_table_entries = roundup_pow_of_two(nr_table_entries + 1);
……
for (lopt->max_qlen_log = 3;
(1 << lopt->max_qlen_log) < nr_table_entries;
lopt->max_qlen_log++);
……
lopt->nr_table_entries = nr_table_entries;
由于其中的sysctl_max_syn_backlog 默认值为256,所以这个具体的侦听数目就是在8到256之间的一个值,并且包含两个值。如果listen传入的backlog值在这个区间中,,那么就用系统传入值,否则分别取和8和256最接近的那个值。例如1000被修改为256,4被设置为8。但是之后还有一个对2的幂取整,也就是向上对2幂数取整,例如5被取整为8,10被取整为16,这个值将会决定lopt->nr_table_entries的值。
2、当连接过多时
当第一次连接到来的时候,执行到tcp_v4_conn_request函数时会首先进行这样的判断
if (inet_csk_reqsk_queue_is_full(sk) && !isn) {
#ifdef CONFIG_SYN_COOKIES
if (sysctl_tcp_syncookies) {
want_cookie = 1;
} else
#endif
goto drop;
}
其中的inet_csk_reqsk_queue_is_full判断最终执行的数值比较为
static inline int reqsk_queue_is_full(const struct request_sock_queue *queue)
{
return queue->listen_opt->qlen >> queue->listen_opt->max_qlen_log;
}
①、qlen变量
这里使用了listen_opt中的qlen变量,它的意义是现在系统中收了客户端SYN,但是还没有收到ACK报文的连接数目。这个值在tcp_v4_conn_request--->>>inet_csk_reqsk_queue_hash_add--->>>inet_csk_reqsk_queue_added-->>reqsk_queue_added
static inline int reqsk_queue_added(struct request_sock_queue *queue)
{
struct listen_sock *lopt = queue->listen_opt;
const int prev_qlen = lopt->qlen;
lopt->qlen_young++;
lopt->qlen++;
return prev_qlen;
}
并且在tcp_check_req---->>>inet_csk_reqsk_queue_removed--->>reqsk_queue_removed
static inline int reqsk_queue_removed(struct request_sock_queue *queue,
struct request_sock *req)
{
struct listen_sock *lopt = queue->listen_opt;
if (req->retrans == 0)
--lopt->qlen_young;
return --lopt->qlen;
}
也就是说,当收到第一个SYN的时候,这个值增加,在收到客户端对SYN的ACK之后会递减这个值,所以在正常情况下这个值是比较小的,能够比较迅速的下降到零。
②、sk_ack_backlog变量
tcp_v4_conn_request函数中在qlen判断之后还有一个判断:
/* Accept backlog is full. If we have already queued enough
* of warm entries in syn queue, drop request. It is better than
* clogging syn queue with openreqs with exponentially increasing
* timeout.
*/
if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1)
goto drop;
其中
static inline int sk_acceptq_is_full(struct sock *sk)
{
return sk->sk_ack_backlog > sk->sk_max_ack_backlog;
}
该变量表示的就是已经完成三次握手,但是还没有被accept接收的套接字,它的限制来自和sk->sk_max_ack_backlog变量的比较,而这个变量在sys_listen-->>>inet_listen函数中被赋值为listen的backlog参数
sk->sk_max_ack_backlog = backlog;
③qlen_young变量
在tcp_v4_conn_request--->>inet_csk_reqsk_queue_added--->>>reqsk_queue_len_young中同样使用了qlen_young变量,该变量时用来统计这个侦听中那些没有进行过SYN+ACK重传的套接口,服务器在收到客户端的SYN之后会发送SYN+ACK回应,但是这个报文可能丢失,所以服务器就会尝试重传,但是服务器一旦重传一个连接的回应,那么这个连接就不再年轻,成为mature,而收到一个客户端SYN的时候这个连接是出于young状态的,这个值在tcp_keepalive_timer--->>tcp_synack_timer--->>>inet_csk_reqsk_queue_prune中递减
五、服务器端accept套接口何时从SYN RCV转换为ESTABLISHED
在之前说的子套接口创建的过程中,里面说到真正的子套接口创建的调用链为
tcp_v4_hnd_req--->>>tcp_check_req--->>>tcp_v4_syn_recv_sock--->>tcp_create_openreq_child--->>inet_csk_clone,但是这个函数中分明是把新创建的套接口设置为了SYN RCV状态而不是ESTABLISHED状态,而这个状态的转换则是在
tcp_v4_do_rcv--->>tcp_child_process--->>tcp_rcv_state_process---->>>tcp_rcv_synsent_state_process
smp_mb();
tcp_set_state(sk, TCP_ESTABLISHED);
security_inet_conn_established(sk, skb);
六、syn flood 攻击原理
现在假设说有人进行syn flood的攻击,它只需要使用一台电脑,不断的向服务器发送SYN报文,当服务器返回SYN+ACK报文时,这个攻击者并不响应这个报文,从而服务器会尝试重传这个SYN+ACK报文(默认持续三分钟)。在这段时间内,服务器的request_sock_queue.listen_opt->qlen将会大于backlog中设置的数值,从而在四、2节中描述的tcp_v4_conn_request函数内inet_csk_reqsk_queue_is_full(判断处丢弃报文,所以其它用户的连接请求被丢弃,此时就会出现DOS(Deial-Of-Service)现象。
if (inet_csk_reqsk_queue_is_full(sk) && !isn) {
#ifdef CONFIG_SYN_COOKIES
if (sysctl_tcp_syncookies) {
want_cookie = 1;
} else
#endif
goto drop;
}
七、todo
当前Linux中对于syn flood攻击的防范措施。