Linux快速统计TCP半连接的数量

如果你想知道当前系统(运行Linux内核)中处在SYN RECV状态的TCP半连接数量的大小,最朴素的方法莫过如下:

netstat -antp|grep SYN_RECV|wc -l
ss -ant|grep SYN-RECV|wc -l

有什么问题吗?

方法朴素归朴素,但只能应对日常需求,如果遇到SYN Flood攻击,执行netstat/ss -ant命令需要 扫描遍历系统中所有的连接 ,然后再grep出半连接,在系统已经遭受攻击时,遍历操作无疑是雪上加霜!

你想如果上百万的半连接到达你的系统,会怎样?

然而,令人遗憾的是,系统中没有这样的关于SYN RECV状态的连接总量的之际统计值,至少我是没有找到,无论从ss -s还是从netstat -s,或者说snmp中,都没有找到这样的统计值。

那么如何在系统在面临攻击已经扛不住的情况下,快速统计半连接的数量,这无疑是一个很有意思的工作。

也许你会想到tcp_diag模块,但是它依然是遍历,只不过是采用更加复杂的netlink接口(研究的不多)。当我看inet_diag_dump_icsk函数时,我失望至极!所以,激动的心瞬间又沉了下去。


2020年第一个雷雨天,开了一下午会,饭也没吃一口,写下这篇文章。


在系统压力很大时,遍历所有的socket不现实, 但是LISTEN socket的数量是固定的 ,你见过系统的Listener超过1万的吗?不超过1万,遍历开销就不是事儿。

系统的Listener不会随着系统压力的增加而增加,它们分布在INET_LHTABLE_SIZE个hash桶中,每一个Listener都保存着自己的半连接计数,用其listen_sock的qlen字段计数,我们只需要把这些加起来就是结果了。

看来只有自己动手了。下面是代码:

#include <linux/module.h>
#include <net/tcp.h>

static unsigned int dump_syn_recv(void)
{
    unsigned int num = 0;
    int i;
    struct inet_hashinfo *hashinfo = &tcp_hashinfo;

    for (i = 0; i < INET_LHTABLE_SIZE; i++) {
        struct sock *sk;
        struct hlist_nulls_node *node;
        struct inet_listen_hashbucket *ilb;

        ilb = &hashinfo->listening_hash[i];
        spin_lock_bh(&ilb->lock);
        sk_nulls_for_each(sk, node, &ilb->head) {
            struct inet_connection_sock *icsk = inet_csk(sk);
            struct listen_sock *lopt;

            read_lock_bh(&icsk->icsk_accept_queue.syn_wait_lock);
            lopt = icsk->icsk_accept_queue.listen_opt;
            if (lopt && lopt->qlen)
                num += lopt->qlen;
            read_unlock_bh(&icsk->icsk_accept_queue.syn_wait_lock);
        }
        spin_unlock_bh(&ilb->lock);
    }
    return num;
}

static ssize_t dump_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)
{
    unsigned int num = dump_syn_recv(), n;
    char kbuf[16] = {0};

    if (*ppos != 0) {
        return 0;
    }

    n = snprintf(kbuf, 16, "%d\n", num);
    memcpy(ubuf, kbuf, n);
    *ppos += n;

    return n;
}

static struct file_operations dump_ops = {
    .owner = THIS_MODULE,
    .read = dump_read,
};

static struct proc_dir_entry *ent;
static int __init dumpstat_init(void)
{
    ent = proc_create("syn-recv-cnt", 0660, NULL, &dump_ops);
    if (!ent)
        return -1;

    return 0;
}

static void __exit dumpstat_exit(void)
{
    proc_remove(ent);
}

module_init(dumpstat_init);
module_exit(dumpstat_exit);
MODULE_LICENSE("GPL");

下面是一个模拟攻击时的测试结果:

# cat /proc/syn-recv-cnt
82719

也许有人会质疑这里面的两把锁,其实这种质疑是徒劳的:

  • ilb->lock是hash冲突桶的锁,对于Listener而言,冲突桶并不会很大。
  • icsk_accept_queue.syn_wait_lock是队列锁,它和热点函数互斥,但是互斥区间非常小。

由于这个dump操作基本上是一个oneshot的低频操作,且在Listener固定的情况下,它的时间复杂度是O(1)的,所以我觉得是OK的,这主要要感谢的是,内核自己做了lopt->qlen计数统计,我们只需要将每一个Listener的该字段累加起来就是了。

没有必要看见有锁操作就dis,这是一种因噎废食的偏见。

我不明白为什么tcp diag没有提供一个轻量级的如此这般的dump机制,也许是我没有注意到,不过随他去吧,反正我这个已经够轻量了。


netstat/ss工具好不好?当然好,但是它是常规意义上的好。同样的案例还有延伸到iptables/nftables,ebpf/xdp,如果精准化特定场景,还是需要自己动手来搞非通用方案。

为什么Linux内核为什么直到现在都没有一个计数器来统计半连接的数量?因为那是针对TCP的专有需求,Linux内核显然不是偏向于TCP的。无论如何,我觉得为其增加一些percpu的无锁计数器,是优雅的做法,你至少可以应对别人说你引入原子变量锁总线的开销,是吧,哈哈。

当然,这些话并不是对经理说的。


浙江温州皮鞋湿,下雨进水不会胖。

原文链接: https://blog.csdn.net/dog250/article/details/105013772

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    Linux快速统计TCP半连接的数量

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/406067

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年4月26日 上午9:36
下一篇 2023年4月26日 上午9:36

相关推荐