惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

SecWiki News
SecWiki News
I
InfoQ
The Cloudflare Blog
人人都是产品经理
人人都是产品经理
博客园 - Franky
T
Tailwind CSS Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
量子位
博客园_首页
罗磊的独立博客
V
V2EX
李成银的技术随笔
大猫的无限游戏
大猫的无限游戏
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
True Tiger Recordings
Vercel News
Vercel News
Cyberwarzone
Cyberwarzone
Cisco Talos Blog
Cisco Talos Blog
F
Fox-IT International blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
M
Microsoft Research Blog - Microsoft Research
Know Your Adversary
Know Your Adversary
爱范儿
爱范儿
The Register - Security
The Register - Security
G
Google Developers Blog
The Hacker News
The Hacker News
Malwarebytes
Malwarebytes
S
Securelist
博客园 - 三生石上(FineUI控件)
Jina AI
Jina AI
T
Threat Research - Cisco Blogs
T
The Exploit Database - CXSecurity.com
S
SegmentFault 最新的问题
博客园 - 叶小钗
F
Fortinet All Blogs
Apple Machine Learning Research
Apple Machine Learning Research
宝玉的分享
宝玉的分享
博客园 - 聂微东
T
Threatpost
博客园 - 【当耐特】
D
Docker
P
Privacy & Cybersecurity Law Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
G
GRAHAM CLULEY
V
Visual Studio Blog
C
Cisco Blogs
IT之家
IT之家
S
Security Archives - TechRepublic
Latest news
Latest news
阮一峰的网络日志
阮一峰的网络日志

蛮荆

如何获取更多的免费服务器 Kubernetes 调度器队列 - 设计与实现 Kubernetes 调度器 - 核心流程 Kubernetes Networking Model & CNI Kubernetes 控制器管理总结 Kubernetes CronJob 设计与实现 Kubernetes Job 设计与实现 Kubernetes HPA 设计与实现 Kubernetes Deployment 滚动更新实现原理 Kubernetes GC 设计与实现 Kubernetes Pod 驱逐 - 设计与实现 Kubernetes Daemonset 设计与实现 Kubernetes ReplicaSet 设计与实现 Kubernetes EndPoint 设计与实现 Kubernetes Informer 设计与实现 降本增效之应用优化 (三) 日志存储与检索 Kubernetes Pod 设计与实现 - 创建流程 Kubernetes 探针设计与实现 Unix 编程艺术名句摘录 Kubernetes - CRI 概述 Golang 编译速度为什么这么快? Kubernetes Pod 设计与实现 - Pause 容器 Kubernetes - kube-proxy 代理模式工程优化 Kubernetes 应用最佳实践 - 优雅关闭长连接 Kubernetes Service 类型和会话亲和性 Kubernetes 为什么需要 Ingress Kubernetes 架构 - 控制平面和数据平面 降本增效之应用优化 (二) 大报表 Go 语言如何获取 CPU 利用率 降本增效之应用优化 (一) Redis 业务规则引擎演变过程简述 微服务中的熔断算法 漏桶算法和令牌桶算法 jsonparser 为什么比标准库的 encoding/json 快 10 倍 ? zap 高性能设计与实现 HTTP Router 算法演进 布谷鸟过滤器 fastcache 高性能设计与实现 Web 常见的三个安全问题 ants Code Reading 布谷鸟过滤器 Go 线程安全 map 方案选型 布隆过滤器 死锁、活锁、饥饿、自旋锁 sync.Pool Code Reading Go 内存管理概述 Go netpoll Code Reading goroutine 泄漏与检测 time/Timer Code Reading GMP Scheduler Code Reading Go channel 的 15 条规则和底层实现 为什么 Linux “一切皆文件” context.Context Code Reading runtime/HACKING.md Goland 最佳实践 互联网开发与金庸武学 为什么 Redis 6.0 引入多线程模型? Kubernetes 应用最佳实践 - 金丝雀发布 容器中如何正确配置 GOMAXPROCS ? singleflight Code Reading sync.Map Code Reading sync.Cond Code Reading sync.WaitGroup Code Reading sync.RWMutex Code Reading sync.Mutex Code Reading sync.Once Code Reading Go 无锁编程 sync/atomic Code Reading goroutine 交替打印奇偶数 GODEBUG Go 并发模式 Go 汇编 UUID 通用技术选型 Kubernetes 应用最佳实践 - 水平自动伸缩 Go 高性能 Tips fasthttp 为什么比标准库 net/http 快 10 倍 ? 技术文章配图指南 ChatGPT 初体验 Docker 网络原理概览 iptables 的五表五链 Kubernetes 应用最佳实践 - 亲和性和污点容忍度 Go 的反射与三大定律 Docker 官方提供的最佳实践 Go 语言内置的设计模式 HTTP1 到 HTTP3 的工程优化 Kubernetes 应用最佳实践 - Sidecar 模式 Kubernetes 应用最佳实践 - init 容器和钩子函数 为什么 recover 必须在 defer 中调用? 为什么 defer 的执行顺序和注册顺序不同? Go map 设计与实现 Go 切片扩容底层实现 Go 语言中的零拷贝 Go Delve 云原生和边缘计算简介 Kubernetes Pod 服务质量等级 Kubernetes 应用最佳实践 - 探针 Kubernetes 应用最佳实践 - 资源请求和限制 CDN 原理 Kubernetes 应用最佳实践 - 开篇 缓存策略和模式
TCP Fast Open
2019-01-05 · via 蛮荆

