【动手实验】TCP半连接队列、全连接队列实战分析

本文是对 从一次线上问题说起,详解 TCP 半连接队列、全连接队列 这篇文章的实验复现和总结,借此加深对 TCP 半连接队列、全连接队列的理解。

实验环境

两台腾讯云服务器 node2(172.19.0.12) 和 node3(172.19.0.15)配置为 2C4G,Ubuntu 系统,内核版本 5.15.0-130-generic 。

全连接半连接队列简介

在 TCP 三次握手过程中,Linux 会维护两个队列分别是:

  • SYN Queue 半连接队列
  • Accept Queue 全连接队列

创建连接时,两个队列作用如下:

  • 客户端向服务端发送 SYN 包,客户端进入 SYN_SENT 状态
  • 服务端收到 SYN 包后,进入 SYN_RECV 状态,内核将连接信息放入 SYN Queue 队列,然后向客户端发送 SYN+ACK 包
  • 客户端收到 SYN+ACK 包后,发送 ACK 包,客户端进入 ESTABLISHED 状态
  • 服务端收到 ACK 包后,将连接从 SYN Queue 队列中取出移到 Accept Queue 队列,Server 端进入 ESTABLISHED。
  • 服务端应用程序调用 accept 函数处理数据,连接从 Accept Queue 队列移除。


图片来自:从一次线上问题说起,详解 TCP 半连接队列、全连接队列

图片来自Cloudflare Blog: SYN Packet Handling in the Wild

两个队列的长度都是有限的,当队列满了之后,新建连接时内核会将 SYN 包丢弃或者直接返回 RST 包。

全连接队列实战

全连接队列长度控制

TCP 全连接队列的长度计算公式为:

min(somaxconn, backlog)

  • somaxconn Linux 内核参数 net.core.somaxconn 的值,默认为 4096。可以通过修改该参数来控制全连接队列的长度。
  • backlog 是系统调用 listen 函数 int listen(int sockfd, int backlog) 的 backlog 参数, Golang 中默认使用系统 somaxconn 的值。

下面是 Linux 5.15.130 内核源码中计算全连接队列长度的代码:

源码地址:https://elixir.bootlin.com/linux/v5.15.130/source/net/socket.c#L1716

我们修改 somaxconn 的值,然后运行实验代码查看全连接队列的长度变化。

  • 服务端实验代码
package main

import (
  "log"
  "net"
  "time"
)

func main() {
  l, err := net.Listen("tcp", ":8888")
  if err != nil {
    log.Printf("failed to listen due to %v", err)
  }
  defer l.Close()
  log.Println("listen :8888 success")

  for {
    time.Sleep(time.Second * 100)
  }
}

首先我们修改 somaxconn 为 128:

sudo sysctl -w net.core.somaxconn=128

启动服务后查看全连接队列的长度:

$ go run server.go
2025/02/13 09:53:01 listen :8888 success


$ ss -lnt
State             Recv-Q            Send-Q                         Local Address:Port                         Peer Address:Port            Process
LISTEN            0                 128                                        *:8888                                    *:*
...

这里简单解释下 ss 命令输出的含义:

  • 对于 Listen 状态的 socket,Recv-Q 表示当前全连接队列的长度,也就是已经完成三次握手,等待应用层调用 accept 的 TCP 连接数;Send-Q 表示全连接队列的最大长度。

  • 对于非 Listen 状态的 socket,Recv-Q 表示已经收到但尚未被应用读取的字节数;Send-Q 表示已发送但尚未收到确认的字节数。

再次修改 somaxconn 为 1024 重启服务后,查看全连接队列的长度已经变成了 1024。

$ sudo sysctl -w net.core.somaxconn=1024
$ go run server.go
2025/02/13 09:53:01 listen :8888 success


$ ss -lnt
State             Recv-Q            Send-Q                         Local Address:Port                         Peer Address:Port            Process
LISTEN            0                 1024                                       *:8888                                    *:*
...

全连接队列溢出

下面我们让服务端只 Listen 端口但不执行 accept() 处理数据,模拟全连接队列溢出的情况。

  • 服务端代码
// server 端监听 8888 tcp 端口 
package main 
 
import ( 
  "log" 
  "net" 
  "time" 
) 
 
func main() { 
  l, err := net.Listen("tcp", ":8888") 
  if err != nil { 
    log.Printf("failed to listen due to %v", err) 
  } 
  defer l.Close() 
  log.Println("listen :8888 success") 
 
  for { 
    time.Sleep(time.Second * 100) 
  } 
}
  • 客户端代码

和原实验相比加了 time.Sleep(500 * time.Millisecond) 一行代码,让连接一个个建立,可以更精准的复现全连接队列已满的情况。

package main 
 
import ( 
  "context" 
  "log" 
  "net" 
  "os" 
  "os/signal" 
  "sync" 
  "syscall" 
  "time" 
) 
 
var wg sync.WaitGroup 
 
func establishConn(ctx context.Context, i int) { 
  defer wg.Done() 
  conn, err := net.DialTimeout("tcp", ":8888", time.Second*5) 
  if err != nil { 
    log.Printf("%d, dial error: %v", i, err) 
    return 
  } 
  log.Printf("%d, dial success", i) 
  _, err = conn.Write([]byte("hello world")) 
  if err != nil { 
    log.Printf("%d, send error: %v", i, err) 
    return 
  } 
  select { 
  case <-ctx.Done(): 
    log.Printf("%d, dail close", i) 
  } 
} 
 
