内核TCP的SYNCOOKIES

如下PROC文件tcp_syncookies默认值为1,表明在套接口的SYN backlog队列溢出时,将开启SYNCOOKIES功能,抵御SYN泛洪攻击。如果tcp_syncookies设置为2,将会无条件的开启SYNCOOKIES功能。

$ cat /proc/sys/net/ipv4/tcp_syncookies
1
$ 
$ cat /proc/sys/net/ipv4/tcp_max_syn_backlog
128
$ 

syncookies开启

如下函数tcp_conn_request,在接收到客户端SYN请求报文之后,如果tcp_syncookies设置为2,或者SYN报文队列已满(tcp_max_syn_backlog),并且ISN等于0,是一个全新的TCP连接。此时,调用tcp_syn_flood_action函数判断一下是否需要开启syncookie功能。

int tcp_conn_request(struct request_sock_ops *rsk_ops,
             const struct tcp_request_sock_ops *af_ops, struct sock *sk, struct sk_buff *skb)
{
    struct tcp_fastopen_cookie foc = { .len = -1 };
    __u32 isn = TCP_SKB_CB(skb)->tcp_tw_isn;
    struct tcp_options_received tmp_opt;
	struct request_sock *req;
    bool want_cookie = false;

    /* TW buckets are converted to open requests without
     * limitations, they conserve resources and peer is evidently real one.
     */
    if ((net->ipv4.sysctl_tcp_syncookies == 2 || inet_csk_reqsk_queue_is_full(sk)) && !isn) {
        want_cookie = tcp_syn_flood_action(sk, skb, rsk_ops->slab_name);
        if (!want_cookie)
            goto drop;
    }

如果以上的判断确认要开启syncookies后,使用函数cookie_init_sequence计算初始序号ISN的值。

    if (want_cookie) {
        isn = cookie_init_sequence(af_ops, sk, skb, &req->mss);
        req->cookie_ts = tmp_opt.tstamp_ok;
        if (!tmp_opt.tstamp_ok)
            inet_rsk(req)->ecn_ok = 0;
    }

如果启用了syncookies,将客户端SYN报文中的一些信息保存在了序号中,就不需要保留此连接的request_sock结构了,在发送完SYN+ACK报文之后,将其释放,降低DDos攻击时的资源消耗。

    if (fastopen_sk) {
    } else {
        ...
        af_ops->send_synack(sk, dst, &fl, req, &foc,
                    !want_cookie ? TCP_SYNACK_NORMAL : TCP_SYNACK_COOKIE);
        if (want_cookie) {
            reqsk_free(req);
            return 0;
        }
    }

以下几节将分别介绍上面遇到的syncookies相关函数。

SYN泛洪动作

如下SYN Flood动作判断函数tcp_syn_flood_action,如果tcp_syncookies设置为0,将不启用syncookie,意味着将丢弃报文。否则,tcp_syncookies不为零,启用该功能。synflood_warned控制仅打印一次SYN泛洪警告。

static bool tcp_syn_flood_action(const struct sock *sk,
                 const struct sk_buff *skb, const char *proto)
{
    struct request_sock_queue *queue = &inet_csk(sk)->icsk_accept_queue;
    const char *msg = "Dropping request";
    bool want_cookie = false;
    struct net *net = sock_net(sk);

#ifdef CONFIG_SYN_COOKIES
    if (net->ipv4.sysctl_tcp_syncookies) {
        msg = "Sending cookies";
        want_cookie = true;
        __NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPREQQFULLDOCOOKIES);
    } else
#endif
        __NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPREQQFULLDROP);

    if (!queue->synflood_warned && net->ipv4.sysctl_tcp_syncookies != 2 &&
        xchg(&queue->synflood_warned, 1) == 0)
        net_info_ratelimited("%s: Possible SYN flooding on port %d. %s.  Check SNMP counters.\n",
                     proto, ntohs(tcp_hdr(skb)->dest), msg);

    return want_cookie;

syncookie生成序号

