内核TCP的SYNCOOKIES

TCPIP协议 专栏收录该内容
105 篇文章 6 订阅

如下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
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值