2019-01-05 计算机网络

概述

在之前的文章 为什么 TCP 建立连接需要三次握手 中,关于下面 3 个问题给出了简单的回答:

  1. 第一次握手时可以携带应用数据吗?
  2. 第二次握手时可以携带应用数据吗?
  3. 第三次握手时可以携带应用数据吗?

简单来说,传统的 TCP 建立连接时需要三次握手,而且这三次握手只是发送简单的 SYNACK 报文。

从网络带宽的资源利用的角度来看,传输层的 TCP 头部 + 网络层的 IP 头部,最少有 40 个字节,为了发送几个字节的报文数据包,而额外组装了 40 个字节的头部,这有点类似 所谓的 “糊涂窗口综合症”

从应用优化的角度来看,因为要等到 TCP 经过三次握手建立连接之后才能发送应用层数据,所以会造成应用程序首次发送数据时存在一定的延迟,尤其是短连接、移动设备等场景中,这种副作用会加剧。

那么这种问题如何解决呢?使用 TFO 解决方案。

📝 PS: 因为 TCP 协议栈不同版本间存在差异,所以本文的前提是 TCP 三次握手时不传输数据 (传统的三次握手),事实上,很多云计算服务商提供的 Linux 发行版本都对网络协议栈进行了优化,会在 TCP 第三次握手时直接发送数据,读者自己抓包验证时,可能会和本文结果存在一定差异。


TFO

TCP Fast Open (TFO) 是在传统的三次握手基础上进行优化,允许在握手过程中发送数据,从而减少首次发送数据的延迟,提升网络应用性能

实现原理:

TFO 的核心原理是在发送方和接收方通信中,引入 1 个 Cookie 机制,这样使发送方在后续重连接收方时,能够简化 TCP 三次握手。

顾名思义,TFO Cookie 中的 Cookie 和 Web 应用层 中的 Cookie 机制一样,第一次访问时,需要登录验证,然后由服务端验证后,后续访问中可以直接携带,无需再次登录。

1. 首次连接

  • 当发送方第一次和接收方建立 TCP 连接时,发送 1 个 SYN 报文
  • 接收方返回 SYN-ACK 报文的同时,附带一个随机生成的名为 TFO Cookie 的标识符给发送方
  • 发送方收到 SYN-ACK 报文后,保存 TFO Cookie,发送 ACK 报文给接收方,完成三次握手,开始传输数据

2. 后续连接

  • 当发送方再次连接同一个接收方时,可以在 SYN 报文中携带上次保存的 TFO Cookie,同时在 SYN 报文中附带应用层数据 (也就是第一次握手时就直接发送数据)
  • 接收方验证发送方的 TFO Cookie 后,将数据发送给应用层处理,并返回 SYN-ACK 报文 (同时也可以发送数据)
  • 发送方收到 SYN-ACK 报文后,发送 ACK 报文给接收方,完成三次握手

优点

通过 TFO,发送方在发送 SYN 报文时就可以直接携带数据,接收方可以在第一次握手时直接处理数据,并且在第二次握手时直接发送数据,最终:

  • 发送方第一次发送数据,减少了 1.5 个 RTT 延迟
  • 接收方第一次发送数据,减少了 1 个 RTT 延迟