func main() { 
  ctx, cancel := context.WithCancel(context.Background()) 
  // 并发请求 10 次服务端,连接建立成功后发送数据
  for i := 0; i < 10; i++ { 
    wg.Add(1) 
    time.Sleep(500 * time.Millisecond)
    go establishConn(ctx, i) 
  } 
 
  go func() { 
    sc := make(chan os.Signal, 1) 
    signal.Notify(sc, syscall.SIGINT) 
    select { 
    case <-sc: 
      cancel() 
    } 
  }() 
 
  wg.Wait() 
  log.Printf("client exit") 
}

我们先将全连接队列的最大长度设置为 5:

$ sudo sysctl -w net.core.somaxconn=5

$ cat /proc/sys/net/core/somaxconn
5

运行服务端和客户端后,查看全连接队列情况:

  • 服务端 socket 情况
$ ss -ant | grep -E "Recv|8888"
State      Recv-Q Send-Q        Local Address:Port            Peer Address:Port Process
LISTEN     6      5                         *:8888                       *:*
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:40148
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:40162
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:40128
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:40132
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:40110
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:40112
  • 客户端 socket 情况
$ ss -ant | grep -E "Recv|8888"
State      Recv-Q Send-Q Local Address:Port    Peer Address:Port Process
ESTAB      0      0        172.19.0.15:40132    172.19.0.12:8888
ESTAB      0      0        172.19.0.15:40162    172.19.0.12:8888
ESTAB      0      0        172.19.0.15:40148    172.19.0.12:8888
SYN-SENT   0      1        172.19.0.15:51906    172.19.0.12:8888
ESTAB      0      0        172.19.0.15:40112    172.19.0.12:8888
ESTAB      0      0        172.19.0.15:40128    172.19.0.12:8888
SYN-SENT   0      1        172.19.0.15:51912    172.19.0.12:8888
SYN-SENT   0      1        172.19.0.15:40176    172.19.0.12:8888
ESTAB      0      0        172.19.0.15:40110    172.19.0.12:8888
SYN-SENT   0      1        172.19.0.15:51926    172.19.0.12:8888
  • 客户端日志输出
$ go run client.go
2025/02/19 11:14:22 0, dial success
2025/02/19 11:14:22 1, dial success
2025/02/19 11:14:23 2, dial success
2025/02/19 11:14:23 3, dial success
2025/02/19 11:14:24 4, dial success
2025/02/19 11:14:24 5, dial success
2025/02/19 11:14:30 6, dial error: dial tcp 172.19.0.12:8888: i/o timeout
2025/02/19 11:14:30 7, dial error: dial tcp 172.19.0.12:8888: i/o timeout
2025/02/19 11:14:31 8, dial error: dial tcp 172.19.0.12:8888: i/o timeout
2025/02/19 11:14:31 9, dial error: dial tcp 172.19.0.12:8888: i/o timeout

我们来分析下上述结果:

1. 全连接队列是否已满

服务端 Listen 状态的 socket 显示 Send-Q 为 5,表示该 socket 的全连接队列最大值为 5;Recv-Q 为 6,表示当前 Accept queue 中数量为 6,我们看有 6 条 ESTAB 状态的连接,符合观察结果。Linux 内核的判断依据是 > 而不是 >=,所以实际的连接数为比队列的最大值多 1 个。5.15.0-130-generic 内核代码如下:

// 源码地址
// https://elixir.bootlin.com/linux/v5.15.130/source/include/net/sock.h#L980
/* Note: If you think the test should be:
 *	return READ_ONCE(sk->sk_ack_backlog) >= READ_ONCE(sk->sk_max_ack_backlog);
 * Then please take a look at commit 64a146513f8f ("[NET]: Revert incorrect accept queue backlog changes.")
 */
static inline bool sk_acceptq_is_full(const struct sock *sk)
{
	return READ_ONCE(sk->sk_ack_backlog) > READ_ONCE(sk->sk_max_ack_backlog);
}

之所以这样做,是为了保证在 backlog 设置为 0 时,依然可以有一个连接进入全连接队列,具体可以查看以下 commit 信息:

https://github.com/torvalds/linux/commit/64a146513f8f12ba204b7bf5cb7e9505594ead42

[NET]: Revert incorrect accept queue backlog changes.
This reverts two changes:

8488df8
248f067

A backlog value of N really does mean allow "N + 1" connections
to queue to a listening socket.  This allows one to specify
"0" as the backlog and still get 1 connection.

Noticed by Gerrit Renker and Rick Jones.

Signed-off-by: David S. Miller <davem@davemloft.net>
2. 内核 drop 包处理逻辑

客户端有 6 个 ESTAB 状态的 socket,另外还有 4 个 SYN-SENT 状态的 socket,对应着 4 条 timeout 报错信息。我们只改了全连接队列大小为 5,半连接队列大小依然为默认的 net.ipv4.tcp_max_syn_backlog=256,所以第 6 个请连接建立后 Accept Queue 满了但 SYN Queue 还没有满。按理说从第 7 个请求开始服务端可以接收 SYN 但不能在处理客户端的 ACK 进入 Accept Queue,服务端会有 4 条 SYN-RECV 状态的连接,而实际情况是服务端不存在 SYN_RECV 状态的连接,这是因为当 Accept Queue 被占满时,即使 SYN Queue 没有满,Linux 内核也会将新来的 SYN 请求丢弃掉。 5.15.0-130-generic 内核处理这部分逻辑的代码如下::

