前言: 之所以翻译这篇文章是在搭建 NaïveProxy 的时候,看到有关 Do not turn on TCP Fast Open 的讨论
原文来源:The enemy of firewalls: TCP Fast Open
使用 Kimi 翻译,人工校正
去年在 SRECon16 Europe 会议上,我和 Booking.com 的一位工程师交谈时,他提到他们计划在 L3 负载均衡(ECMP)下的服务器农场部署TCP快速打开(TFO)。
我问他们是否在生产环境中部署了 TFO。第一个回答是:“我们没有,因为我们不知道如何在应用节点之间管理 TFO 密钥。”
然后我脑海中出现了一个问题:“为什么不使用自动化工具同步它们,或者定期这样做,比如在 cookie 内部插入时间戳?”
今天我回想起这个案例,想尝试看看它在现实中的表现如何。我不会深入探讨它的工作原理以及如何为客户端和服务器启用它。如果你需要关于 TFO 如何工作的基本信息,这篇文章非常有用。我将描述其他有用的内容,这些内容在之前提到的帖子中没有涵盖。
验证 TFO 是否工作
第一个验证它是否工作的工具是 tcpdump
:
使用 TFO 时:
07:03:45.442747 IP 1.1.1.1.41360 > 2.2.2.2.81: Flags [S], seq 3248579141:3248579160, win 29200, options [mss 1460,sackOK,TS val 0 ecr 0,nop,wscale 7,unknown-34 0x2cc5086de402d86a,nop,nop], length 19
07:03:45.442876 IP 2.2.2.2.81 > 1.1.1.1.41360: Flags [S.], seq 1989043609, ack 3248579161, win 28960, options [mss 1460,sackOK,TS val 37965533 ecr 0,nop,wscale 7], length 0
07:03:45.443034 IP 1.1.1.1.41360 > 2.2.2.2.81: Flags [.], ack 1, win 229, options [nop,nop,TS val 1277090187 ecr 37965533], length 0
不使用 TFO(常规的三次握手):
19:36:19.471384 IP 1.1.1.1.62248 > 2.2.2.2.81: Flags [S], seq 1237548774, win 65535, options [mss 1460,nop,wscale 5,nop,nop,TS val 932576322 ecr 0,sackOK,eol], length 0
19:36:19.471483 IP 2.2.2.2.81 > 1.1.1.1.62248: Flags [S.], seq 3862688090, ack 1237548775, win 28960, options [mss 1460,sackOK,TS val 83119562 ecr 932576322,nop,wscale 7], length 0
19:36:19.478781 IP 1.1.1.1.62248 > 2.2.2.2.81: Flags [.], ack 1, win 4117, options [nop,nop,TS val 932576329 ecr 83119562], length 0
你应该注意到第一个输出中的 unknown-34 0x2cc5086de402d86a
部分和 length 19
,这意味着,SYN 数据包携带了数据(19字节),并且 TFO 的 cookie 是 0x2cc5086de402d86a
。第二个例子显示了没有 TFO 选项的 TCP 常规三次握手。在我的示例中, SYN-ACK
没有携带任何数据,因为我没有使用任何响应请求的应用程序。
TFO 队列长度
在这种情况下,我安装了 HAProxy,它支持 TFO,但似乎没有简单的方法来监控 TFO 队列。使用 stap,你几乎可以像往常一样捕获几乎所有东西。
probe kernel.statement("tcp_fastopen_queue_check@net/ipv4/tcp_fastopen.c:234")
{
if ($fastopenq->qlen)
printf("%d/%d\n", $fastopenq->qlen, $fastopenq->max_qlen);
}
产生的输出:
关于 TCP/IP 内核栈如何处理 TCP 选项的一些信息
让我们谈谈这个在 RFC7413 中定义的选项:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Kind | Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
~ Cookie ~
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Kind 1 byte: value = 34
Length 1 byte: range 6 to 18 (bytes); limited by
remaining space in the options field.
The number MUST be even.
Cookie 0, or 4 to 16 bytes (Length - 2)
我开始深入研究 net/ipv4/tcp_input.c
,看看这些 TCP 选项是如何处理的,因为我不理解 Kind
和 Length
的含义。
我感兴趣的函数是:
void tcp_parse_options(const struct sk_buff *skb,
struct tcp_options_received *opt_rx, int estab,
struct tcp_fastopen_cookie *foc)
查看代码本身并不那么有信息量,除非打开另一个有说明的标签页。
所有 TCP 选项都立即放在 TCP 头部之后,因此这个 ptr = (const unsigned char *)(th + 1);
移动指针到选项开始的地方。
好的,我们有了指向选项的指针,现在我们需要逐个解析这些选项。 int opcode = *ptr++;
导致前述的 Kind
。
后来我们有一个循环,通过opsize = *ptr++;
迭代所有选项,这是定义为Length
的偏移量。
现在我们有了 Cookie
。迭代选项,直到你找到 TCPOPT_FASTOPEN
(这就是我们在 tcpdump 中看到的 unknown-34 ):
case TCPOPT_FASTOPEN:
tcp_parse_fastopen_option(
opsize - TCPOLEN_FASTOPEN_BASE,
ptr, th->syn, foc, false);
break;
这适当地调用了 tcp_parse_fastopen_option
,并将 cookie 值复制到 tcp_fastopen_cookie
结构中,以便进一步处理,这在 net/ipv4/tcp_fastopen.c
下。然后一切都几乎清晰明了,就像 RFC 中定义的那样:
struct sock *tcp_try_fastopen(struct sock *sk, struct sk_buff *skb,
struct request_sock *req,
struct tcp_fastopen_cookie *foc,
struct dst_entry *dst)
好的,但为什么 TFO 是防火墙的敌人呢?
连接中间的防火墙可能会导致 TFO 在传输中卡住 – 黑洞。这可能发生在以下场景中:
防火墙丢弃带有有效载荷的 SYN 数据包; 防火墙丢弃三次握手中带有有效载荷的 SYN/ACK 数据包。 结果,连接将长时间停滞,客户端将挂起。
这个问题可以通过引入 tcp_fastopen_blackhole_timeout_sec
sysctl 来解决,它将使时间呈指数增长以禁用活跃的 TFO。当 TFO 恢复正常时,它会减少这个时间。
总结
ip tcp_metrics show
命令在某些情况下很有用;nstat
有助于查看一些 TFO 活动;- TFO 目前还不是那么流行,但它正在加速发展(例如:TLS 1.3(False Start))。