Kill a TCP connection
How many ways do you have to kill a TCP
connection?
ptrace/GDB
In the old days, people invented ptrace
and GDB
, which provided us with an option to terminate a TCP
connection from outside the process.
Open two terminals and use nc
to run a server and client respectively:
# console 1
$ nc -l 127.0.0.1 1215
# console 2
$ nc 127.0.0.1 1215
Open another terminal and check the pid
and fd
of the process:
# 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))
Close this connection using 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]
After calling close
, we found that the server connection closed normally, but the client reported an error:
# 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
Because fd
is registered in select(2)
, this error is expected:
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.
...
This also tells us that this plan is not perfect.
tcpkill
Later, people invented BPF
, here refers to the classic BPF
, also known as cBPF
. So, we have a new choice: tcpkill
Similarly, open two terminals, use nc
to run a server and a client respectively:
# console 1
$ nc -l 127.0.0.1 1215
# console 2
$ nc 127.0.0.1 1215
Restart tcpkill
:
$ tcpkill -ilo tcp and port 1215
tcpkill: listening on lo [tcp and port 1215]
It can be seen that the TCP
connection is not directly closed. This is because tcpkill
needs to capture the network packets on the target connection, obtain the TCP sequence
, in order to construct the TCP RST
packet to close the connection.
By creating some traffic on the connection, the connection can be closed normally:
# 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
This solution is not perfect either, it needs to connect with traffic. If TCP Keepalive
is not turned on, there may be no opportunity to close the connection.
tcp_diag
Time came to 2015
, Google
engineers added the option CONFIG_INET_DIAG_DESTROY
in kernel commit c1e64e298b8c
:
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>
So that we can close a TCP
connection by sending a netlink
message, we can use the ss
tool for testing:
# console 1
$ nc -l 127.0.0.1 1215
# console 2
$ nc 127.0.0.1 1215
# console 3
$ ss -K dport = 1215
Very cool, except that programming with the netlink
interface is a bit troublesome, there is no problem.
bpf_iter_tcp
By 2020
, the kernel had something even cooler: 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
Through a simple BPF
program, we can traverse all TCP
connections and filter out our target connections:
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;
}
How about it? Quite simple, right? Coupled with the user-mode Raw Socket
, we can create a modern tcpkill
, which can close a connection without needing to connect to traffic.
bpf_sock_destroy
If your kernel is new enough, we can use the bpf_sock_destroy
kfunc
, a new feature of Linux v6.5
that entered the kernel mainline about a year ago.
With it, we can discard the above Raw Socket
, and directly end a TCP
connection in the BPF
program.
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;
}
Reference: https://mp.weixin.qq.com/s/LY1ectZffu1mgwk87V-HHA