// 源码地址:https://elixir.bootlin.com/linux/v5.15.130/source/net/ipv4/tcp_input.c#L6848

int tcp_conn_request(struct request_sock_ops *rsk_ops,
		     const struct tcp_request_sock_ops *af_ops,
		     struct sock *sk, struct sk_buff *skb)
{
 // ... 代码省略

	syncookies = READ_ONCE(net->ipv4.sysctl_tcp_syncookies);

	/* TW buckets are converted to open requests without
	 * limitations, they conserve resources and peer is
	 * evidently real one.
	 */
   // 强制启用 SYN cookie 或者半连接队列已满
   // !isn 表示是一个新的请求连接建立的 SYN
	if ((syncookies == 2 || inet_csk_reqsk_queue_is_full(sk)) && !isn) {
    // 这里表示是否启用 SYN cookie 机制;如果不开启,则直接 drop,如果开启,则继续执行。
		want_cookie = tcp_syn_flood_action(sk, rsk_ops->slab_name);
		if (!want_cookie)
			goto drop;
	}
  // 如果 accept queue 满了则 drop
	if (sk_acceptq_is_full(sk)) {
		NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
		goto drop;
	}


static bool tcp_syn_flood_action(const struct sock *sk, const char *proto)
{
	struct request_sock_queue *queue = &inet_csk(sk)->icsk_accept_queue;
	const char *msg = "Dropping request";
	struct net *net = sock_net(sk);
	bool want_cookie = false;
	u8 syncookies;

	syncookies = READ_ONCE(net->ipv4.sysctl_tcp_syncookies);

// 开启 SYN Cookie 机制
#ifdef CONFIG_SYN_COOKIES
	if (syncookies) {
		msg = "Sending cookies";
		want_cookie = true;
		__NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPREQQFULLDOCOOKIES);
	} else
#endif
    // 没有启用 syncookies,统计丢弃包的数量
		__NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPREQQFULLDROP);
  
  // 如果启用了 SYN cookie 机制,发送警告
	if (!queue->synflood_warned && syncookies != 2 &&
	    xchg(&queue->synflood_warned, 1) == 0)
		net_info_ratelimited("%s: Possible SYN flooding on port %d. %s.  Check SNMP counters.\n",
				     proto, sk->sk_num, msg);

	return want_cookie;
}

// 判断半连接队列是否满,用的是半连接队列的长度是否大于等于全连接队列的最大长度
static inline int inet_csk_reqsk_queue_is_full(const struct sock *sk)
{
	return inet_csk_reqsk_queue_len(sk) >= sk->sk_max_ack_backlog;
}

从代码中可以推测出 net.ipv4.tcp_syncookies 参数值的含义和 Linux 的处理机制:

  • 2:强制开启 SYN Cookie 机制,发送警告
  • 1:当半连接队列满时,开启 SYN Cookie 机制,发送警告
  • 0:不开启 SYN Cookie 机制,并统计丢弃包的数量

这里判断半连接队列是否满的依据是 inet_csk_reqsk_queue_len(sk) >= sk->sk_max_ack_backlog,也就是说当半连接队列长度不小于全连接队列的最大长度时,如果不开启 SYN Cookie 机制,就会将 SYN 包丢弃。

回到我们的实验环境,net.ipv4.tcp_syncookies 设置为 1 并且半连接队列没满,因此不会开启 SYN Cookie 机制,继续往后执行时会因为 Accept Queue 满了将包丢弃。可以通过 netstat -s 命令查看丢弃包的数量。

$ date;netstat -s | grep -i "SYNs to LISTEN"
Wed Feb 19 12:05:51 PM CST 2025
    1289 SYNs to LISTEN sockets dropped


$ date;netstat -s | grep -i "SYNs to LISTEN"
Wed Feb 19 12:06:05 PM CST 2025
    1301 SYNs to LISTEN sockets dropped

可以看到有 12 个 SYN 包被 DROP 了,查看抓包情况可以看到,我们有 4 个请求连接超时,每个请求传了 3 次 SYN(一次发起 + 两次重传)。

查看客户端 socket 状态能够看到重传计时器在工作,这里重传了两次和默认的 net.ipv4.tcp_syn_retries = 6 有出入,是因为代码 conn, err := net.DialTimeout("tcp", "172.19.0.12:8888", time.Second*5)设置了 5s 超时,操作系统的默认重传间隔大约为 1s、2s、4s、8s、16s、32s,第 3 次重传会发生在 7s 以后,客户端已经主动断开连接了。

