Why
为什么要终结一个已经存在的 tcp 连接?
- 强制实施新策略,如安全组、多活规则等。传统的防火墙基于 conntrack 模块(有状态),对于已经存在的连接一般不会实施新策略,比如我们想要即刻关掉某端口的对外开放,新策略只会对新连接生效,老连接则会忽略新策略,与我们预期不符。该场景下,我们可以人为终结不符合预期的连接来落实新策略
- 负载均衡,尤其是长连接。某些场景下,负载不均衡了,我们可以通过终结某些连接,让 client 重新发起请求,给 server一次重新负载均衡的机会。
How
你可能会说了,我直接代码内 close,四步挥手不就终结 tcp 连接了吗?是,这是 tcp 终结的正常渠道,但是一般应用不会自己去实现这个接口,更别提把这个接口暴露出来。我们的方案最好是让应用无感知(就不需要代码改造),才能多快好省地落地。所以可以依靠另一个 tcp 终结的渠道:Reset报文。通过在旁路,发送 Reset 报文,实现应用无感知的 tcp 连接终结。具体实现方式有以下几种:
tcpkill
Reset报文也不是随便发发就能终结 tcp 连接的,需要落在接收端接受窗口内,接收端才会接受这个 Reset 报文并终结自己这个连接1。
tcpkill 通过 libpcap2 嗅探到我们关注(通过 tcpdump 表达式体现)的连接的报文,提取 sequence 并构造 Reset 报文发送出去3,具体代码4概要如下:
// 构造报文
libnet_build_ip(TCP_H, 0, 0, 0, 64, IPPROTO_TCP,
ip->ip_dst.s_addr, ip->ip_src.s_addr,
NULL, 0, buf);
libnet_build_tcp(ntohs(tcp->th_dport), ntohs(tcp->th_sport),
0, 0, TH_RST, 0, 0, NULL, 0, buf + IP_H);
// 获取对方需要的序号和窗口大小
seq = ntohl(tcp->th_ack);
win = ntohs(tcp->th_win);
ip = (struct libnet_ip_hdr *)buf;
tcp = (struct libnet_tcp_hdr *)(ip + 1);
for (i = 0; i < Opt_severity; i++) {
ip->ip_id = libnet_get_prand(PRu16);
seq += (i * win);
tcp->th_seq = htonl(seq);
libnet_do_checksum(buf, IPPROTO_TCP, TCP_H);
if (libnet_write_ip(*sock, buf, sizeof(buf)) < 0)
warn("write_ip");
fprintf(stderr, "%s R %lu:%lu(0) win 0\n", ctext, seq, seq);
}
这里的 Opt_severity 设置是因为有些连接很快速,我们嗅探到报文并构造 Reset 报文的时候,原连接可能已经有好几次报文交互了,所以可以通过构造后续几个窗口的报文,来确保连接有更大的概率能被终结。
具体案例:server 监听 9090 端口,client 连接之并有报文交互,tcpkill 双向终结连接
tcpkill 通过嗅探报文来获得合法的 sequence,那么如果这个连接不活跃,嗅探不到报文怎么办?且看下文。
killcx/hping3
从 RFC5961 4.2 节5可以推出,当一个连接处于 established 状态的时候,若收到 syn 包则会返回一个包含本端预期 sequence 的所谓的 “challenge ack” 包(可以和 Linux 源码互相印证6789),有了这个合法 sequence,就可以发送 Reset 报文终结连接了。killcx10和 hping311 都可以做到这件事。
以 hping3 为例:server 监听 9099 端口,client 连接之,但无报文交互,hping3 终结 client 连接(非双向终结)
ebpf
无论是 tcpkill 还是 killcx/hping3,可编程性都比较弱,并且不是内核原生支持,限制较多。其实我们完全可以用 ebpf 来做这个事,只是对内核版本有一定要求,篇幅有限,这块我们放到下一篇文章讲。
引用
- https://www.ietf.org/rfc/rfc793.txt Reset Processing ↩
- https://www.tcpdump.org/pcap.html ↩
- https://linux.die.net/man/3/libnet10 libnet_build_ip libnet_build_tcp libnet_write_ip ↩
- https://github.com/tecknicaltom/dsniff/blob/master/tcpkill.c#L40 ↩
- https://www.ietf.org/rfc/rfc5961.txt ↩
- https://elixir.bootlin.com/linux/v6.6/source/net/ipv4/tcp_ipv... tcp_v4_do_rcv ↩
- https://elixir.bootlin.com/linux/v6.6/source/net/ipv4/tcp_inp... tcp_rcv_established ↩
- https://elixir.bootlin.com/linux/v6.6/source/net/ipv4/tcp_inp... tcp_validate_incoming syn_challenge ↩
- https://elixir.bootlin.com/linux/v6.6/source/net/ipv4/tcp_inp... ↩
- https://killcx.sourceforge.net/ ↩
- https://linux.die.net/man/8/hping3 ↩
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。