在一个线上问题排查过程中,出现了一个新的问题:wget 下载文件在连接建立很短的时间内(1 - 2s)就被 RESET 了。 首先先简要说明一下链路:

客户端经过一个SNAT设备出公网,在服务器上下载一个大文件。端到端的 RTT 大约 90 ms。由于最开始并没有怀疑到 SNAT 设备,并且 server 端无法抓包,我们分别在客户端 和 SNAT 设备到 server 间一台网元设备上抓包。 中间网元设备

SNAT 设备

如上,中间网元设备显示客户端(端口 34567)先发送了 RST 报文,随后服务器(端口 443)响应 RST 报文。但是客户端抓包显示它根本没有发送过 RST 报文。

此时,我们再从 SNAT 设备上抓包,发现就是 SNAT 设备首先给服务器发送了 RST 报文。

这里的 SNAT 设备实际上是一台基于 IPtables 的 Linux 设备。

抓包可以看到,RST 是对一个 TCP Spurious Retransmission 的响应。为什么会发出这个 RST 呢?正好网上有一个相似的场景:Add workaround for spurious retransmits leading to connection resets

总的来说,就是 spurious retransmits 报文在序列号超出 TCP 窗口时,会被 conntrack 认为是 invalid 包,从而不再经过反向 SNAT 规则的处理。由于目的地址没有被转换,报文会按照原来的目的地址送往 INPUT,而本地又没有这个 socket,则响应一个 RST 报文。

作者给出的两种 workarounds,第一种是不要让 conntrack 把这种超出窗口的包认为是 invalid,第二种是既然你把没有经过 DNAT 转换的包直接送给了我的 INPUT,那么我就在 INPUT 上把这个包给丢弃,防止协议栈发送错误的 RST。

显然,这两种 workarounds 都有不小的副作用。

be_liberal 生效代码:

__printf(6, 7)
static enum nf_ct_tcp_action nf_tcp_log_invalid(const struct sk_buff *skb,
						const struct nf_conn *ct,
						const struct nf_hook_state *state,
						const struct ip_ct_tcp_state *sender,
						enum nf_ct_tcp_action ret,
						const char *fmt, ...)
{
	const struct nf_tcp_net *tn = nf_tcp_pernet(nf_ct_net(ct));
	struct va_format vaf;
	va_list args;
	bool be_liberal;

	be_liberal = sender->flags & IP_CT_TCP_FLAG_BE_LIBERAL || tn->tcp_be_liberal;
	if (be_liberal)
		return NFCT_TCP_ACCEPT;

	va_start(args, fmt);
	vaf.fmt = fmt;
	vaf.va = &args;
	nf_ct_l4proto_log_invalid(skb, ct, state, "%pV", &vaf);
	va_end(args);

	return ret;
}
	res = tcp_in_window(ct, dir, index,
			    skb, dataoff, th, state);
	switch (res) {
	case NFCT_TCP_IGNORE:
		spin_unlock_bh(&ct->lock);
		return NF_ACCEPT;
	case NFCT_TCP_INVALID:
		nf_tcp_handle_invalid(ct, dir, index, skb, state);
		spin_unlock_bh(&ct->lock);
		return -NF_ACCEPT;
	case NFCT_TCP_ACCEPT:
		break;
	}
     in_window:
     // 

cursor 生成的代码 call stack:

  1. Invalid packet detection: When packets arrive with old sequence numbers outside the TCP window, tcp_in_window() in nf_conntrack_proto_tcp.c calls nf_tcp_log_invalid() which returns NFCT_TCP_INVALID.

  2. Conntrack bypass: Since conntrack marks the packet as invalid, DNAT rules don’t apply, so the packet reaches the actual destination IP/port in the headers.

  3. No matching connection: The packet arrives at the real destination where no established connection exists to handle it.

  4. Reset generation: The kernel calls tcp_v4_send_reset() (lines 2380 or similar call sites in tcp_ipv4.c) to send a RST packet back to the source.

  5. Connection termination: The RST packet has source/destination that matches the original NAT’d connection, causing it to be terminated.