$ sudo netstat -anpo | grep -E "Recv-Q|8888"
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name     Timer
tcp        0      0 172.19.0.15:57384       172.19.0.12:8888        ESTABLISHED 3123924/client       keepalive (7.57/0/0)
tcp        0      0 172.19.0.15:57388       172.19.0.12:8888        ESTABLISHED 3123924/client       keepalive (8.07/0/0)
tcp        0      0 172.19.0.15:60276       172.19.0.12:8888        ESTABLISHED 3123924/client       keepalive (9.58/0/0)
tcp        0      1 172.19.0.15:60304       172.19.0.12:8888        SYN_SENT    3123924/client       on (0.08/1/0)
tcp        0      1 172.19.0.15:60286       172.19.0.12:8888        SYN_SENT    3123924/client       on (2.60/2/0)
tcp        0      0 172.19.0.15:60270       172.19.0.12:8888        ESTABLISHED 3123924/client       keepalive (9.08/0/0)
tcp        0      0 172.19.0.15:60280       172.19.0.12:8888        ESTABLISHED 3123924/client       keepalive (10.08/0/0)
tcp        0      1 172.19.0.15:60292       172.19.0.12:8888        SYN_SENT    3123924/client       on (3.11/2/0)
tcp        0      0 172.19.0.15:57398       172.19.0.12:8888        ESTABLISHED 3123924/client       keepalive (8.57/0/0)
tcp        0      1 172.19.0.15:60294       172.19.0.12:8888        SYN_SENT    3123924/client       on (3.62/2/0)
3. overflow 参数控制

当全连接队列满时,Linux 默认会 drop 掉包,这个受 net.ipv4.tcp_abort_on_overflow 参数控制,默认为 0 表示直接 drop,为 1 则表示中断连接,服务端会返回 RST 包。可以通过如下方式修改

$ sudo sysctl -w net.ipv4.tcp_abort_on_overflow=1

或者

echo 1 > /proc/sys/net/ipv4/tcp_abort_on_overflow

我们修改参数后再次执行客户端请求,会出现 connection reset by peer 错误,抓包能看到 RST 包。(在实验时,如果客户端不加时间间隔,会出现返回 RST 包的情况,如果加了则不会出现这种情况,应该是和两者的生效机制有关,SYN Cookie 和全连接队列满 drop 发生在 tcp_conn_request 函数,而 abort_on_overflow 发生在 tcp_check_req 函数, 先挖个坑,等后续梳理整个网络传输流程时在做进一步分析)。

$ go run client.go
2025/03/01 13:36:55 2, dial success
2025/03/01 13:36:55 5, dial success
2025/03/01 13:36:55 4, dial success
2025/03/01 13:36:55 1, dial success
2025/03/01 13:36:55 3, dial success
2025/03/01 13:36:55 0, dial success
2025/03/01 13:36:55 7, dial error: dial tcp 172.19.0.12:8888: connect: connection reset by peer
2025/03/01 13:36:55 6, dial error: dial tcp 172.19.0.12:8888: connect: connection reset by peer

4. ss 命令展示含义

服务端有 6 条 ESTAB 状态的 socket,RECV_Q 的值为 11,与客户端发送的数据 []byte("hello world") 数据长度一致,因为我们的没有执行 accept 接收数据,所以 RECV_Q 会展示这部分数据的大小;

客户端 6 条 ESTAB 状态的 socket,其 RECV_Q 和 SEND_Q 均为 0;而 4 条 SYN-SENT 状态的 SEND-Q 为 1,这是因为 6 条已建立连接的 socket 包可以被正常 ACK,而 4 条建立连接失败的 socket,其 SYN 包没有收到 ACK 包,因为 SEND-Q 显示为 1。由此我们可以再次总结下 ss 的展示含义:

对于 LISTEN 状态的 socket

  • Recv-Q:表示当前全连接队列的大小,即已完成三次握手等待应用程序 accept() 的 TCP 连接数。
  • Send-Q:全连接队列的最大长度,即全连接队列所能容纳的 socket 数量。

对于非 LISTEN 状态的 socket

  • Recv-Q:表示已被接收但尚未执行 accept 被应用程序读取的数据字节数,通常在服务端能观察到。
  • Send-Q:表示已经发送但尚未收到 ACK 确认的字节数。

内核代码如下:

// https://elixir.bootlin.com/linux/v5.15.130/source/net/ipv4/tcp_diag.c#L18
static void tcp_diag_get_info(struct sock *sk, struct inet_diag_msg *r,
			      void *_info)
{
	struct tcp_info *info = _info;

	if (inet_sk_state_load(sk) == TCP_LISTEN) { // LISTEN 状态的连接

    // 当前已完成三次握手但未被 accept 的连接数
		r->idiag_rqueue = READ_ONCE(sk->sk_ack_backlog); 
    // 最大队列长度
		r->idiag_wqueue = READ_ONCE(sk->sk_max_ack_backlog);
	} else if (sk->sk_type == SOCK_STREAM) { // 非 LISTEN 状态的普通连接
		const struct tcp_sock *tp = tcp_sk(sk);

    // TCP 读队列,即接收缓冲区中未被应用层读取的数据量,单位是字节
		r->idiag_rqueue = max_t(int, READ_ONCE(tp->rcv_nxt) -
					     READ_ONCE(tp->copied_seq), 0);
    // TCP 写队列,即已经发送但尚未被对方 ACK 确认的数据量,单位是字节
		r->idiag_wqueue = READ_ONCE(tp->write_seq) - tp->snd_una;
	}
	if (info)
		tcp_get_info(sk, info);
}
5. SYN+ACK 重传

原实验有三种情况:

  • 三次握手成功,数据正常发送
  • 客户端认为连接建立成功,但服务端一直处于 SYN-RECV 状态,不断重传 SYN + ACK
  • 客户端发送 SYN 未得到响应一直在重传

