Why
为什么要终结一个已经存在的 tcp 连接?
- 强制实施新策略,如安全组、多活规则等。传统的防火墙基于 conntrack 模块(有状态),对于已经存在的连接一般不会实施新策略,比如我们想要即刻关掉某端口的对外开放,新策略只会对新连接生效,老连接则会忽略新策略,与我们预期不符。该场景下,我们可以人为终结不符合预期的连接来落实新策略
- 负载均衡,尤其是长连接。某些场景下,负载不均衡了,我们可以通过终结某些连接,让 client 重新发起请求,给 server 一次重新负载均衡的机会。
上一篇文章,我们讲到了如何使用 tcpkill 和 killcx/hping3 来分别关闭活跃和非活跃连接。然而无论是 tcpkill 还是 killcx/hping3,可编程性都比较弱,并且不是内核原生支持,限制较多。
其实我们完全可以用 ebpf 来做这个事,闲话不多说,首先我们就来看看最终效果。
效果
server 监听 9099 端口,client 与其连接并发送消息,稍后运行我们基于 eBPF 的连接终结程序,结果:
可见在 server 侧终结了 tcp 连接,client 侧的连接也被 reset 了。
实现
首先,采用 ebpf 终结 tcp 连接的 idea 来自 lpc1,顺着里面的关键字,我们可以找到对应的 commit2,从中找到了我们需要的 bpf_sock_destroy
。
这里强调一下:学习 ebpf,Linux 的 selftests 是一个好素材。于是我们去找到对应的 test34,结合相关的文档5构造出了一个在 server 端终结特定 tcp 连接的程序。
内核态关键代码:
SEC("iter/tcp")
int iter_tcp_server(struct bpf_iter__tcp *ctx)
{
struct sock_common *skc = ctx->sk_common;
if (skc == (void *)0){
bpf_printk("sk_common not valid");
return 0;
}
if (skc->skc_family != AF_INET){
return 0;
}
int dport = bpf_ntohs(skc->skc_dport);
int sport = (skc->skc_num);
bpf_printk("sock_iter,sip4:%pI4n,dip4:%pI4n,sport:%u,dport:%u,server_port:%u,protocol:%u",
&skc->skc_rcv_saddr,&skc->skc_daddr,sport,dport,server_port,skc->skc_family);
// dport == 0 means serversocket
if (sport == server_port && dport > 0){
bpf_printk("sock_destroy,sport:%u,dport:%u,server_port:%u,cookie:%u",sport,dport,server_port,sock_cookie);
bpf_sock_destroy(skc);
}
return 0;
}
这里面的关键点:
- 当我们从 ctx 中获得 socket 后,发现是 server 端目标进程(server_port 标识)所属的 socket,即利用 bpf_sock_destroy 终结连接。
- 引入 bpf_sock_destroy 的 commit 时间是 2023.5,所以要想跑这个demo,内核要非常新(你可以查看这个 commit 合并到的内核版本,我直接升级到了 6.6.0)
用户态关键代码(基于 bpf skeleton):
skel = killer_bpf__open();
if (!skel) {
fprintf(stderr, "Failed to open and load BPF skeleton\n");
return 1;
}
skel->rodata->server_port = env.server_port;
/* Load & verify BPF programs */
err = killer_bpf__load(skel);
if (err) {
fprintf(stderr, "Failed to load and verify BPF skeleton\n");
goto cleanup;
}
struct bpf_link *link;
char buf[1024] = {};
int iter_fd = -1, len;
link = bpf_program__attach_iter(skel->progs.iter_tcp_client,NULL);
if (!link) {
fprintf(stderr, "Failed to attach_iter\n");
goto cleanup;
}
iter_fd = bpf_iter_create(bpf_link__fd(link));
if (iter_fd < 0) {
fprintf(stderr, "Failed to create_iter\n");
goto cleanup;
}
while (!exiting) {
sleep(20);
fprintf(stderr, "Begin to read attach_iter\n");
while ((len = read(iter_fd, buf, sizeof(buf) - 1)) > 0) {
buf[len] = 0;
fprintf(stderr, "%s\n", buf);
}
}
cleanup:
if (iter_fd >= 0)
close(iter_fd);
ring_buffer__free(rb);
bpf_link__destroy(link);
killer_bpf__destroy(skel);
这里核心有几点:
- iter/tcp 类型的 ebpf 程序不像 kprobe/uprobe 那样:auto attach 后就等着事件触发回调了,而是需要
bpf_program__attach_iter
、bpf_link__fd
、bpf_iter_create
等一系列操作获得一个描述符。然后对这个描述符发起read
系统调用,从而触发回调(本文的iter_tcp_server
) skel->rodata->server_port = env.server_port;
将参数传给内核程序,以便其按需终结 tcp 连接
原理
其实我们从 bpf 程序和 server/client 程序的输出,已经大概能够猜出实现原理了,不过我们还是从源码的角度来分析下。
首先是:
__bpf_kfunc int bpf_sock_destroy(struct sock_common *sock)
{
struct sock *sk = (struct sock *)sock;
/* The locking semantics that allow for synchronous execution of the
* destroy handlers are only supported for TCP and UDP.
* Supporting protocols will need to acquire sock lock in the BPF context
* prior to invoking this kfunc.
*/
if (!sk->sk_prot->diag_destroy || (sk->sk_protocol != IPPROTO_TCP &&
sk->sk_protocol != IPPROTO_UDP))
return -EOPNOTSUPP;
return sk->sk_prot->diag_destroy(sk, ECONNABORTED);
}
可见目前只对 tcp/udp socket 生效。
然后搜索 ECONNABORTED
,得到
#define ECONNABORTED 103 /* Software caused connection abort */
#define ECONNRESET 104 /* Connection reset by peer */
这里和 server 与 client 的输出对应上了。
最后,tcp v4 对应的 diag_destroy
是 tcp_abort
6,关键代码7是
if (!sock_flag(sk, SOCK_DEAD)) {
WRITE_ONCE(sk->sk_err, err);
sk_error_report(sk);
if (tcp_need_reset(sk->sk_state))
tcp_send_active_reset(sk, GFP_ATOMIC);
tcp_done(sk);
}
static inline bool tcp_need_reset(int state)
{
return (1 << state) &
(TCPF_ESTABLISHED | TCPF_CLOSE_WAIT | TCPF_FIN_WAIT1 |
TCPF_FIN_WAIT2 | TCPF_SYN_RECV);
}
可见,本质就是 abort 了本端连接(sk_err),并向对端发送了 reset 报文,实现了连接双向终结的目的,连接活跃与否都可行。
引用
- https://lpc.events/event/16/contributions/1358/ ↩
- https://github.com/torvalds/linux/commit/4ddbcb886268af8d12a23e6640b39d1d9c652b1b ↩
- https://lwn.net/ml/bpf/20230519225157.760788-10-aditi.ghag@is... ↩
- https://github.com/torvalds/linux/blob/2af9b20dbb39f6ebf9b9b6c090271594627d818e/tools/testing/selftests/bpf/progs/bpf_iter_tcp4.c ↩
- https://docs.kernel.org/bpf/bpf_iterators.html#how-to-use-bpf... ↩
- https://github.com/torvalds/linux/blob/v6.6/net/ipv4/tcp_ipv4.c#L3154 ↩
- https://github.com/torvalds/linux/blob/master/net/ipv4/tcp.c#L4483 ↩
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。