你有多少种姿势杀死一个 TCP 连接?

ptrace/GDB

古早的时候,人们发明了 ptrace 和 GDB,这为我们从进程外部结束一个 TCP 连接提供了一种选择。

打开两个终端,使用 nc 分别运行一个服务端和客户端:

# console 1
$ nc -l 127.0.0.1 1215

# console 2
$ nc 127.0.0.1 1215

打开另一个终端,查看进程的 pid 和 fd

# console 3
$ ps aux | grep nc
root      561894  0.0  0.0  34068  4228 pts/0    S+   11:27   0:00 nc -l 127.0.0.1 1215
root      561905  0.0  0.0  34988  7292 pts/1    S+   11:27   0:00 nc 127.0.0.1 1215

$ ss -tanp | grep nc
ESTAB  0      0          127.0.0.1:57076     127.0.0.1:1215  users:(("nc",pid=561905,fd=3))
ESTAB  0      0          127.0.0.1:1215      127.0.0.1:57076 users:(("nc",pid=561894,fd=4))

使用 GDB 关闭这个连接:

# console 3
$ gdb -p 561905
...
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
0x00007fc33a95c94b in select () from /lib64/libc.so.6
(gdb) call (int)close(3)
$1 = 0
(gdb) quit
A debugging session is active.

        Inferior 1 [process 561905] will be detached.

Quit anyway? (y or n) y
Detaching from program: /usr/bin/ncat, process 561905
[Inferior 1 (process 561905) detached]

调用 close 后,我们发现,服务端连接正常关闭了,客户端却报错了:

# console 1
$ nc -l 127.0.0.1 1215
$ echo $?
0

# console 2
$ nc 127.0.0.1 1215
libnsock select_loop(): nsock_loop error 9: Bad file descriptor
$ echo $?
1

因为 fd 被注册到 select(2) 里,这个错误是符合预期的:

select(2)                  System Calls Manual                 select(2)
...
ERRORS
       EBADF  An invalid file descriptor was given in one of the sets.
              (Perhaps a file descriptor that was already closed, or one
              on which an error has occurred.)  However, see BUGS.
...

这也告诉我们这个方案并不完美。

tcpkill

后来,人们又发明了 BPF,这里指的是经典的 BPF,后人又称为 cBPF。于是,我们又有了新选择:tcpkill

同样,打开两个终端,使用 nc 分别运行一个服务端和客户端:

# console 1
$ nc -l 127.0.0.1 1215

# console 2
$ nc 127.0.0.1 1215

再启动 tcpkill

$ tcpkill -ilo tcp and port 1215
tcpkill: listening on lo [tcp and port 1215]

可以看到,TCP 连接并没有被直接关闭,这是因为 tcpkill 需要捕获到目标连接上的网络报文,获取到 TCP sequence,才能构造 TCP RST 报文以关闭连接。

我们在连接上制造一点流量,连接才能被正常关闭:

# console 1
$ nc -l 127.0.0.1 1215
hello

# console 2
$ nc 127.0.0.1 1215
hello

# console 3
$ tcpkill -ilo tcp and port 1215
tcpkill: listening on lo [tcp and port 1215]
127.0.0.1:42832 > 127.0.0.1:1215: R 3705939593:3705939593(0) win 0
127.0.0.1:42832 > 127.0.0.1:1215: R 3705940105:3705940105(0) win 0
127.0.0.1:42832 > 127.0.0.1:1215: R 3705941129:3705941129(0) win 0
127.0.0.1:1215 > 127.0.0.1:42832: R 813204288:813204288(0) win 0
127.0.0.1:1215 > 127.0.0.1:42832: R 813204800:813204800(0) win 0
127.0.0.1:1215 > 127.0.0.1:42832: R 813205824:813205824(0) win 0

这个方案同样不完美,需要连接上有流量,如果没有开启 TCP Keepalive,可能就没有时机关闭连接了。

tcp_diag

时间来到 2015 年,Google 工程师在内核 commit c1e64e298b8c 加入了选项 CONFIG_INET_DIAG_DESTROY

