网络协议栈(13)syn flood攻击防范及部分重传定时器参数分析
一、syn cookie攻击防御方法
在前一篇文章中说明了syn flood的原理,可以看到,该机制会造成服务器的DOS瘫痪而无法提供正常服务,所以在当前的Linux中提供了一种相对比较智能的方法方法,就是使用syn_cookie机制。它的实现原理就是把连接的状态信息体现在自己提议的初始化序列号上,这个状态信息一定需要包含当前的时间信息,这样一方面可以保持序列号在合理长的时间内不会出现回绕,另一方面,这个时间也可以判断连接发起方的最后一个报文是否合法。这个和原始实现的最大区别在于这种机制不会在连接被动方使用数据结构来记录这个三次握手的前两次交互,而是把第三次交互放在报文的序列号中,这样当三次握手最后一个报文到来时直接进入连接建立状态。
二、当连接backlog用完时
当backlog用完时,此时又有新的连接请求到来,同样是进入tcp_v4_conn_request函数,在该函数中进行了额外处理:
/* TW buckets are converted to open requests without
* limitations, they conserve resources and peer is
* evidently real one.
*/
if (inet_csk_reqsk_queue_is_full(sk) && !isn) {
#ifdef CONFIG_SYN_COOKIES
if (sysctl_tcp_syncookies) {
want_cookie = 1;
} else
#endif
goto drop;
}
if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1)如果用户来不及accept,即使配置了syn cookie,同样丢失请求。
goto drop;
……
if (want_cookie) {
#ifdef CONFIG_SYN_COOKIES
syn_flood_warning(skb);
#endif
isn = cookie_v4_init_sequence(sk, skb, &req->mss); 该步骤需要完成自己初始序列号的精心选择。
}
……
if (want_cookie) {
reqsk_free(req); 此时连接请求结构被释放,而握手的前两个报文信息(完成情况)被保存到精心选择的初始序列号中,然后在收到最后一个确认报文之后还原并验证三次握手是否完成。
} else {
inet_csk_reqsk_queue_hash_add(sk, req, TCP_TIMEOUT_INIT);
}
return 0;
三、syn+ack报文序列号选择函数
#define COOKIEBITS 24 /* Upper bits store count */
#define COOKIEMASK (((__u32)1 << COOKIEBITS) - 1)
__u32 cookie_v4_init_sequence(struct sock *sk, struct sk_buff *skb, __u16 *mssp)
{
struct tcp_sock *tp = tcp_sk(sk);
int mssind;
const __u16 mss = *mssp;
tp->last_synq_overflow = jiffies;
/* XXX sort msstab[] by probability? Binary search? */
for (mssind = 0; mss > msstab[mssind + 1]; mssind++)
;
*mssp = msstab[mssind] + 1;
NET_INC_STATS_BH(LINUX_MIB_SYNCOOKIESSENT);
return secure_tcp_syn_cookie(skb->nh.iph->saddr, skb->nh.iph->daddr,
skb->h.th->source, skb->h.th->dest,
ntohl(skb->h.th->seq),
jiffies / (HZ * 60), mssind);
}
static __u32 secure_tcp_syn_cookie(__be32 saddr, __be32 daddr, __be16 sport,
__be16 dport, __u32 sseq, __u32 count,
__u32 data)
{
/*
* Compute the secure sequence number.
* The output should be:
* HASH(sec1,saddr,sport,daddr,dport,sec1) + sseq + (count * 2^24)
* + (HASH(sec2,saddr,sport,daddr,dport,count,sec2) % 2^24).
* Where sseq is their sequence number and count increases every
* minute by 1.
* As an extra hack, we add a small "data" value that encodes the
* MSS into the second hash value.
*/
return (cookie_hash(saddr, daddr, sport, dport, 0, 0) + 常量数值使用不可逆hash算法,这些值在下个ack报文中保持不变。
sseq + (count << COOKIEBITS) + 其中sseq 包含对方提议序列号,count表示一个由时间引入的合理盐值,存入最高8bit。
((cookie_hash(saddr, daddr, sport, dport, count, 1) + data)一些随机值放入低24bits
& COOKIEMASK));
}
四、三次握手最后一个ack到来
static struct sock *tcp_v4_hnd_req(struct sock *sk, struct sk_buff *skb)
……
#ifdef CONFIG_SYN_COOKIES
if (!th->rst && !th->syn && th->ack) 这个是三次握手最后一个报文特征ack置位,syn没有置位,并且重要的是父套接口为listen状态。
sk = cookie_v4_check(sk, skb, &(IPCB(skb)->opt)); 此时需要用这个响应报文还原syn+ack报文信息并完成验证。
#endif
真正的检测函数为
static __u32 check_tcp_syn_cookie(__u32 cookie, __be32 saddr, __be32 daddr,
__be16 sport, __be16 dport, __u32 sseq,
__u32 count, __u32 maxdiff)
{
__u32 diff;
/* Strip away the layers from the cookie */
cookie -= cookie_hash(saddr, daddr, sport, dport, 0, 0) + sseq;根据secure_tcp_syn_cookie的计算方法,该步骤之后cookie的值等于(count << COOKIEBITS) +((cookie_hash(saddr, daddr, sport, dport, count, 1) + data) & COOKIEMASK),也就是高8bits为count值,而低24位则为一个单向hash值加上一个data。而其中的count就是系统时间相关的一个变量,这样如果这个ack报文不是在合理时间内的一个响应,同样认为非法连接。
/* Cookie is now reduced to (count * 2^24) ^ (hash % 2^24) */
diff = (count - (cookie >> COOKIEBITS)) & ((__u32) - 1 >> COOKIEBITS);这里count为接收到三次握手最后一个ack是系统时间,以分钟为单位。由于maxdiff值默认为4,所以4分钟之内的ack认为有效。
if (diff >= maxdiff)
return (__u32)-1;
return (cookie -
cookie_hash(saddr, daddr, sport, dport, count - diff, 1))
& COOKIEMASK; /* Leaving the data behind */还原出连接请求报文提议的mss并返回给用户。
}
如果这些检测通过,tcp_v4_hnd_req--->>>cookie_v4_check同样会返回连接成功的socket三次握手套接字。
五、syn、synack及建链之后重传
这两个参数是三次握手中比较重要的两个参数,其中第一个参数是连接发起方对syn发送的重试次数、后一个是连接被动方恢复synack的时间相关,当然还有一个建链完成之后的重试时间。一下将以系统默认TCP_TIMEOUT_INIT计算RTO,该值为
#define TCP_TIMEOUT_INIT ((unsigned)(3*HZ)) /* RFC 1122 initial RTO value */
也就是3秒钟。最大时间为120秒
#define TCP_RTO_MAX ((unsigned)(120*HZ))
1、syn次数及时间计算
sysctl_tcp_syn_retries该值控制连接发起套接口的重试次数,其默认值为TCP_SYN_RETRIES=5。这个值在
/* A write timeout has occurred. Process the after effects. */
static int tcp_write_timeout(struct sock *sk)
{
……
if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
if (icsk->icsk_retransmits)
dst_negative_advice(&sk->sk_dst_cache);
retry_until = icsk->icsk_syn_retries ? : sysctl_tcp_syn_retries;
}
我们看一下,如果对方一直不给这个syn回应,这里的syn重试时间。第一次超时之后
static void tcp_retransmit_timer(struct sock *sk)
……
if (tcp_write_timeout(sk))如果这里判断出时间超时,则不会重传。
goto out;
……
icsk->icsk_backoff++;
icsk->icsk_retransmits++;
out_reset_timer:
icsk->icsk_rto = min(icsk->icsk_rto << 1, TCP_RTO_MAX);不断倍增超时时间,但是不能大于120秒。
inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, icsk->icsk_rto, TCP_RTO_MAX);
现在我们想一下边界值5,到底是进行4次还是5次重传。当第一次重传之后icsk_retransmits的值为1,当第四次重传之后该值为4,当第五次到来时,进入tcp_write_timeout函数后
if (icsk->icsk_retransmits >= retry_until) 还是不满足的,所以第五次重传依然会进行。所以总共会尝试六次,包括第一次正常发送。
这个时间为
3+3*2+3*4+……+3*2^5=3*(2^6-1)=189
这也就是
#define TCP_SYN_RETRIES 5 /* number of times to retry active opening a
* connection: ~180sec is RFC minimum */
注释中说的~180sec的由来。
2、synack重传次数及时间
这个同样是使用一个系统变量控制次数,该值在keepalive定时器中对请求队列的修剪中触发:
void inet_csk_reqsk_queue_prune(struct sock *parent,
const unsigned long interval,
const unsigned long timeout,
const unsigned long max_rto)
{
struct inet_connection_sock *icsk = inet_csk(parent);
struct request_sock_queue *queue = &icsk->icsk_accept_queue;
struct listen_sock *lopt = queue->listen_opt;
int max_retries = icsk->icsk_syn_retries ? : sysctl_tcp_synack_retries;
……
do {
reqp=&lopt->syn_table[i];
while ((req = *reqp) != NULL) {
if (time_after_eq(now, req->expires)) {
if ((req->retrans < thresh ||
(inet_rsk(req)->acked && req->retrans < max_retries))
&& !req->rsk_ops->rtx_syn_ack(parent, req, NULL)) {
unsigned long timeo;
if (req->retrans++ == 0)
lopt->qlen_young--;
timeo = min((timeout << req->retrans), max_rto);这里的计算方法和之前相似,注意的一点是,前面的判断已经递增了retrans的值,所以第一次重传就已经倍增超时时间。
req->expires = now + timeo;
reqp = &req->dl_next;
continue;
}
/* Drop this request */
inet_csk_reqsk_queue_unlink(parent, req, reqp);
reqsk_queue_removed(queue, req);
reqsk_free(req);
continue;
}
reqp = &req->dl_next;
}
i = (i + 1) & (lopt->nr_table_entries - 1);
} while (--budget > 0);
该时间同样是189秒,计算过程同上。
3、链路建立之后重传时间
#define TCP_RETR2 15 /*
* This should take at least
* 90 minutes to time out.
* RFC1122 says that the limit is 100 sec.
* 15 is ~13-30min depending on RTO.
*/
这个时间比较长,计算方法和之前有些不同,因为第六次重传时3*2^6=172,该值已经大于TCP_RTO_MAX120,所以其总共时间为
3*(2^6-1)+ 120*(15-5) = 189+1200=1389s=23分9秒。
在前一篇文章中说明了syn flood的原理,可以看到,该机制会造成服务器的DOS瘫痪而无法提供正常服务,所以在当前的Linux中提供了一种相对比较智能的方法方法,就是使用syn_cookie机制。它的实现原理就是把连接的状态信息体现在自己提议的初始化序列号上,这个状态信息一定需要包含当前的时间信息,这样一方面可以保持序列号在合理长的时间内不会出现回绕,另一方面,这个时间也可以判断连接发起方的最后一个报文是否合法。这个和原始实现的最大区别在于这种机制不会在连接被动方使用数据结构来记录这个三次握手的前两次交互,而是把第三次交互放在报文的序列号中,这样当三次握手最后一个报文到来时直接进入连接建立状态。
二、当连接backlog用完时
当backlog用完时,此时又有新的连接请求到来,同样是进入tcp_v4_conn_request函数,在该函数中进行了额外处理:
/* TW buckets are converted to open requests without
* limitations, they conserve resources and peer is
* evidently real one.
*/
if (inet_csk_reqsk_queue_is_full(sk) && !isn) {
#ifdef CONFIG_SYN_COOKIES
if (sysctl_tcp_syncookies) {
want_cookie = 1;
} else
#endif
goto drop;
}
if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1)如果用户来不及accept,即使配置了syn cookie,同样丢失请求。
goto drop;
……
if (want_cookie) {
#ifdef CONFIG_SYN_COOKIES
syn_flood_warning(skb);
#endif
isn = cookie_v4_init_sequence(sk, skb, &req->mss); 该步骤需要完成自己初始序列号的精心选择。
}
……
if (want_cookie) {
reqsk_free(req); 此时连接请求结构被释放,而握手的前两个报文信息(完成情况)被保存到精心选择的初始序列号中,然后在收到最后一个确认报文之后还原并验证三次握手是否完成。
} else {
inet_csk_reqsk_queue_hash_add(sk, req, TCP_TIMEOUT_INIT);
}
return 0;
三、syn+ack报文序列号选择函数
#define COOKIEBITS 24 /* Upper bits store count */
#define COOKIEMASK (((__u32)1 << COOKIEBITS) - 1)
__u32 cookie_v4_init_sequence(struct sock *sk, struct sk_buff *skb, __u16 *mssp)
{
struct tcp_sock *tp = tcp_sk(sk);
int mssind;
const __u16 mss = *mssp;
tp->last_synq_overflow = jiffies;
/* XXX sort msstab[] by probability? Binary search? */
for (mssind = 0; mss > msstab[mssind + 1]; mssind++)
;
*mssp = msstab[mssind] + 1;
NET_INC_STATS_BH(LINUX_MIB_SYNCOOKIESSENT);
return secure_tcp_syn_cookie(skb->nh.iph->saddr, skb->nh.iph->daddr,
skb->h.th->source, skb->h.th->dest,
ntohl(skb->h.th->seq),
jiffies / (HZ * 60), mssind);
}
static __u32 secure_tcp_syn_cookie(__be32 saddr, __be32 daddr, __be16 sport,
__be16 dport, __u32 sseq, __u32 count,
__u32 data)
{
/*
* Compute the secure sequence number.
* The output should be:
* HASH(sec1,saddr,sport,daddr,dport,sec1) + sseq + (count * 2^24)
* + (HASH(sec2,saddr,sport,daddr,dport,count,sec2) % 2^24).
* Where sseq is their sequence number and count increases every
* minute by 1.
* As an extra hack, we add a small "data" value that encodes the
* MSS into the second hash value.
*/
return (cookie_hash(saddr, daddr, sport, dport, 0, 0) + 常量数值使用不可逆hash算法,这些值在下个ack报文中保持不变。
sseq + (count << COOKIEBITS) + 其中sseq 包含对方提议序列号,count表示一个由时间引入的合理盐值,存入最高8bit。
((cookie_hash(saddr, daddr, sport, dport, count, 1) + data)一些随机值放入低24bits
& COOKIEMASK));
}
四、三次握手最后一个ack到来
static struct sock *tcp_v4_hnd_req(struct sock *sk, struct sk_buff *skb)
……
#ifdef CONFIG_SYN_COOKIES
if (!th->rst && !th->syn && th->ack) 这个是三次握手最后一个报文特征ack置位,syn没有置位,并且重要的是父套接口为listen状态。
sk = cookie_v4_check(sk, skb, &(IPCB(skb)->opt)); 此时需要用这个响应报文还原syn+ack报文信息并完成验证。
#endif
真正的检测函数为
static __u32 check_tcp_syn_cookie(__u32 cookie, __be32 saddr, __be32 daddr,
__be16 sport, __be16 dport, __u32 sseq,
__u32 count, __u32 maxdiff)
{
__u32 diff;
/* Strip away the layers from the cookie */
cookie -= cookie_hash(saddr, daddr, sport, dport, 0, 0) + sseq;根据secure_tcp_syn_cookie的计算方法,该步骤之后cookie的值等于(count << COOKIEBITS) +((cookie_hash(saddr, daddr, sport, dport, count, 1) + data) & COOKIEMASK),也就是高8bits为count值,而低24位则为一个单向hash值加上一个data。而其中的count就是系统时间相关的一个变量,这样如果这个ack报文不是在合理时间内的一个响应,同样认为非法连接。
/* Cookie is now reduced to (count * 2^24) ^ (hash % 2^24) */
diff = (count - (cookie >> COOKIEBITS)) & ((__u32) - 1 >> COOKIEBITS);这里count为接收到三次握手最后一个ack是系统时间,以分钟为单位。由于maxdiff值默认为4,所以4分钟之内的ack认为有效。
if (diff >= maxdiff)
return (__u32)-1;
return (cookie -
cookie_hash(saddr, daddr, sport, dport, count - diff, 1))
& COOKIEMASK; /* Leaving the data behind */还原出连接请求报文提议的mss并返回给用户。
}
如果这些检测通过,tcp_v4_hnd_req--->>>cookie_v4_check同样会返回连接成功的socket三次握手套接字。
五、syn、synack及建链之后重传
这两个参数是三次握手中比较重要的两个参数,其中第一个参数是连接发起方对syn发送的重试次数、后一个是连接被动方恢复synack的时间相关,当然还有一个建链完成之后的重试时间。一下将以系统默认TCP_TIMEOUT_INIT计算RTO,该值为
#define TCP_TIMEOUT_INIT ((unsigned)(3*HZ)) /* RFC 1122 initial RTO value */
也就是3秒钟。最大时间为120秒
#define TCP_RTO_MAX ((unsigned)(120*HZ))
1、syn次数及时间计算
sysctl_tcp_syn_retries该值控制连接发起套接口的重试次数,其默认值为TCP_SYN_RETRIES=5。这个值在
/* A write timeout has occurred. Process the after effects. */
static int tcp_write_timeout(struct sock *sk)
{
……
if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
if (icsk->icsk_retransmits)
dst_negative_advice(&sk->sk_dst_cache);
retry_until = icsk->icsk_syn_retries ? : sysctl_tcp_syn_retries;
}
我们看一下,如果对方一直不给这个syn回应,这里的syn重试时间。第一次超时之后
static void tcp_retransmit_timer(struct sock *sk)
……
if (tcp_write_timeout(sk))如果这里判断出时间超时,则不会重传。
goto out;
……
icsk->icsk_backoff++;
icsk->icsk_retransmits++;
out_reset_timer:
icsk->icsk_rto = min(icsk->icsk_rto << 1, TCP_RTO_MAX);不断倍增超时时间,但是不能大于120秒。
inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, icsk->icsk_rto, TCP_RTO_MAX);
现在我们想一下边界值5,到底是进行4次还是5次重传。当第一次重传之后icsk_retransmits的值为1,当第四次重传之后该值为4,当第五次到来时,进入tcp_write_timeout函数后
if (icsk->icsk_retransmits >= retry_until) 还是不满足的,所以第五次重传依然会进行。所以总共会尝试六次,包括第一次正常发送。
这个时间为
3+3*2+3*4+……+3*2^5=3*(2^6-1)=189
这也就是
#define TCP_SYN_RETRIES 5 /* number of times to retry active opening a
* connection: ~180sec is RFC minimum */
注释中说的~180sec的由来。
2、synack重传次数及时间
这个同样是使用一个系统变量控制次数,该值在keepalive定时器中对请求队列的修剪中触发:
void inet_csk_reqsk_queue_prune(struct sock *parent,
const unsigned long interval,
const unsigned long timeout,
const unsigned long max_rto)
{
struct inet_connection_sock *icsk = inet_csk(parent);
struct request_sock_queue *queue = &icsk->icsk_accept_queue;
struct listen_sock *lopt = queue->listen_opt;
int max_retries = icsk->icsk_syn_retries ? : sysctl_tcp_synack_retries;
……
do {
reqp=&lopt->syn_table[i];
while ((req = *reqp) != NULL) {
if (time_after_eq(now, req->expires)) {
if ((req->retrans < thresh ||
(inet_rsk(req)->acked && req->retrans < max_retries))
&& !req->rsk_ops->rtx_syn_ack(parent, req, NULL)) {
unsigned long timeo;
if (req->retrans++ == 0)
lopt->qlen_young--;
timeo = min((timeout << req->retrans), max_rto);这里的计算方法和之前相似,注意的一点是,前面的判断已经递增了retrans的值,所以第一次重传就已经倍增超时时间。
req->expires = now + timeo;
reqp = &req->dl_next;
continue;
}
/* Drop this request */
inet_csk_reqsk_queue_unlink(parent, req, reqp);
reqsk_queue_removed(queue, req);
reqsk_free(req);
continue;
}
reqp = &req->dl_next;
}
i = (i + 1) & (lopt->nr_table_entries - 1);
} while (--budget > 0);
该时间同样是189秒,计算过程同上。
3、链路建立之后重传时间
#define TCP_RETR2 15 /*
* This should take at least
* 90 minutes to time out.
* RFC1122 says that the limit is 100 sec.
* 15 is ~13-30min depending on RTO.
*/
这个时间比较长,计算方法和之前有些不同,因为第六次重传时3*2^6=172,该值已经大于TCP_RTO_MAX120,所以其总共时间为
3*(2^6-1)+ 120*(15-5) = 189+1200=1389s=23分9秒。