我们复现了第 1 中和第 3 种,之所以没有第二种情况是因为每次请求加了 500ms 的间隔,这样下一个请求发起 SYN 时,上一个请求已经完成三次握手,服务端的 socket 已经进入全连接队列了。如果我们去掉时间间隔,请求可能会一下子发出去全部进入半连接队列,等到服务端在接收到客户端的 ACK 包时,全连接队列已经满了,从而导致服务端的 socket 无法进入全连接队列,从而 DROP 掉 ACK 包出现第二种情况。这里我们去掉时间间隔尝试复现,此时可以看到服务端有 SYN-RECV 状态的连接,

$ ss -ant | grep -E "Recv|8888"
State      Recv-Q Send-Q        Local Address:Port            Peer Address:Port Process
LISTEN     6      5                         *:8888                       *:*
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:33430
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:33458
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:33482
SYN-RECV   0      0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:33512
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:33442
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:33428
ESTAB      11     0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:33472
SYN-RECV   0      0      [::ffff:172.19.0.12]:8888    [::ffff:172.19.0.15]:33496

查看抓包结果可以看到 SYN-ACK 包重传。

全连接队列的实验就到这里,下面我们来看半连接队列的实验。

半连接队列实战

半连接队列的最大长度计算有些麻烦,网络上资料也很繁杂,本着 talk is cheap, show me the code 的原则,这里还是直接看 Linux 的源码来分析,还是 tcp_conn_request 函数。

// 源码地址:https://elixir.bootlin.com/linux/v5.15.130/source/net/ipv4/tcp_input.c#L6848

int tcp_conn_request(struct request_sock_ops *rsk_ops,
		     const struct tcp_request_sock_ops *af_ops,
		     struct sock *sk, struct sk_buff *skb)
{
   // ... 代码省略
	u8 syncookies;

  // 第一部分,基于 syncookies 和半连接队列是否超过全连接队列长度、半连接队列是否已满来判断是否 drop
	syncookies = READ_ONCE(net->ipv4.sysctl_tcp_syncookies);

	if ((syncookies == 2 || inet_csk_reqsk_queue_is_full(sk)) && !isn) {
		want_cookie = tcp_syn_flood_action(sk, rsk_ops->slab_name);
		if (!want_cookie)
			goto drop;
	}

	// 第二部分,判断全连接队列是否已满
	if (sk_acceptq_is_full(sk)) {
		NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
		goto drop;
	}

	req = inet_reqsk_alloc(rsk_ops, sk, !want_cookie);
	if (!req)
		goto drop;

// ... 代码省略

	if (!want_cookie && !isn) {
    // 获取系统参数 ``net.ipv4.tcp_max_syn_backlog`` 的值
		int max_syn_backlog = READ_ONCE(net->ipv4.sysctl_max_syn_backlog);

		/* Kill the following clause, if you dislike this way. */
    // 第三部分:判断半连接队列是否超过长度限制
		if (!syncookies &&
		    (max_syn_backlog - inet_csk_reqsk_queue_len(sk) <
		     (max_syn_backlog >> 2)) &&
		    !tcp_peer_is_proven(req, dst)) {
			/* Without syncookies last quarter of
			 * backlog is filled with destinations,
			 * proven to be alive.
			 * It means that we continue to communicate
			 * to destinations, already remembered
			 * to the moment of synflood.
			 */
			pr_drop_req(req, ntohs(tcp_hdr(skb)->source),
				    rsk_ops->family);
			goto drop_and_release;
		}

		isn = af_ops->init_seq(skb);
	}

	tcp_ecn_create_request(req, skb, sk, dst);

	if (want_cookie) {
		isn = cookie_init_sequence(af_ops, sk, skb, &req->mss);
		if (!tmp_opt.tstamp_ok)
			inet_rsk(req)->ecn_ok = 0;
	}

	return 0;

}

核心计算逻辑是 (max_syn_backlog - inet_csk_reqsk_queue_len(sk) < (max_syn_backlog >> 2)),即 max_syn_backlog 的值减去当前半连接队列的长度的值小于 max_syn_backlog 的 1/4 时,就会将 SYN 包丢弃。简单来说就是半连接队列长度不能超过 max_syn_backlog 的 3/4。因为比较条件是 > 而不是 >=,所以在不开启 syncookies 的情况下,实际的半连接队列长度应该是 max_syn_backlog 的 3/4 + 1。大致计算如下:

  • max_syn_backlog 为 128,则半连接队列长度最大为 97
  • max_syn_backlog 为 256,则半连接队列长度最大为 193
  • max_syn_backlog 为 512,则半连接队列长度最大为 385
  • max_syn_backlog 为 1024,则半连接队列长度最大为 769

结合上面全连接实验中的代码分析,我们可以总结下 Linux 5.15.30 内核下 SYN 包的 Drop 机制:

我们修改参数验证下上述三种情况。

实验一:关闭 syncookies,半连接长度超过全连接最大长度

客户端我们使用 iptables 将服务端的包拦截,模拟 SYN Flood 攻击,这样服务端不会收到 ACK 包,也就不会进入全连接队列。系统参数 syn_cookies=0,max_syn_backlog=128,somaxconn=64,理论上会有 64 个 SYN-RECV 状态连接,其余的包被丢弃。

# 拦截服务端 8888 端口的包
$ sudo iptables -A INPUT -p tcp --sport 8888 -j DROP

