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

news2025/3/4 3:06:27

本文是对 从一次线上问题说起,详解 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 的网络工作机制,不要只是死记硬背协议。
  • 动手!动手!动手!实践出真知。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【六祎 - Note】SQL备忘录;DDL,DML,DQL,DCL

SQL备忘录 from to : 点击访问源地址

智能AI替代专家系统(ES)、决策支持系统(DSS)?

文章目录 前言一、专家系统&#xff08;ES&#xff09;是什么&#xff1f;二、决策支持系统&#xff08;DSS&#xff09;是什么&#xff1f;1.决策支持系统定义2.决策系统的功能与特点3.决策支持系统的组成 三、专家系统&#xff08;ES&#xff09;与决策支持系统&#xff08;D…

比较Spring AOP和AspectJ

1. 介绍 当前有多个可用的AOP库&#xff0c;这些库必须能够回答许多问题&#xff1a; 它与我现有的或新的应用程序兼容吗&#xff1f;在哪里可以实施AOP&#xff1f;它与我的应用程序集成的速度有多快&#xff1f;性能开销是多少&#xff1f; 在本文中&#xff0c;我们将着眼…

现今大语言模型性能(准确率)比较

现今大语言模型性能(准确率)比较 表头信息:表的标题为“大语言模型性能比较结果”(英文:Table 1: Large Language Model Performance Comparison Results),表明该表是用于对比不同大语言模型的性能。列信息: 模型:列出参与比较的不同大语言模型名称,包括LLAMA3(70B)…

(十 五)趣学设计模式 之 命令模式!

目录 一、 啥是命令模式&#xff1f;二、 为什么要用命令模式&#xff1f;三、 策略模式的实现方式四、 命令模式的优缺点五、 命令模式的应用场景六、 总结 &#x1f31f;我的其他文章也讲解的比较有趣&#x1f601;&#xff0c;如果喜欢博主的讲解方式&#xff0c;可以多多支…

React低代码项目:问卷编辑器 I

问卷编辑器 Date: February 20, 2025 4:17 PM (GMT8) 目标 完成问卷编辑器的设计和开发完成复杂系统的 UI 组件拆分完成复杂系统的数据结构设计 内容 需求分析技术方案设计开发 注意事项&#xff1a; 需求指导设计&#xff0c;设计指导开发。前两步很重要页面复杂的话&…

信刻光盘安全隔离与信息交换系统让“数据摆渡”安全高效

随着数据传输、存储及信息技术的飞速发展&#xff0c;信息安全保护已成为重中之重。各安全领域对跨网数据交互的需求日益迫切&#xff0c;数据传输的安全可靠性成为不可忽视的关键。为满足业务需求并遵守保密规范&#xff0c;针对于涉及重要秘密信息&#xff0c;需做到安全的物…

【项目管理】基于 C 语言的 QQ 聊天室实现(TCP + 多线程 + SQLite3)

基于 C 语言的 QQ 聊天室(TCP + 多线程 + SQLite3) 项目功能基础功能: 登录、注册、添加好友、私聊、创建群聊、群聊扩展功能: 删除好友、注销账号、好友在线状态、群管理(拉人/踢人)、VIP 特权、邮件通知等 功能介绍:模拟QQ聊天客户端:登录界面:1、登录2、注册 //将用…

力扣27.移除元素(双指针)

题目看起来很乱&#xff0c;实际上意思是&#xff1a;把数组中值不等于val的元素放在下标为0,1,2,3......&#xff0c;并且返回数组中值不等于val的元素的个数 方法一&#xff1a;直接判断覆盖 class Solution { public:int removeElement(vector<int>& nums, int…

VADv2: 基于矢量表征和概率规划的E2E架构

1. 写在前面 今天分享一篇自动驾驶领域的论文VADv2(End-to-End Vectorized Autonomous Driving via Probabilistic Planning), 基于矢量表征和概率规划的E2E架构,2024年2月份华中科技大和地平线合作的一篇文章, 在经典的端到端模型架构上作出了基于概率规划去输出规划轨迹的…

