创建半链接时的三个长度检查
在处理TCP-SYN首包时候, tcp_conn_request函数里, 会有三个不同条件的长度检查。
inet_csk_reqsk_queue_is_full半链接的个数超过sk_max_ack_backlog, 则丢包。sk_acceptq_is_full: accept 队列长度超过sk_max_ack_backlog,则丢包。sysctl_tcp_syncookies禁用(值为0)时,sysctl_max_syn_backlog与inet_csk_reqsk_queue_len: 队列长度如果超过sysctl_max_syn_backlog的3/4则丢包
其中,
sysctl_max_syn_backlog: 初始化时,最小 128。如果ehash_entries/128比128大,取最大值。ehash_entries是 tcp 的 hash 桶个数。sysctl_tcp_syncookies: 初始值为 1
判断1: 半链接队列是否溢出 inet_csk_reqsk_queue_is_full
虽然不再维护半链接队列了, 但是每次创建req socket后,这个统计值都是在增加的。
因此如果半链接个数超过了最大值sk_max_ack_backlog,则启用cookie(sysctl_tcp_syncookies为1或2),如果不支持cookie,则丢弃。
1 | 278 static inline int inet_csk_reqsk_queue_len(const struct sock *sk) |
当前内核默认启用syncookie机制(sysctl_tcp_syncookies为1),队列溢出会触发synccookie 机制。
只有关闭了tcpcookie(0)后,才会在队列溢出时候丢弃syn报文。
判断2:accept接队列溢出 sk_acceptq_is_full
如果accept队列里的已经完成三次握手,等待被accept的socket数量超过了监听socket 的sk_max_ack_backlog,后续的 req socket 就会被丢弃。
1 | 1034 static inline bool sk_acceptq_is_full(const struct sock *sk) |
当因为accept 队列满,而被丢弃的SYN 请求,会被统计到LINUX_MIB_LISTENOVERFLOWS里。
如何查看LINUX_MIB_LISTENOVERFLOWS
cat /proc/net/netstat
在TcpExt这部分统计里有个ListenOverflows子统计项netstat -es
判断3: inet_csk_reqsk_queue_len 和 sysctl_max_syn_backlog
如果tcp syncookie机制没有使用,那么还有第3个检查
- 半链接队列的长度不能超过sysctl_max_syn_backlog的3/4, 否则丢弃。
1 | 7283 if (!want_cookie && !isn) { |
只有关闭syncookie时,才会走到这个检查逻辑里,不要被前面want_cookie(是否使用cookie的判断结果)干扰。
listen 调用 backlog 与 somaxconn 取最小值
- 比较listen调用里的第二个参数
backlog和 sysctl 里的net.core.somaxconn,取 最小值 - 结果存放到
sk->sk_max_ack_backlog, 这也是accept 队列和变链接队列的最大长度。
1 | 1860 */ |
1 | 191 int __inet_listen_sk(struct sock *sk, int backlog) |
系统参数 sysctl_max_syn_backlog
- 最小 128
- 取ehash_entries/128 和 128 的最大值
ehash_entries如何计算, 如何查看:简单看了下,内核启动时候可以指定大小, 否则系统默认初始化是在内存模块里做的。待细看
1 | 3434 net->ipv4.sysctl_max_syn_backlog = max(128U, ehash_entries / 128); |
1 | 245 static inline int reqsk_queue_len(const struct request_sock_queue *queue) |
如何查看accept队列里的child socket数量以及accept队列的最大长度
通过ss -leti命令,可以查看每个 listen socket 下的 accept 队列里的实际长度,也就是 等待被 accept 的 child socket 的个数。其实 accept 队列的总长度也已经透过内核传递给了ss里,只是ss命令在Send-Q列显示。
具体代码逻辑待查看。
内核 struct tcp_info里的 tcpi_unacked和tcpi_sacked
内核没有专门为两个统计值,设置上传的名字,而是借用 struct tcp_info里的tcpi_unacked和tcpi_sacked两个字段传递 listen socket 的 accept 队列长度sk_ack_backlog和最大值sk_max_ack_backlog
1 | 4096 if (info->tcpi_state == TCP_LISTEN) { |
ss命令的实现
ss命令通过 netlink 消息获取到 tcpinfo 后,会吧数据转换到一个 s 结构体,并打印出来。
因此队列长度就被当做unacked字段打印出来。 而listen socket 的队列最大长度,特意过滤没有打印。
1 | 3072 s.unacked = info->tcpi_unacked; |
备注:ss 代码在https://github.com/shemminger/iproute2.git
我也不知道为啥不打印出来最大长度值
这部分功能在 2007 年比较早就进入了内核https://web.git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=5ee3afba88f5a79d0bff07ddd87af45919259f91
而 ss 命里第一个版本是2013 年加入的时候, 就把 LISTEN socket 作为一个特殊情况处理了。https://web.git.kernel.org/pub/scm/network/iproute2/iproute2.git/commit/?id=260804f422fd33aa78379270d564a495b7bb5717
- 备注:内核代码版本v6.14(v6.14-rc6-22-gb7f94fcf5546)
如何查看accept队列里半连接的长度?
我也不没找到命令, 待学习。
实验记录
- 设置tcp_syncookies为 0, 关闭syncookie 机制。关闭后才能够进入tcp_max_syn_backlog的检查逻辑。
- 设置tcp_max_syn_backlog 值为 64,
1 | echo 64 > /proc/sys/net/ipv4/tcp_max_syn_backlog |
持续发送syn报文到server端口。
投过 scapy 构造简单的 syn 报文, 往 server 端发送 syn 报文。
- 预期最大半链接数 64*(3/4)=48, 48+ 1 = 49, 对应源代码,见判断 3
+1的原因是代码里判断是<, 而不是<=, 所以要多发一个。
实际抓包数据显示从第 49 个syn 报文开始, server 不再响应。
1 | 20:57:22.633658 IP 9.9.9.9.10000 > 10.0.111.13.2005: Flags [S], seq 0, win 8192, length 0 |