如下函数cookie_init_sequence,首先是调用函数tcp_synq_overflow记录下最近一次SYN队列溢出的时间戳。之后,调用协议注册的cookie初始化函数,对于IPv4,此为函数cookie_v4_init_sequence,而对于IPv6,为函数cookie_v6_init_sequence。

static inline __u32 cookie_init_sequence(const struct tcp_request_sock_ops *ops,
                     const struct sock *sk, struct sk_buff *skb, __u16 *mss)
{
    tcp_synq_overflow(sk);
    __NET_INC_STATS(sock_net(sk), LINUX_MIB_SYNCOOKIESSENT);
    return ops->cookie_init_seq(skb, mss);
}

函数tcp_synq_overflow记录最近syn队列溢出的时间戳,对于端口重用的套接口,将时间戳记录在sock_reuseport结构的synq_overflow_ts中。否则,将其记录在TCP接收选项结构的ts_recent_stamp成员中。每一秒做一次更新。

static inline void tcp_synq_overflow(const struct sock *sk)
{
    unsigned int last_overflow;
    unsigned int now = jiffies;

    if (sk->sk_reuseport) {
        struct sock_reuseport *reuse;

        reuse = rcu_dereference(sk->sk_reuseport_cb);
        if (likely(reuse)) {
            last_overflow = READ_ONCE(reuse->synq_overflow_ts);
            if (time_after32(now, last_overflow + HZ))
                WRITE_ONCE(reuse->synq_overflow_ts, now);
            return;
        }
    }

    last_overflow = tcp_sk(sk)->rx_opt.ts_recent_stamp;
    if (time_after32(now, last_overflow + HZ))
        tcp_sk(sk)->rx_opt.ts_recent_stamp = now;
}

与以上的函数tcp_synq_overflow对应,函数tcp_synq_no_recent_overflow负责在接收到ACK握手报文时,检查记录的SYN队列溢出时间戳与当前时间的差值,如果小于2分钟(TCP_SYNCOOKIE_VALID),则认为还在有效期内。

#define MAX_SYNCOOKIE_AGE   2
#define TCP_SYNCOOKIE_PERIOD    (60 * HZ)
#define TCP_SYNCOOKIE_VALID (MAX_SYNCOOKIE_AGE * TCP_SYNCOOKIE_PERIOD)
 
