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