发送方第一次发送数据,减少了 1.5 个 RTT 延迟

接收方第一次发送数据,减少了 1 个 RTT 延迟

局限性

  1. 兼容性

需要通信双方都支持 TFO, 如果其中一方不支持,连接自动回退到传统的 TCP 连接建立过程,此外,通信链路中的转发设备 (NAT, 防火墙) 也会执行这个兼容性机制。

  1. 安全性

虽然 TFO 的 Cookie 是由接收方生成并发送给发送方的,并且每个 Cookie 都与发送方关联,但是增加了接收方的安全攻击面,可能引发诸如 “TCP SYN Flood” 放大攻击 等安全风险。

如果攻击者从被入侵主机获取到有效的 TFO Cookie,进而伪造了大量的携带数据报文,那么接收方就需要大量的内存来临时存储应用数据,最终导致内存耗尽。

  1. 部署环境要求

对内核版本有要求,且需要修改内核参数。

  1. 应用数据过大

如果发送方第一次要发送的数据大于 TCP 的 MSS, 依然需要拆包进行多次发送,当应用数据过大时,TCP Fast Open 带来的优势 (RTT 减少) 几乎可以忽略。


模拟环境

TFO 需要发送方和接收方同时支持,如果任意一方不支持 TFO,连接会自动回退到传统的三次握手方式。

为了演示效果,笔者使用了 2 个 Linux 服务器作为通信发送方和接收方,对应的发行版本和内核版本参数如下。

TFO 对 Linux 内核版本要求: >= 3.7。


# 发送方

## 发行版本 (WSL2 环境)
$ cat /etc/os-release 

PRETTY_NAME="Debian GNU/Linux 11 (bullseye)"
NAME="Debian GNU/Linux"
VERSION_ID="11"
VERSION="11 (bullseye)"
VERSION_CODENAME=bullseye
ID=debian

## 内核版本
$ uname -r

5.10.0-21-amd64

# 接收方

## 发行版本
$ cat /etc/os-release

NAME="CentOS Linux"
VERSION="7 (Core)"
ID="centos"
ID_LIKE="rhel fedora"
VERSION_ID="7"
PRETTY_NAME="CentOS Linux 7 (Core)"

## 内核版本
$ uname -r

3.10.0-1160.53.1.el7.x86_64

内核参数调整

TFO 启用需要修改默认内核参数:

  • 0:关闭 TFO
  • 1:启用发送方模式 TFO
  • 2:启用接收方模式 TFO
  • 3:同时启用发送方和接收方模式 TFO

# 发送方启用 TFO
$ echo 1 | sudo tee /proc/sys/net/ipv4/tcp_fastopen

# 接收方启用 TFO
# 写入 3 表示既启用发送方 TFO 也启用接收方 TFO
$ echo 3 | sudo tee /proc/sys/net/ipv4/tcp_fastopen

作为模拟实验,笔者只是临时修改了参数,可以采用如下步骤进行配置永久生效:

  1. 编辑 /etc/sysctl.conf 文件,添加配置项

net.ipv4.tcp_fastopen=3
  1. 运行 sysctl -p 命令生效,重启之后仍然有效

程序代码

如果读者使用主机的 curl 版本较高,可以直接使用如下方式直接开启 TFO 机制方式访问:


$ curl --tcp-fastopen http://example.com

# 可以使用如下方式确认 curl 版本是否支持 TFO
$ curl -V | grep -i TFO

因为笔者所使用的服务器中的 curl 版本较低,所以这里编写 Python 脚本代码,核心代码其实就是 2 个套接字的参数的设置而已。

接收方 (服务端) 代码

将接收方作为服务端程序的方式来实现,绑定/监听指定端口,然后接收来自发送方 (客户端) 的 TCP 连接。


# service.py

import socket

