一文读懂半连接队列
简介
上文我们介绍了全连接队列,这次介绍半连接队列(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(8888, 1);
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 Server
和 java 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(8888, 50);
System.out.println("Started on 8888!");
Thread.currentThread().join();
}
}
在我的机器上,net.ipv4.tcp_max_syn_backlog = 128
,net.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 运算)的值有限
总结
- 半连接队列长度取决于 backlog、somaxconn 和 tcp_max_syn_backlog 三个参数的最小值。
- 半连接队列溢出后根据 tcp_syncookies 的取值有不同的表现,推荐打开 tcp_syncookies。
转载自:https://juejin.cn/post/7373975016084766772