# 发送 SYN 包
$ sudo hping3 -S 172.19.0.12 -p 8888 --flood

查看服务端情况

$ ss -ant | grep -E "Recv|8888"
State     Recv-Q Send-Q        Local Address:Port            Peer Address:Port Process
LISTEN    0      64                        *:8888                       *:*

# ubuntu @ node2 in ~ [11:58:11]
$ sudo netstat -nat | grep :8888 | grep SYN_RECV  | wc -l
64

结果符合预期。这里可以用 go 客户端做更精确的验证,我们使用 Go 程序发送 100 个请求,然后查看服务端连接数和 DROP 数

$ date;netstat -s | grep -i "SYNs to LISTEN"
Fri Feb 21 12:01:58 PM CST 2025
    3030591019 SYNs to LISTEN sockets dropped

$ sudo netstat -nat | grep :8888 | grep SYN_RECV  | wc -l
64

$ date;netstat -s | grep -i "SYNs to LISTEN"
Fri Feb 21 12:02:14 PM CST 2025
    3030591127 SYNs to LISTEN sockets dropped

可以看到服务端只有 64 个 SYN-RECV 状态连接,程序执行有有 3030591127-3030591019=108 个 SYN 包被丢弃。上面我们分析过,因为客户端设置了超时时间为 5s,所以 SYN 只会重传 2 次,也就是每个被 DROP 的连接都会发送 3 次 SYN。100 - 64 = 36,36 * 3 = 108,符合我们预期。

实验二:关闭 syncookies,全连接队列已满

修改服务端系统参数 syn_cookies=0,max_syn_backlog=128,somaxconn=64,这样全连接队列最大长度为 64,当有 65 个连接建立时,全连接队列就会满,此时再有 SYN 包建立连接时就会被丢弃。

首先我们清理掉客户端机器的 iptables 规则,是的三次握手能够正常进程。

$ sudo iptables -F

设置系统参数

$ sudo sysctl -w net.ipv4.tcp_syncookies=0
$ sudo sysctl -w net.ipv4.tcp_max_syn_backlog=128
$ sudo sysctl -w net.core.somaxconn=64

我们再次用 Go 客户端发送 100 个请求,然后查看服务端状态,可以看到有 65 个 ESTAB 状态连接,没有 SYN-RECV 状态连接,因为全连接队列已满,所有 SYN 包都会被丢弃。

$ ss -ant | grep -E "Recv|8888"
State     Recv-Q Send-Q        Local Address:Port            Peer Address:Port Process
LISTEN    65     64                        *:8888


$ sudo netstat -nat | grep :8888 | grep ESTAB  | wc -l
65


# ubuntu @ node2 in ~ [12:18:27] C:130
$ sudo netstat -nat | grep :8888 | grep SYN_RECV  | wc -l
0

按照以上逻辑,会有 35 个连接被拒绝,一共有 35 * 3 = 105 个 SYN 包被丢弃。我们查看统计信息可以验证,3030591766 - 3030591661 = 105,符合预期。

$ date;netstat -s | grep -i "SYNs to LISTEN"
Fri Feb 21 12:18:19 PM CST 2025
    3030591661 SYNs to LISTEN sockets dropped

# ubuntu @ node2 in ~ [12:18:19]
$ date;netstat -s | grep -i "SYNs to LISTEN"
Fri Feb 21 12:18:34 PM CST 2025
    3030591766 SYNs to LISTEN sockets dropped

实验三:关闭 syncookies,半连接队列长度超过 max_syn_backlog 的 3/4

现在我们将全连接队列长度调大 net.core.somaxconn 设置为 4096,使用 iptables 拦截服务端 8888 端口的包,这样全连接队列始终不会填满,然后 max_syn_backlog 分别设置为:

  • 128,预期有 97 个 SYN-RECV 状态连接
  • 256,预期有 193 个 SYN-RECV 状态连接
  • 512,预期有 385 个 SYN-RECV 状态连接
  • 1024,预期有 769 个 SYN-RECV 状态连接

分别设置并发送请求后,服务端显示结果如下,基本符合预期。

# 客户端设置 iptables 拦截服务端
sudo iptables -A INPUT -p tcp --sport 8888 -j DROP

# 服务端查看 SYN-RECV 状态连接数
$ sudo sysctl -w net.ipv4.tcp_max_syn_backlog=128
$ ss -ant | grep -E "Recv|:8888" | grep SYN-RECV | wc -l
97

$ sudo sysctl -w net.ipv4.tcp_max_syn_backlog=256
$ ss -ant | grep -E "Recv|:8888" | grep SYN-RECV | wc -l
193

$ sudo sysctl -w net.ipv4.tcp_max_syn_backlog=512
$ ss -ant | grep -E "Recv|:8888" | grep SYN-RECV | wc -l
385

$ sudo sysctl -w net.ipv4.tcp_max_syn_backlog=1024
$ ss -ant | grep -E "Recv|:8888" | grep SYN-RECV | wc -l
769

执行过程数值会有变化,但最大半连接队列长度符合预期。

实验四:开启 syncookies,半连接队列长度取决于 max(somaxconn, backlog)

当开启 syncookies 时,半连接队列不在保留 1/4 的限制,而是取决于 max(somaxconn, backlog)。这里源码判断是 >=,因此最大长度应该会等于 max(somaxconn, backlog)

