likes
comments
collection
share

一文读懂半连接队列

作者站长头像
站长
· 阅读数 30

简介

上文我们介绍了全连接队列,这次介绍半连接队列(syn queue)当服务端收到客户端的 SYN 连接请求时,构造数据放到半连接队列中,等到收到客户端 ACK 后,从半连接队列中删除数据到全连接队列。

本文内容是基于 Linux 3.10.0-1160.118.1.el7.x86_64 内核版本,不保证与其它版本一致。

一文读懂半连接队列

队列长度

半连接队列长度取决于 backlog、somaxconn 和 tcp_max_syn_backlog 三个参数。我们可以通过编写 systemtap 脚本来探测队列长度。

probe kernel.function("tcp_v4_conn_request") {
    tcphdr = __get_skb_tcphdr($skb)
    dport = __tcp_skb_dport(tcphdr)

    if (dport == 8888) {
        syn_qlen = @cast($sk"struct inet_connection_sock")->icsk_accept_queue->listen_opt->qlen
        syn_qlen_stats <<< syn_qlen

        if (max_syn_qlen == 0) {
            max_qlen_log = @cast($sk"struct inet_connection_sock")->icsk_accept_queue->listen_opt->max_qlen_log
            max_syn_qlen = (1 << max_qlen_log)
            printf("SYN queue length limit: %d\n", max_syn_qlen)
        }
    }
}
public class Server {
    public static void main(String[] args) throws Exception {
        ServerSocket ss = new ServerSocket(88881);
        System.out.println("Started on 8888!");
        Thread.currentThread().join();
    }
}
public class Client {
    public static void main(String[] args) throws Exception {
        System.out.println("Started on: " + System.currentTimeMillis());
        try {
            for (int i = 0; i < 10; i++) {
                Socket socket = new Socket("127.0.0.1"8888);
                System.out.println(socket.getPort());
            }
        } finally {
            System.out.println("End on: " + System.currentTimeMillis());
        }
    }
}

当我们分别执行 java Serverjava Client 后,systemtap 会打印:SYN queue length limit: 16

确定半连接长度的代码如下:

int reqsk_queue_alloc(struct request_sock_queue *queue,
        unsigned int nr_table_entries)
{

  // 半连接计算方法
 nr_table_entries = min_t(u32, nr_table_entries, sysctl_max_syn_backlog);
 nr_table_entries = max_t(u32, nr_table_entries, 8);
 nr_table_entries = roundup_pow_of_two(nr_table_entries + 1);
 
 // 分配空间代码省略 ……

 for (lopt->max_qlen_log = 3;
      (1 << lopt->max_qlen_log) < nr_table_entries;
      lopt->max_qlen_log++);
}

传入的 nr_table_entries 是 min(backlog, somaxconn)。nr_table_entries 最终就是将前面三个参数的最小值与 8 的最大值 + 1 后取了个离它最近的 2 的幂次方(类似于 Java 的 HashMap),后面的代码是计算 max_qlen_log 的,像是半连接队列的长度。但经过演算,max_queue_len 并不是长度,而是存的 2 的幂次。

因此,在判断半连接队列是否溢出时,是用右移计算而不是直接大于。

// 半连接队列是否满
static inline int reqsk_queue_is_full(const struct request_sock_queue *queue)
{
    // 注意这里是用>>(右移)来判断的,不是大于号
 return queue->listen_opt->qlen >> queue->listen_opt->max_qlen_log;
}

溢出实验

根据我们前面的分析,客户端只需不断向服务端发起 SYN 请求,并忽略服务端的 SYN-ACK 包,很快队列就会满。服务端我们仍使用之前代码,只不过方便观察,将 backlog 改为 50。

public class Server {
    public static void main(String[] args) throws Exception {
        ServerSocket ss = new ServerSocket(888850);
        System.out.println("Started on 8888!");
        Thread.currentThread().join();
    }
}

在我的机器上,net.ipv4.tcp_max_syn_backlog = 128net.core.somaxconn = 128 根据计算,我们半连接队列长度应为 64。

关闭 tcp_syncookies 实验

关闭 net.ipv4.tcp_syncookies

vim /etc/sysctl.conf,在其中修改 net.ipv4.tcp_syncookies = 0,然后 sysctl -p 使其生效。

实验

java Server 启动服务端,通过hping3 -S 127.0.0.1 -p 8888 --rand-source --flood 向 127.0.0.1 发送大量 SYN 包。执行 netstat -nat | grep :8888 | grep SYN_RECV | wc -l 打印 64,后续不再增长,并且通过 telnet localhost 8888 无法连接,证明半连接队列满,抛弃掉 SYN 包,无法正常连接。

一文读懂半连接队列

抓包结果也可以看出,前面的请求服务端还会给回复 SYN-ACK,后面就直接丢弃客户端的 SYN 请求,不再回复了。

一文读懂半连接队列

int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
 // 省略代码
 
 // 如果半连接队列满并且没有开启 tcp_syncookies,丢弃 syn 包。
 if (inet_csk_reqsk_queue_is_full(sk) && !isn) {
  want_cookie = tcp_syn_flood_action(sk, skb, "TCP");
  if (!want_cookie)
   goto drop;
 }
}

打开 tcp_syncookies 实验

SYN FLOOD 攻击通过伪造 IP 向被攻击端发送大量 SYN 包,使得被攻击端不断回复伪造 IP SYN-ACK 和重传 SYN-ACK,导致占满半连接队列,使正常的请求无法进入。

当然,我们可以通过增大半连接队列长度以及减少 SYN-ACK 重传次数来缓解,但面对攻击,这些参数的调整仍然无济于事。

Linux 通过 tcp_syncookies 机制来避免直接分配到半连接队列中,等到客户端回复最后一次 ACK 时,才进行空间分配。

打开 tcp_syncookies

vim /etc/sysctl.conf,在其中修改 net.ipv4.tcp_syncookies = 1,然后 sysctl -p 使其生效。

实验

java Server 启动服务端,通过hping3 -S 127.0.0.1 -p 8888 --rand-source --flood 向 127.0.0.1 发送大量 SYN 包。执行 netstat -nat | grep :8888 | grep SYN_RECV | wc -l 打印 64,后续不再增长。通过 telnet [localhost](http://localhost) 8888 连接成功。

tcp_syncookies 劣势

  • tcp_syncookies 使得状态不存储在服务端,而是客户端。这会导致一些 TCP 参数不可用。
  • tcp_syncookies 的 MSS 值(参与 tcp_syncookies 运算)的值有限

详细参考:改进 syncookie [LWN.net]

总结

  • 半连接队列长度取决于 backlog、somaxconn 和 tcp_max_syn_backlog 三个参数的最小值。
  • 半连接队列溢出后根据 tcp_syncookies 的取值有不同的表现,推荐打开 tcp_syncookies。
转载自:https://juejin.cn/post/7373975016084766772
评论
请登录