如果你想知道当前系统(运行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的无锁计数器,是优雅的做法,你至少可以应对别人说你引入原子变量锁总线的开销,是吧,哈哈。
当然,这些话并不是对经理说的。
浙江温州皮鞋湿,下雨进水不会胖。