From c1e64e298b8cad309091b95d8436a0255c84f54a Mon Sep 17 00:00:00 2001
From: Lorenzo Colitti <lorenzo@google.com>
Date: Wed, 16 Dec 2015 12:30:05 +0900
Subject: [PATCH] net: diag: Support destroying TCP sockets.

This implements SOCK_DESTROY for TCP sockets. It causes all
blocking calls on the socket to fail fast with ECONNABORTED and
causes a protocol close of the socket. It informs the other end
of the connection by sending a RST, i.e., initiating a TCP ABORT
as per RFC 793. ECONNABORTED was chosen for consistency with
FreeBSD.

Signed-off-by: Lorenzo Colitti <lorenzo@google.com>
Acked-by: Eric Dumazet <edumazet@google.com>
Signed-off-by: David S. Miller <davem@davemloft.net>

使得我们可以通过发送 netlink 消息关闭一个 TCP 连接,我们可以使用 ss 工具来进行测试:

# console 1
$ nc -l 127.0.0.1 1215

# console 2
$ nc 127.0.0.1 1215

# console 3
$ ss -K dport = 1215

很酷,除了使用 netlink 接口编程麻烦一点,没什么毛病。

bpf_iter_tcp

到了 2020 年,内核有了更酷的东西:BPF Iterator

From 52d87d5f6418ba1b8b449ed5eea1532664896851 Mon Sep 17 00:00:00 2001
From: Yonghong Song <yhs@fb.com>
Date: Tue, 23 Jun 2020 16:08:05 -0700
Subject: [PATCH] net: bpf: Implement bpf iterator for tcp

The bpf iterator for tcp is implemented. Both tcp4 and tcp6
sockets will be traversed. It is up to bpf program to
filter for tcp4 or tcp6 only, or both families of sockets.

Signed-off-by: Yonghong Song <yhs@fb.com>
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
Acked-by: Martin KaFai Lau <kafai@fb.com>
Link: https://lore.kernel.org/bpf/20200623230805.3987959-1-yhs@fb.com

通过一个简单的 BPF 程序,我们可以遍历所有的 TCP 连接,过滤出我们的目标连接:

SEC("iter/tcp")
int tcp_conn(struct bpf_iter__tcp *ctx)
{
 struct sock_common *skc = ctx->sk_common;
 struct tcpconn t = {};
 struct tcp_sock *tp;

 if (!skc)
  return 0;

 tp = bpf_skc_to_tcp_sock(skc);
 if (!tp)
  return 0;

 if (skc->skc_state != TCP_ESTABLISHED)
  return 0;

 /* Add your filters here */

 t.saddr = skc->skc_rcv_saddr;
 t.daddr = skc->skc_daddr;
 t.sport = skc->skc_num;
 t.dport = bpf_ntohs(skc->skc_dport);
 t.seq = tp->snd_nxt;
 t.ack_seq = tp->rcv_nxt;
 bpf_seq_write(ctx->meta->seq, &t, sizeof(t));
 return 0;
}

怎么样?很简单吧。再配合用户态的 Raw Socket,我们可以打造一个现代的 tcpkill,不需要连接上有流量,也能关闭一个连接。

bpf_sock_destroy

如果你的内核足够新,我们还可以使用 bpf_sock_destroy 这个 kfunc,这是 Linux v6.5 的新特性,在大约一年前进入内核主线。

有了它,我们可以丢掉上述的 Raw Socket,直接在 BPF 程序中结束一个 TCP 连接:

SEC("iter/tcp")
int tcp_conn(struct bpf_iter__tcp *ctx)
{
 struct sock_common *skc = ctx->sk_common;
 struct tcpconn t = {};
 struct tcp_sock *tp;

 if (!skc)
  return 0;

 tp = bpf_skc_to_tcp_sock(skc);
 if (!tp)
  return 0;

 if (skc->skc_state != TCP_ESTABLISHED)
  return 0;

 /* Add your filters here */

 bpf_sock_destroy(skc);
 return 0;
}

原创文章,作者:速盾高防cdn,如若转载,请注明出处:https://www.sudun.com/ask/58632.html

(0)
速盾高防cdn's avatar速盾高防cdn
上一篇 2024年5月16日 上午1:03
下一篇 2024年5月16日 上午1:04

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注