机器学习:强化学习的epsilon贪心算法

强化学习&#xff08;Reinforcement Learning, RL&#xff09;是一种机器学习方法&#xff0c;旨在通过与环境交互&#xff0c;使智能体&#xff08;Agent&#xff09;学习如何采取最优行动&#xff0c;以最大化某种累积奖励。它与监督学习和无监督学习不同&#xff0c;强调试错…

计算机毕业设计SpringBoot+Vue.js基于JAVA语言的在线考试与学习交流网页平台(源码+文档+PPT+讲解)

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…

数据图表ScottPlot.WPF用法示例

目录 一、添加 NuGet 程序包&#xff08;5.0.47&#xff09; 二、MainWindow.xaml中添加引用 三、MainWindow.xaml.cs 具体使用代码 图表示例&#xff1a; 一、添加 NuGet 程序包&#xff08;5.0.47&#xff09; 二、MainWindow.xaml中添加引用 <Window x:Class"…

【第二十五周】:DeepPose:通过深度神经网络实现人体姿态估计

DeepPose 摘要Abstract文章信息引言DeepPose归一化网络结构初始网络&#xff08;粗略估计所有关节点位置&#xff09;精细化级联网络&#xff08;分别修正每个关节点的位置&#xff09; 疑问与解决代码实践总结 摘要 这篇博客介绍了DeepPose&#xff0c;这是首个基于深度神经网…

ARM Linux LCD上实时预览摄像头画面

文章目录 1、前言2、环境介绍3、步骤4、应用程序编写4.1、lcd初始化4.2、摄像头初始化4.3、jpeg解码4.4、开启摄像头4.5、完整的程序如下 5、测试5.1、编译应用程序5.2、运行应用程序 6、总结 1、前言 本次应用程序主要针对支持MJPEG格式输出的UVC摄像头。 2、环境介绍 rk35…

MacOS本地部署Deepseek,不联网也可以使用AI,保护隐私

苹果笔记本本地部署deepseek主要用到Ollama与open-webui 1. 安装Ollama “Ollama” 是一个轻量级的 AI 模型运行时环境&#xff08;runtime&#xff09;&#xff0c;旨在简化在本地部署和使用大语言模型&#xff08;LLM&#xff09;的过程。它由 Vicarious 公司开发&#xff…

Sqlserver安全篇之_启用TLS即配置SQL Server 数据库引擎以加密连接

官方文档 https://learn.microsoft.com/zh-cn/sql/database-engine/configure-windows/configure-sql-server-encryption?viewsql-server-ver16 https://learn.microsoft.com/zh-cn/sql/database-engine/configure-windows/manage-certificates?viewsql-server-ver15&pre…

创建一个MCP服务器,并在Cline中使用,增强自定义功能。

MCP介绍 MCP 是一个开放协议&#xff0c;它标准化了应用程序如何向LLMs提供上下文。可以将 MCP 视为 AI 应用程序的 USB-C 端口。正如 USB-C 提供了一种标准化的方法来将您的设备连接到各种外围设备和配件一样&#xff0c;MCP 提供了一种标准化的方法来将 AI 模型连接到不同的…

游戏引擎学习第131天

仓库:https://gitee.com/mrxiao_com/2d_game_3 运行游戏并识别我们的小问题 今天的工作重点是对游戏引擎进行架构优化&#xff0c;特别是针对渲染和多线程的部分。目前&#xff0c;我们的目标是让地面块在独立线程上进行渲染&#xff0c;以提高性能。在此过程中&#xff0c;我…

人大金仓国产数据库与PostgreSQL

一、简介 在前面项目中&#xff0c;我们使用若依前后端分离整合人大金仓&#xff0c;在后续开发过程中&#xff0c;我们经常因为各种”不适配“问题&#xff0c;但可以感觉得到大部分问题&#xff0c;将人大金仓视为postgreSQL就能去解决大部分问题。据了解&#xff0c;Kingba…