// 源码地址:https://elixir.bootlin.com/linux/v5.15.130/source/include/net/inet_connection_sock.h#L280
static inline int inet_csk_reqsk_queue_is_full(const struct sock *sk)
{
	return inet_csk_reqsk_queue_len(sk) >= sk->sk_max_ack_backlog;
}

我们分别设置 net.core.somaxconn 为 512,1024,4096并设置 net.ipv4.tcp_syncookies=1 开启 syncookies,每次设置完重启服务端,然后在发起请求,理论上会有 512,1024,4096 个 SYN-RECV 状态连接。

修改服务端 somaxconn 并重启后,使用 watch 命令查看 SYN-RECV 状态连接数,结果如下,符合预期。

$ watch -n 1 "netstat -nat | grep :8888 | grep SYN_RECV | wc -l"
Every 1.0s: netstat -nat | grep :8888 | grep SYN_RECV | wc -l              node2: Sat Mar  1 15:07:22 2025

512

$ watch -n 1 "netstat -nat | grep :8888 | grep SYN_RECV | wc -l"
Every 1.0s: netstat -nat | grep :8888 | grep SYN_RECV | wc -l              node2: Sat Mar  1 15:08:15 2025

1024

$ watch -n 1 "netstat -nat | grep :8888 | grep SYN_RECV | wc -l"
Every 1.0s: netstat -nat | grep :8888 | grep SYN_RECV | wc -l              node2: Sat Mar  1 15:09:11 2025

4096

简要总结

  • 半连接队列受限于全连接队列长度,而全连接队列会受应用的影响,尽量不要将 somaxconn 设置的过小,否则会影响服务器的性能。
  • 尽量开启 syncookies,可以有效防止 SYN Flood 攻击,同时可以避免半连接队列被大量占用。
  • ss、netstat 的熟练使用对探查网络状态非常重要,要熟练掌握。
  • 代码之下无秘密,一定要结合源码去理解 Linux 的网络工作机制,不要只是死记硬背协议。
  • 动手!动手!动手!实践出真知。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/979723.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【弹性计算】弹性裸金属服务器和神龙虚拟化(二):适用场景

弹性裸金属服务器和神龙虚拟化&#xff08;二&#xff09;&#xff1a;适用场景 1.混合云和第三方虚拟化软件部署2.高隔离容器部署3.高质量计算服务4.高速低时延 RDMA 网络支持场景5.RISC CPU 支持6.GPU 性能无损输出 公共云服务提供商推出 弹性裸金属服务器&#xff0c;很显然…

正大杯攻略|量表类问卷数据分析基本步骤

在量表类问卷研究领域&#xff0c;分析变量之间的影响关系是基础且常用的手段。一般先提出关于自变量 X 对因变量 Y 影响关系的假设&#xff0c;随后运用合适的统计方法进行验证&#xff0c;挖掘二者间规律&#xff0c;进而得出结论&#xff0c;为研究发展提供建议。具体分析步…

stm32(hal库)学习笔记-时钟系统

在stm32中&#xff0c;时钟系统是非常重要的一环&#xff0c;他控制着整个系统的频率。因此&#xff0c;我们有理由好好学一下时钟系统。 什么是时钟&#xff1f; 时钟是具有周期性的脉冲信号&#xff0c;一般我们常用占空比为50%的方波。可以形象的说&#xff0c;时钟就是单…

浅入浅出Selenium DevTools

前言 在自动化测试领域&#xff0c;Selenium一直是主流工具之一。随着前端技术的不断发展&#xff0c;浏览器的功能也在不断丰富。 Selenium 3版本前&#xff0c;一套通用的采集流程如上图所示&#xff1a; 打开Charles&#xff0c;设置Session自动导出频次及导出路径Seleniu…

网络安全-使用DeepSeek来获取sqlmap的攻击payload

文章目录 概述DeepSeek使用创建示例数据库创建API测试sqlmap部分日志参考 概述 今天来使用DeepSeek做安全测试&#xff0c;看看在有思路的情况下实现的快不快。 DeepSeek使用 我有一个思路&#xff0c;想要测试sqlmap工具如何dump数据库的&#xff1a; 连接mysql数据库&#…

猿大师播放器:HTML内嵌VLC播放RTSP视频流,无需转码,300ms级延迟,碾压服务器转码方案

在智慧城市、工业安全、应急指挥等关键领域&#xff0c;实时视频监控已成为守护生命与财产的核心防线‌。然而&#xff0c;行业普遍面临三大矛盾&#xff1a; ‌实时性要求与高延迟矛盾‌&#xff1a;火灾蔓延速度达1米/秒&#xff0c;化工泄漏扩散仅需数秒&#xff0c;传统方…

[Jsprit]Jsprit学习笔记-vrp问题新解的接收策略

阈值接收器 作者实现了一个阈值接收器&#xff0c;SchrimpfAcceptance 下面是对这个接收器的解释 阈值接受函数&#xff1a; 这个概念可以描述如下&#xff1a;大多数问题不仅仅有一个唯一的最小值&#xff08;或最大值&#xff09;&#xff0c;而是有多个局部最小值&#xff…

传奇3光通版手游行会战攻略:团队协作与战术布局详解