def listen():
    # 初始化服务端监听对象
    listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # 参数 32 表示 TCP 协议栈
    # 未完成的 Fast Open 队列长度
    listener.setsockopt(socket.SOL_TCP, socket.TCP_FASTOPEN, 32)

    # 监听 12345 端口号
    # 为了模拟,所以不用主流端口号了 :-)
    listener.bind(('0.0.0.0', 12345))
    # 最大连接数设置为 1024
    listener.listen(1024)

    print("Server is listening on port 12345...")

    # 轮询接收新的 TCP 连接
    while True:
        conn, addr = listener.accept()
        print(f"Accepted connection from {addr}")
        print(f"Received data: {conn.recv(1024)}")

        conn.send(b"Hello, Client")
        conn.close()

        print(f"Closed connection with {addr}")


if __name__ == "__main__":
    try:
        # 启动监听
        listen()
    except KeyboardInterrupt:
        # 捕获 Ctrl + C 终止程序
        print("Server shutting down...")

发送方 (客户端) 代码


# client.py

import socket

# 初始化客户端监听对象
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 参数 32 表示 TCP 协议栈
# 未完成的 Fast Open 队列长度
sock.setsockopt(socket.SOL_TCP, socket.TCP_FASTOPEN, 32)

# 向服务端发送数据时
# 设置 Fast Open 选项
sock.sendto(b"Hello, Server", socket.MSG_FASTOPEN, ("104.21.71.166", 12345))

print(f"Received data: {conn.recv(1024)}")

sock.close()

运行程序实验

程序核心代码 (总共 2 行) 准备就绪,接下来开始运行程序,验证 TCP Fast Open 过程。

服务端公网 IP: 104.21.71.166

1. 启动服务端程序,并确认监听状态


# 在 1 个终端启动服务端程序

$ python3 server.py

# 在另外 1 个终端查看程序监听状态是否正常

$ netstat -ant | grep 12345 | grep LISTEN

tcp        0      0 0.0.0.0:12345            0.0.0.0:*               LISTEN

2. 客户端开始抓包

打开 WireShark, 监听对应的网卡设备。

3. 运行客户端程序


# 为了验证效果,这里可以连续运行几次
# 每次运行间隔 3 - 5 秒即可
$ python3 client.py

# 输出省略
...

4. 查看客户端 TCP 连接状态


netstat -ant | grep 12345 | grep TIME_WAIT

# 输出如下
# 连续运行了多少次 client.py 
# 就会产生多少 TIME_WAIT 状态的 TCP 连接
# 10.0.0.53 为客户端的内网 IP 地址


tcp        0      0 10.0.0.53:38084         104.21.71.166:12345       TIME_WAIT  
tcp        0      0 10.0.0.53:37530         104.21.71.166:12345       TIME_WAIT  
tcp        0      0 10.0.0.53:37528         104.21.71.166:12345       TIME_WAIT  
tcp        0      0 10.0.0.53:38076         104.21.71.166:12345       TIME_WAIT  
tcp        0      0 10.0.0.53:38078         104.21.71.166:12345       TIME_WAIT  

...

一切运行正常,接下来就可以去看 WireShark 的抓包结果了。


WireShark 抓包结果分析

首先使用 tcp.options.tfo 过滤条件,快速筛选出和 TCP Fast Open 有关的 TCP 报文。

下面对 WireShark 抓包结果展开分析一下。

第一次建立连接

当发送方第一次和接收方建立 TCP 连接时,发送 1 个 SYN 报文,以及设置 TCP Options 字段 TCP Fast Open

此时并没有发送任何数据,所以 WireShark 抓包结果中的 Len = 0

接收方返回 SYN-ACK 报文的同时,附带一个随机生成的名为 TFO Cookie 的标识符给发送方。

发送方收到 SYN-ACK 报文后,保存 TFO Cookie,发送 ACK 报文给接收方,完成三次握手。

其中 TFO Cookie 的值为: d82d9074a6105a13

三次握手完成后,开始传输数据。

后续建立连接

通过截图可以看到,后续客户端和服务端建立 TCP 连接时,会在第一次握手时携带 FTO Cookie 并且直接发送数据,所以 WireShark 抓包结果中的 Len = 13

那么这个 13 是什么?就是客户端发送的数据,正好是 13 个字节。


conn.sendto(b"Hello, Server", ...)

后续 TCP 连接建立 (第一次握手) 时就可以直接发送数据 (篇幅所限,这里只截图 2 个数据抓包详情):

每个数据包中的 TFO Cookie 的值都是 d82d9074a6105a13,也就是第一次建立 TCP 连接时,服务端发送 SYN-ACK 报文时携带的值。


Reference