创建半链接时的三个长度检查
在处理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 大,取最大值。sysctl_tcp_syncookies
: 初始值为 1
判断1: 半链接队列是否溢出 inet_csk_reqsk_queue_is_full
虽然不再维护半链接队列了, 但是每次创建req socket后,这个统计值都是在增加的。
因此如果队列长度
超过了最大值sk_max_ack_backlog
,则丢弃。
1 | 278 static inline int inet_csk_reqsk_queue_len(const struct sock *sk) |
判断2:accept接队列溢出 sk_acceptq_is_full
如果 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 机制没有被启用(0),那么还有有第3 个检查
- 半链接队列的长度不能超过sysctl_max_syn_backlog的 3/4, 否则丢弃。sysctl_tcp_syncookies为 0 时候,才会走到这个检查逻辑里,这个 sysctl 参数默认数值是 1. 一般不会走到这里。
1
2
3
4
5
6
7
8
9
107229 syncookies = READ_ONCE(net->ipv4.sysctl_tcp_syncookies);
...
7283 if (!want_cookie && !isn) {
7284 int max_syn_backlog = READ_ONCE(net->ipv4.sysctl_max_syn_backlog);
7287 if (!syncookies &&
7288 (max_syn_backlog - inet_csk_reqsk_queue_len(sk) <
7289 (max_syn_backlog >> 2)) &&
7290 !tcp_peer_is_proven(req, dst)) {
7301 }
listen 调用 backlog 与 somaxconn 取最小值
- 比较listen调用里的第二个参数
backlog
和 sysctl 里的net.core.somaxconn
,取 最小值 - 结果存放到
sk->sk_max_ack_backlog
, 这也是accept 队列和变链接队列的最大长度。1
2
3
4
5
6
71860 */
1861 int __sys_listen_socket(struct socket *sock, int backlog)
1862 {
...
1865 somaxconn = READ_ONCE(sock_net(sock->sk)->core.sysctl_somaxconn);
1866 if ((unsigned int)backlog > somaxconn)
1867 backlog = somaxconn;1
2
3
4191 int __inet_listen_sk(struct sock *sk, int backlog)
192 {
...
199 WRITE_ONCE(sk->sk_max_ack_backlog, backlog);
系统参数 sysctl_max_syn_backlog
- 取ehash_entries/128 和 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 命令没有显示。
具体代码逻辑如下
内核 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 命令查看 accept 队列的最大长度, 需要修改下 ss 的代码
1 | ubuntu@VM-111-13-ubuntu:~/git/iproute2$ git diff |
备注: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 |