戳一戳&#xff1b;了解更多 在《传奇3光通版》手游中&#xff0c;行会战是玩家们展现团队协作与战术布局的重要舞台。下面&#xff0c;我们就来详细解析一下行会战中的团队协作与战术布局攻略。 一、团队协作 ​职业搭配 在行会战中&#xff0c;合理的职业搭配至关重要。一般…

初出茅庐的小李博客之按键驱动库使用

驱动库介绍 源码地址&#xff1a;https://github.com/jiejieTop/ButtonDrive 使用只需3步&#xff0c;创建按键&#xff0c;按键事件与回调处理函数链接映射&#xff0c;周期检查按键&#xff0c;支持单双击、连按、长按&#xff1b;采用回调处理按键事件&#xff08;自定义消…

基于springboot+vue实现的食物营养分析与推荐网站 (源码+L文+ppt)43-18

摘 要 食物营养分析与推荐网站是一个综合性的在线平台&#xff0c;它汇集了各类食材的详细营养数据&#xff0c;为用户提供深入的食物营养分析。通过输入个人需求和健康状况&#xff0c;网站能够智能生成个性化的饮食建议&#xff0c;帮助用户更好地规划日常饮食&#xff0c;…

#渗透测试#批量漏洞挖掘#某图创图书馆集群管理系统updOpuserPw SQL注入(CVE-2021-44321)

免责声明 本教程仅为合法的教学目的而准备&#xff0c;严禁用于任何形式的违法犯罪活动及其他商业行为&#xff0c;在使用本教程前&#xff0c;您应确保该行为符合当地的法律法规&#xff0c;继续阅读即表示您需自行承担所有操作的后果&#xff0c;如有异议&#xff0c;请立即停…

java基础+面向对象

Java基础语法 CMD命令 cls 清屏 cd 目录进入文件 cd… 退回 dir 查看当前目录所有文件 E&#xff1a;进入E盘 exit 退出 环境变量就是不用去专门的盘符去找&#xff0c;直接去环境变量里找到文件 语言优势 编译型语言c&#xff1a; 整体翻译 解释型语言python&#x…

水滴tabbar canvas实现思路

废话不多说之间看效果图,只要解决了这个效果水滴tabbar就能做出来了 源码地址 一、核心实现步骤分解 布局结构搭建 使用 作为绘制容器 设置 width=600, height=200 基础尺寸 通过 JS 动态计算实际尺寸(适配高清屏) function initCanvas() {// 获取设备像素比(解决 Re…

SpringBoot+Redis+Mybatis-plus黑马点评

短信登录 基于Session实现登录 流程&#xff1a; 发送短信验证码-->短信验证码注册登录-->校验登录状态&#xff08;保存用户到ThreadLocal&#xff0c;方便后续使用&#xff09; 不能每次请求服务都要进行登录状态校验&#xff0c;解决办法&#xff1a;拦截器 在Sp…

Windows之远程终端问题集锦(十二)

1.描述 远程终端的默认端口是3389 云vps一般会开启 没有启用的情况下&#xff0c;在较高的权限下可以使用注册表命令进行开启 2.注册表开启远程终端命令 2.1 2008 2012 2016 开启3389 这个就是新建.bat文件 在本来不开启的情况下 我们双击运行 他就自动开启了 当然我们必须获…

跟着源码实现LevelDB(二)util/status.cc

概述 本小节实现了leveld的Status类&#xff0c;Status看起来是个简单的类&#xff0c;但是其中也包含了leveldb对极致的性能实现的巧妙设计 Status的内存管理 Status 只有一个成员变量 const char* state_; state[0…3] 表示长度&#xff0c;state[4] 1个字节 表示状态的枚…

【Azure 架构师学习笔记】- Azure Databricks (13) -- 搭建Medallion Architecture part 1

本文属于【Azure 架构师学习笔记】系列。 本文属于【Azure Databricks】系列。 接上文 【Azure 架构师学习笔记】- Azure Databricks (12) – Medallion Architecture简介 前言 上文已经介绍了关于Medallion的知识&#xff0c;本文开始用ADB 来实现&#xff0c; 但是基于内容较…

模型优化之强化学习(RL)与监督微调(SFT)的区别和联系

强化学习&#xff08;RL&#xff09;与监督微调&#xff08;SFT&#xff09;是机器学习中两种重要的模型优化方法&#xff0c;它们在目标、数据依赖、应用场景及实现方式上既有联系又有区别。 想了解有关deepseek本地训练的内容可以看我的文章&#xff1a; 本地基于GGUF部署的…

欧氏距离、曼哈顿距离、切比雪夫距离、闵可夫斯基距离、马氏距离理解学习

目录 一、欧氏距离&#xff08;Euclidean Distance&#xff09;公式&#xff1a;原理&#xff1a; 二、曼哈顿距离&#xff08;Manhattan Distance&#xff09;公式&#xff1a;原理&#xff1a; 三、切比雪夫距离&#xff08;Chebyshev Distance&#xff09;公式&#xff1a;原…

计算机三级网络技术备考(3)

【知识点补充&#xff1a;带宽是是链路的传输速率&#xff0c;宽带是一种服务】 第四章&#xff1a;路由设计技术 考点1&#xff1a;IP路由选择与路由汇聚 核心层&#xff1a;s0两个IP进行汇聚就行 汇聚层&#xff1a;聚合后两个以上的往前进几位&#xff08;因为主机号不够因…