创建req scoket时的三个长度检查

创建半链接时的三个长度检查

在处理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_backloginet_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
2
3
4
5
6
7
8
9
278 static inline int inet_csk_reqsk_queue_len(const struct sock *sk)
279 {
280 return reqsk_queue_len(&inet_csk(sk)->icsk_accept_queue);
281 }
282
283 static inline int inet_csk_reqsk_queue_is_full(const struct sock *sk)
284 {
285 return inet_csk_reqsk_queue_len(sk) > READ_ONCE(sk->sk_max_ack_backlog);
286 }

判断2:accept接队列溢出 sk_acceptq_is_full

如果 accept 队列里的三次握手完成的 socket 数量超过了监听 socket 的sk_max_ack_backlog,后续的 req socket 就会被丢弃。

1
2
3
4
1034 static inline bool sk_acceptq_is_full(const struct sock *sk)
1035 {
1036 return READ_ONCE(sk->sk_ack_backlog) > READ_ONCE(sk->sk_max_ack_backlog);
1037 }

当因为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, 否则丢弃。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    7229                 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 }
    sysctl_tcp_syncookies为 0 时候,才会走到这个检查逻辑里,这个 sysctl 参数默认数值是 1. 一般不会走到这里。

listen 调用 backlog 与 somaxconn 取最小值

  • 比较listen调用里的第二个参数backlog 和 sysctl 里的net.core.somaxconn,取 最小值
  • 结果存放到sk->sk_max_ack_backlog, 这也是accept 队列和变链接队列的最大长度。
    1
    2
    3
    4
    5
    6
    7
    1860  */
    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
    4
     191 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
2
3
4
245 static inline int reqsk_queue_len(const struct request_sock_queue *queue)
246 {
247 return atomic_read(&queue->qlen);
248 }

如何查看accept队列里的child socket数量以及accept队列的最大长度

通过ss -leti命令,可以查看每个 listen socket 下的 accept 队列里的实际长度,也就是 等待被 accept 的 child socket 的个数。其实 accept 队列的总长度也已经透过内核传递给了 ss 命里,只是 ss 命令没有显示。
具体代码逻辑如下

内核 struct tcp_info里的 tcpi_unackedtcpi_sacked

内核没有专门为两个统计值,设置上传的名字,而是借用 struct tcp_info里的tcpi_unackedtcpi_sacked两个字段传递 listen socket 的 accept 队列长度sk_ack_backlog和最大值sk_max_ack_backlog

1
2
3
4
5
6
7
8
9
4096         if (info->tcpi_state == TCP_LISTEN) {
4097 /* listeners aliased fields :
4098 * tcpi_unacked -> Number of children ready for accept()
4099 * tcpi_sacked -> max backlog
4100 */
4101 info->tcpi_unacked = READ_ONCE(sk->sk_ack_backlog);
4102 info->tcpi_sacked = READ_ONCE(sk->sk_max_ack_backlog);
4103 return;
4104 }
ss命令的实现

ss命令通过 netlink 消息获取到 tcpinfo 后,会吧数据转换到一个 s 结构体,并打印出来。
因此队列长度就被当做unacked字段打印出来。 而listen socket 的队列最大长度,特意过滤没有打印。

1
2
3
3072                 s.unacked        = info->tcpi_unacked;
...
3076 s.sacked = info->tcpi_sacked;

如果先想通过 ss 命令查看 accept 队列的最大长度, 需要修改下 ss 的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ubuntu@VM-111-13-ubuntu:~/git/iproute2$ git diff
diff --git a/misc/ss.c b/misc/ss.c
index 9438382b..6d0d01a8 100644
--- a/misc/ss.c
+++ b/misc/ss.c
@@ -2689,7 +2689,7 @@ static void tcp_stats_print(struct tcpstat *s)
out(" retrans:%u/%u", s->retrans, s->retrans_total);
if (s->lost)
out(" lost:%u", s->lost);
- if (s->sacked && s->ss.state != SS_LISTEN)
+ if (s->sacked)
out(" sacked:%u", s->sacked);
if (s->dsack_dups)
out(" dsack_dups:%u", s->dsack_dups);
ubuntu@VM-111-13-ubuntu:~/git/iproute2$

备注: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
2
echo 64 > /proc/sys/net/ipv4/tcp_max_syn_backlog
echo 0 > /proc/sys/net/ipv4/tcp_syncookies

持续发送syn报文到server端口。

投过 scapy 构造简单的 syn 报文, 往 server 端发送 syn 报文。

  • 预期最大半链接数 64*(3/4)=48, 48+ 1 = 49, 对应源代码,见判断 3
    +1的原因是代码里判断是<, 而不是<=, 所以要多发一个。

实际抓包数据显示从第 49 个syn 报文开始, server 不再响应。

1
2
3
4
5
6
7
8
9
10
11
12
20:57:22.633658 IP 9.9.9.9.10000 > 10.0.111.13.2005: Flags [S], seq 0, win 8192, length 0
20:57:22.633693 IP 10.0.111.13.2005 > 9.9.9.9.10000: Flags [S.], seq 4100933444, ack 1, win 59220, options [mss 8460], length 0
20:57:22.666099 IP 9.9.9.9.10001 > 10.0.111.13.2005: Flags [S], seq 0, win 8192, length 0
20:57:22.666126 IP 10.0.111.13.2005 > 9.9.9.9.10001: Flags [S.], seq 799721612, ack 1, win 59220, options [mss 8460], length 0

...
20:57:24.218159 IP 9.9.9.9.10047 > 10.0.111.13.2005: Flags [S], seq 0, win 8192, length 0
20:57:24.218178 IP 10.0.111.13.2005 > 9.9.9.9.10047: Flags [S.], seq 4141939655, ack 1, win 59220, options [mss 8460], length 0
20:57:24.250101 IP 9.9.9.9.10048 > 10.0.111.13.2005: Flags [S], seq 0, win 8192, length 0
20:57:24.250111 IP 10.0.111.13.2005 > 9.9.9.9.10048: Flags [S.], seq 3904682644, ack 1, win 59220, options [mss 8460], length 0 <===最后一个syn+ack 报文
20:57:24.282224 IP 9.9.9.9.10049 > 10.0.111.13.2005: Flags [S], seq 0, win 8192, length 0 <===开始不响应 syn 报文了
20:57:24.314192 IP 9.9.9.9.10050 > 10.0.111.13.2005: Flags [S], seq 0, win 8192, length 0