static inline bool tcp_synq_no_recent_overflow(const struct sock *sk)
{      
    unsigned int last_overflow;
    unsigned int now = jiffies;
   
    if (sk->sk_reuseport) {
        struct sock_reuseport *reuse;

        reuse = rcu_dereference(sk->sk_reuseport_cb);
        if (likely(reuse)) {
            last_overflow = READ_ONCE(reuse->synq_overflow_ts);
            return time_after32(now, last_overflow + TCP_SYNCOOKIE_VALID);
        }
    }

    last_overflow = tcp_sk(sk)->rx_opt.ts_recent_stamp;
    return time_after32(now, last_overflow + TCP_SYNCOOKIE_VALID);

以下为IPv4协议注册的syncookie初始化函数cookie_v4_init_sequence,主要功能由封装的函数__cookie_v4_init_sequence完成。

__u32 cookie_v4_init_sequence(const struct sk_buff *skb, __u16 *mssp)
{
    const struct iphdr *iph = ip_hdr(skb);
    const struct tcphdr *th = tcp_hdr(skb);

    return __cookie_v4_init_sequence(iph, th, mssp);
}
static const struct tcp_request_sock_ops tcp_request_sock_ipv4_ops = {
    ...
#ifdef CONFIG_SYN_COOKIES
    .cookie_init_seq =  cookie_v4_init_sequence,
#endif
    .init_ts_off    =   tcp_v4_init_ts_off,

以下为函数__cookie_v4_init_sequence,首先,将客户端的MSS值近似到msstab中最近的较小值,作为编码到cookie中的值,之后,调用secure_tcp_syn_cookie函数,根据报文中的源/目的IP地址,TCP源/目的端口号,TCP序号和MSS索引值,计算cookie。

static __u16 const msstab[] = {
    536,
    1300,
    1440,   /* 1440, 1452: PPPoE */
    1460,
};
u32 __cookie_v4_init_sequence(const struct iphdr *iph, const struct tcphdr *th, u16 *mssp)
{   
    int mssind; 
    const __u16 mss = *mssp;
    
    for (mssind = ARRAY_SIZE(msstab) - 1; mssind ; mssind--)
        if (mss >= msstab[mssind])
            break;
    *mssp = msstab[mssind];
    
    return secure_tcp_syn_cookie(iph->saddr, iph->daddr,
                     th->source, th->dest, ntohl(th->seq), mssind);

根据msstab代码中的注释,MSS值1460为最常用的通告值,概率在30%-46%之间,概率第二高的为1300-1349长度的MSS值,概率为15%-20%,而MSS为537-1299的概率小于1.5%,最后MSS值小于536的概率低于0.2%。此数据来自于S. Alcock 和 R. Nelson的论文’An Analysis of TCP Maximum Segement Sizes’。据此,msstab按需排列,由最高的概率开始检测。

ACK报文处理

在接收到客户端回复的ACK报文后,处理函数tcp_v4_do_rcv判断套接口处于TCP_LISTEN状态时,由于SYNCOOKIE开启,释放了TCP_NEW_SYN_RECV状态的请求套接口,故运行至此,调用tcp_v4_cookie_check检查报文中的cookie,并创建子套接口。返回值nsk与sk不相同,表明子套接口创建成功。

int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
    if (sk->sk_state == TCP_LISTEN) {
        struct sock *nsk = tcp_v4_cookie_check(sk, skb);

        if (!nsk)
            goto discard;
        if (nsk != sk) {
            if (tcp_child_process(sk, nsk, skb)) {
                rsk = nsk;
                goto reset;
            }
            return 0;

如下tcp_v4_cookie_check函数,对于SYNCOOKIE,仅处理ACK报文,所以要求SYN标志不能置位,调用子函数cookie_v4_check处理。

static struct sock *tcp_v4_cookie_check(struct sock *sk, struct sk_buff *skb)
{
#ifdef CONFIG_SYN_COOKIES
    const struct tcphdr *th = tcp_hdr(skb);

    if (!th->syn)
        sk = cookie_v4_check(sk, skb);
#endif
    return sk;

如下cookie_v4_check函数,首先,将报文中的确认序号ack_seq减去一,还原为本端原本的cookie值;之后,检查本地是否启用了SYNCOOKIES功能,如果报文的TCP头部没有设置ACK位,或者设置了RST未,不进行后续处理。函数tcp_synq_no_recent_overflow检查套接口记录的SYN队列溢出时间戳是否还在有效期内。

函数__cookie_v4_check对cookie进行验证,如果是正确的,返回值为编码的MSS。

struct sock *cookie_v4_check(struct sock *sk, struct sk_buff *skb)
{    
    __u32 cookie = ntohl(th->ack_seq) - 1;

    if (!sock_net(sk)->ipv4.sysctl_tcp_syncookies || !th->ack || th->rst)
        goto out;

    if (tcp_synq_no_recent_overflow(sk))
        goto out;

    mss = __cookie_v4_check(ip_hdr(skb), th, cookie);
    if (mss == 0) {
        __NET_INC_STATS(sock_net(sk), LINUX_MIB_SYNCOOKIESFAILED);
        goto out;
    }

    __NET_INC_STATS(sock_net(sk), LINUX_MIB_SYNCOOKIESRECV);

接下来,解析ACK报文中的TCP选项字段,如果存在TCP时间戳选项,并且内核启用了RFC1323定义的时间戳编码,使用cookie_timestamp_decode函数由时间戳编码中解析出客户端SYN报文指定的TCP选项。

由于在函数tcp_synack_options中(稍后介绍),将时间戳的tsval值增加了ts_off的值,这里做响应的减法。

    /* check for timestamp cookie support */
    memset(&tcp_opt, 0, sizeof(tcp_opt));
    tcp_parse_options(sock_net(sk), skb, &tcp_opt, 0, NULL);

    if (tcp_opt.saw_tstamp && tcp_opt.rcv_tsecr) {
        tsoff = secure_tcp_ts_off(sock_net(sk), ip_hdr(skb)->daddr, ip_hdr(skb)->saddr);
        tcp_opt.rcv_tsecr -= tsoff;
    }

    if (!cookie_timestamp_decode(sock_net(sk), &tcp_opt))
        goto out;

以下,正是分配请求套接口结构request_sock,并且初始化接收和发送的初始序号ISN,以及初始化相关TCP选项。之后,保存ACK报文中携带的IP选项,这里寄希望于ACK报文中的IP选项与最初的SYN报文中的IP选项相同。

    req = inet_reqsk_alloc(&tcp_request_sock_ops, sk, false); /* for safety */
    if (!req) goto out;

    ireq = inet_rsk(req);
    treq = tcp_rsk(req);
    treq->rcv_isn       = ntohl(th->seq) - 1;
    treq->snt_isn       = cookie;
    req->mss        = mss;
    ireq->snd_wscale    = tcp_opt.snd_wscale;
    ireq->sack_ok       = tcp_opt.sack_ok;
    ireq->wscale_ok     = tcp_opt.wscale_ok;
    ireq->tstamp_ok     = tcp_opt.saw_tstamp;

    /* We throwed the options of the initial SYN away, so we hope
     * the ACK carries the same options again (see RFC1122 4.2.3.8)
     */
    RCU_INIT_POINTER(ireq->ireq_opt, tcp_v4_save_options(sock_net(sk), skb));

以下将在此初始化本地的接收窗口,这里假定在发送SYNACK和接收客户端ACK之间,我们的接收窗口没有改变。与处理客户端SYN报文时调用的接收窗口函数tcp_openreq_init_rwin类似。

    flowi4_init_output(&fl4, ireq->ir_iif, ireq->ir_mark,
               RT_CONN_FLAGS(sk), RT_SCOPE_UNIVERSE, IPPROTO_TCP,
               inet_sk_flowi_flags(sk), opt->srr ? opt->faddr : ireq->ir_rmt_addr,
               ireq->ir_loc_addr, th->source, th->dest, sk->sk_uid);
    security_req_classify_flow(req, flowi4_to_flowi(&fl4));
    rt = ip_route_output_key(sock_net(sk), &fl4);

    req->rsk_window_clamp = tp->window_clamp ? :dst_metric(&rt->dst, RTAX_WINDOW);

    tcp_select_initial_window(sk, tcp_full_space(sk), req->mss,
                  &req->rsk_rcv_wnd, &req->rsk_window_clamp, ireq->wscale_ok, &rcv_wscale,
                  dst_metric(&rt->dst, RTAX_INITRWND));

    ireq->rcv_wscale  = rcv_wscale;
    ireq->ecn_ok = cookie_ecn_ok(&tcp_opt, sock_net(sk), &rt->dst);

最终,函数tcp_get_cookie_sock创建子套接口。

    ret = tcp_get_cookie_sock(sk, skb, req, &rt->dst, tsoff);
    /* ip_queue_xmit() depends on our flow being setup
     * Normal sockets get it right from inet_csk_route_child_sock()
     */
    if (ret)
        inet_sk(ret)->cork.fl.u.ip4 = fl4;
out:    return ret;

SYNCOOKIE算法

首先看一下cookie时间函数tcp_cookie_time,宏TCP_SYNCOOKIE_PERIOD的值为60秒(60*HZ),所以cookie时间的单位为60秒。

static inline u32 tcp_cookie_time(void)
{
    u64 val = get_jiffies_64();

    do_div(val, TCP_SYNCOOKIE_PERIOD);
    return val;

cookie的生成由函数secure_tcp_syn_cookie完成,其将32位序号分成两部分,前24位(COOKIEBITS)将编码客户端的序号以及cookie的时间值count,由以上函数tcp_cookie_time可知,count没一分钟增加一。后8位将编码MSS的索引值(msstab数组索引)。

#define COOKIEBITS 24   /* Upper bits store count */
#define COOKIEMASK (((__u32)1 << COOKIEBITS) - 1)

具体的,安全序号的生成由两次哈希完成,算法由siphash完成。

static __u32 secure_tcp_syn_cookie(__be32 saddr, __be32 daddr, __be16 sport, __be16 dport, __u32 sseq, __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.
     */
    u32 count = tcp_cookie_time();
    return (cookie_hash(saddr, daddr, sport, dport, 0, 0) +
        sseq + (count << COOKIEBITS) +
        ((cookie_hash(saddr, daddr, sport, dport, count, 1) + data)
         & COOKIEMASK));

cookie的检查工作由函数__cookie_v4_check完成,在接收到客户端的ACK报文后,将TCP的确认序号减去一即得到原有的cookie值,并且将ACK报文的序号减去一,即得到计算cookie时所使用的序号。具体的检查由函数check_tcp_syn_cookie完成,其返回cookie中编码的MSS索引值。

/* Check if a ack sequence number is a valid syncookie.
 * Return the decoded mss if it is, or 0 if not.
 */
int __cookie_v4_check(const struct iphdr *iph, const struct tcphdr *th, u32 cookie)
{
    __u32 seq = ntohl(th->seq) - 1;
    __u32 mssind = check_tcp_syn_cookie(cookie, iph->saddr, iph->daddr,
                        th->source, th->dest, seq);

    return mssind < ARRAY_SIZE(msstab) ? msstab[mssind] : 0;
}

cookie验证函数正是以上函数secure_tcp_syn_cookie的反向操作。分为两个步骤进行,第一检查编码在cookie中的count时间值,如果与当前系统的时间值相比大于2分钟(MAX_SYNCOOKIE_AGE),即认为超时出错。第二,记录编码的MSS索引值。

static __u32 check_tcp_syn_cookie(__u32 cookie, __be32 saddr, __be32 daddr, __be16 sport, __be16 dport, __u32 sseq)
{
    u32 diff, count = tcp_cookie_time();

    /* Strip away the layers from the cookie */
    cookie -= cookie_hash(saddr, daddr, sport, dport, 0, 0) + sseq;

    /* Cookie is now reduced to (count * 2^24) ^ (hash % 2^24) */
    diff = (count - (cookie >> COOKIEBITS)) & ((__u32) -1 >> COOKIEBITS);
    if (diff >= MAX_SYNCOOKIE_AGE)
        return (__u32)-1;

    return (cookie -
        cookie_hash(saddr, daddr, sport, dport, count - diff, 1))
        & COOKIEMASK;   /* Leaving the data behind */

TCP时间戳

在回复客户端的SYN报文时,函数tcp_make_synack判断SYN报文中是否携带了TCP的时间戳选项,即cookie_ts是否为真,为真的话,说明可以使其中的tsval字段,调用函数cookie_init_timestamp进行处理。

struct sk_buff *tcp_make_synack(const struct sock *sk, struct dst_entry *dst,
                struct request_sock *req,...)
{

    memset(&opts, 0, sizeof(opts));
#ifdef CONFIG_SYN_COOKIES
    if (unlikely(req->cookie_ts))
        skb->skb_mstamp_ns = cookie_init_timestamp(req);
    else
#endif

函数cookie_init_timestamp将客户端SYN报文中的TCP选项编码在时间戳选项中,方法是使用后六位(TSBITS)编码窗口扩展系数、SACK支持和ECN。此操作需要确保最终的时间戳小于当前时间戳,如果大于当前TCP时间戳,将当前时间戳由第7位开始,减去一。TCP以毫秒为单位递增时间戳,以上相当于减去了64毫秒。

u64 cookie_init_timestamp(struct request_sock *req)
{    
    struct inet_request_sock *ireq;
    u32 ts, ts_now = tcp_time_stamp_raw();
    u32 options = 0;

    ireq = inet_rsk(req);

    options = ireq->wscale_ok ? ireq->snd_wscale : TS_OPT_WSCALE_MASK;
    if (ireq->sack_ok) 
        options |= TS_OPT_SACK;
    if (ireq->ecn_ok)     
        options |= TS_OPT_ECN;

    ts = ts_now & ~TSMASK;
    ts |= options;
    if (ts > ts_now) {
        ts >>= TSBITS;
        ts--;
        ts <<= TSBITS;
        ts |= options;
    }
    return (u64)ts * (NSEC_PER_SEC / TCP_TS_HZ);

最终的时间戳tsval的值等于tcp_skb_timestamp函数的返回值,加上ts_off的值,而ts_off的值在接收到客户端SYN报文时,由函数secure_tcp_ts_off计算而得。

static inline u32 tcp_skb_timestamp(const struct sk_buff *skb)
{       
    return div_u64(skb->skb_mstamp_ns, NSEC_PER_SEC / TCP_TS_HZ);
} 
static unsigned int tcp_synack_options(..., struct request_sock *req,
                       unsigned int mss, struct sk_buff *skb,...)
{
    if (likely(ireq->tstamp_ok)) {
        opts->options |= OPTION_TS;
        opts->tsval = tcp_skb_timestamp(skb) + tcp_rsk(req)->ts_off;
        opts->tsecr = req->ts_recent;
        remaining -= TCPOLEN_TSTAMP_ALIGNED;
    }

如下函数secure_tcp_ts_off可见,ts_off的值由源和目的IP地址,以及秘钥值ts_secret进行哈希计算而得到。

u32 secure_tcp_ts_off(const struct net *net, __be32 saddr, __be32 daddr)
{                     
    if (net->ipv4.sysctl_tcp_timestamps != 1)
        return 0;
    
    ts_secret_init();
    return siphash_2u32((__force u32)saddr, (__force u32)daddr, &ts_secret);

内核需要打开tcp_timestamps功能,如下PROC文件所示,默认为开启状态。

$ cat /proc/sys/net/ipv4/tcp_timestamps
1

内核版本 5.0

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
TCP SYN Cookies 是一种防止SYN Flood攻击的机制,它在内核中的工作流程如下: 1. 当一个TCP SYN 请求到达内核时,内核会检查当前系统的连接队列是否已满。如果队列已满,则内核会使用 TCP SYN Cookies 机制。 2. 内核会将一部分 SYN 请求的 TCP 序列号和一些额外的信息进行哈希运算,生成一个唯一的 cookie 值,并将 cookie 值填充到 SYN/ACK 的 TCP 序列号字段中。 3. 当客户端收到带有 cookie 值的 SYN/ACK 消息时,它会将 cookie 值解码,并使用 cookie 值作为 TCP 序列号发送 ACK 消息。 4. 当服务端收到客户端的 ACK 消息时,它会再次对 cookie 值进行哈希运算,验证 cookie 值的有效性。如果验证通过,则将连接加入到连接队列中,并向客户端发送 ACK 消息。 在这个过程中,内核需要设置一些参数来控制 TCP SYN Cookies 机制的行为: 1. net.ipv4.tcp_syncookies:该参数用于控制内核是否启用 TCP SYN Cookies 机制。当该参数为 1 时,内核启用 TCP SYN Cookies 机制;当该参数为 0 时,内核关闭 TCP SYN Cookies 机制。 2. net.ipv4.tcp_synack_retries:该参数用于控制内核在等待客户端 ACK 消息的重试次数。当客户端未响应时,内核会重新发送 SYN/ACK 消息,并等待客户端的 ACK 消息。当重试次数超过该参数指定的次数时,内核会丢弃连接请求。 3. net.ipv4.tcp_syn_retries:该参数用于控制内核在等待客户端 SYN 消息的重试次数。当服务端未收到客户端的 SYN 消息时,内核会重新发送 SYN/ACK 消息,并等待客户端的 SYN 消息。当重试次数超过该参数指定的次数时,内核会丢弃连接请求。 4. net.ipv4.tcp_max_syn_backlog:该参数用于控制内核连接队列的最大长度。当连接队列已满时,内核会启用 TCP SYN Cookies 机制。 总之,TCP SYN Cookies 机制是一种有效的防止 SYN Flood 攻击的机制,它可以帮助内核在连接队列已满时仍能够正常处理客户端的连接请求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值