共计 5081 个字符,预计需要花费 13 分钟才能阅读完成。
本篇内容介绍了“Linux Tcp 内核协议栈 Packet Drill 基本原理是什么”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让丸趣 TV 小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!
Linux TCP 内核协议栈是一个非常复杂的实现, 不但沉淀了过去 20 多年的设计与实现,同时还在不停的更新。相关的 RFC 与优化工作一直还在进行中。如何研究和学习 Linux TCP 内核协议栈这样一块硬骨头就成了一大难题。
当然最重要也是最基本的还是要阅读相关的 RFC 和内核中的代码实现。这个是最最基本的要求。想要驯服 TCP 内核协议栈这样的 monster 仅仅浏览和静态分析代码是完全不够的。因为整个实现中充斥着各种边界条件和异常的处理(这里有部分原因是因为 TCP 协议本身设计造成的),尤其是 TCP 是有状态的协议, 很多边界条件的触发需要一系列的报文来构成,同时还需要满足时延等其它条件。
幸运的是 Google 在 2013 年替大家解决了这个难题。Google 在 2013 年发布了 TCP 内核协议栈 测试工具 Packet Drill。这个工具是名副其实,大大的简化了学习和测试 TCP 内核协议栈的难度。基本可以随心所欲的触摸 TCP 内核协议栈的每个细节。Google 的这件工具真是造福了人类。
使用 Packet Drill,用户可以随心所欲的构造报文序列,可以指定所有的报文格式 (类似 tcpdump 语法) 然后通过 TUN 接口和目标系统的 TCP 内核协议栈来通信,并对接收到的来自目标系统 TCP 内核协议栈 的报文进行校验,来确定是否通过测试。再进一步结合 wireshark+Packet Drill 用户可以获得最直观而且具体的体验。每个报文的每个细节都在掌控之中,溜得飞起,人生瞬间到达了巅峰。
Packet Drill 基本原理
TUN 网络设备
TUN 是 Linux 下的虚拟网络设备,可以直通到网络层。使得应用程序可以直接收发 IP 报文。
Packet Drill 脚本解析 / 执行引擎
首先 Packet Drill 脚本必须要被解析和分解为 通过传统 socket 接口收发报文的部分和通过 TUN 接口收发报文的部分
在传统 socket 接口执行对应的动作。
在 TUN 接口执行对应的动作,并对收到的数据进行比对。
在本文中 socket 接口主要扮演的是 server side 的角色。TUN 接口扮演的是 client 的角色。因而我们可以通过 TUN 接口完全掌控我们将要发送出去的 IP 报文,并受到 TCP 协议栈的反馈。并和预设数据进行比对。
Packet Drill 语法简介
相对时间顺序
Packet Drill 每一个事件 (发送 / 接收 / 发起系统调用) 都有相对前后事件的时间便宜。一般使用 +number 来表达。例如 +0 就是在之前的事件结束之后立即发起。+.1 表示为在之前时间结束 0.1 秒之后发起。以此类推
系统调用
Packet Drill 中集成了系统调用,可以通过脚本来完成例如 socket,bind,read,write,getsocketoption 等等系统调用。熟悉 socket 编程的同学很容易理解并使用。
报文的发送与接受
通过内核栈侧。可以通过调用系统调用 read/write 来完成报文的发送与接受。但是因为 tcp 是有状态的协议栈,所以内核栈本身也会根据协议栈所处状态发送报文(例如 ACK/SACK).
TUN 设备侧. Packet Drill 使用 表示发送报文,使用 表示接收报文。
报文的格式描述
报文格式的表达比较类似 tcpdump。例如 S 0:0(0) win 1000 表示 syn 包 win 大小为 1000,同时 tcp 的选项 mss (max segment size)为 1000.
下面我们通过 2 个例子来进一步学习
Handshake and Teardown
我们通过 packet drill 的脚本 复习一下这个经典的流程。
首选来回顾一下 TCP 协议标准的 handshake 和 treardown 流程
接下来我们结合 packet drill 的脚本来重现 整个过程
// 创建 server 侧 socket, server 侧 socket 将通过内核协议栈来通信 // 注意这里使用的是传统的系统调用 0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 // 设置对应的 socket options // 注意这里使用的是传统的系统调用 +0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 //bind socket // 注意这里使用的是传统的系统调用 +0 bind(3, ..., ...) = 0 //listen on the socket // 注意这里使用的是传统的系统调用 +0 listen(3, 1) = 0 // client 侧(TUN)发送 syn 握手的第一个报文 // 注意这里的语法 syn seq 都是相对的,从 0 开始。 +0 S 0:0(0) win 1000 mss 1000 // client 侧(TUN)期望收到的报文格式 syn+ack 且 ack.no=ISN(c)+1 // 参考标准流程图 最后的 ... 表示任何 tcp option 都可以 // 这里是握手的第二步 +0 S. 0:0(0) ack 1 ... // client 侧(TUN)发送 ack 报文 seq = ISN(c)+1, ack = ISN(c) +1 // 这里是握手的第三步 +.1 . 1:1(0) ack 1 win 1000 // 握手成功,server 侧 socket 返回 established socket // 这时通过 accept 系统调用拿到这个 stream 的 socket +0 accept(3, ..., ...) = 4 //server 侧向 stream 写入 10 bytes // 通过系统调用来完成写操作 +0 write(4, ..., 10)=10 //client 侧期望收到 receive 10 bytes +0 P. 1:11(10) ack 1 //client 侧应答 ack 表示接收到 10 bytes +.0 . 1:1(0) ack 11 win 1000 // client 关闭连接 发送 fin 包 +0 F. 1:1(0) ack 11 win 4000 // client 侧期望接收到 server 端的对于 fin 的 ack 报文 // 这里由内核协议栈发回。ack = server seq +1, seq = server ack // 参考标准流程图 +.005 . 11:11(0) ack 2 // server 关闭连接 通过系统调用完成 +0 close(4) = 0 // client 期望接收到的 fin 包格式 +0 F. 11:11(0) ack 2 // client 发送 server 端 fin 包的应答 ack 包 +0 . 2:2(0) ack 12 win 4000
至此,我们纯手动的完成了全部的发起和关闭连接的过程。然后我们用 wireshark 来验证一下
通过结合 packetdrill 与 wireshark 使得每一步都在我们的掌控之中,
SACK
我们将使用 packet drill 来探索一些更为复杂的案例。例如内核协议栈对于 SACK 中各种排列组合的响应。
SACK 是 TCP 协议中优化重传机制的一个重要选项(该选项一般都在报头的 options 部分)。
最原始的情况下如果发送方对于 每一个报文接受到 ACK 之后再发送下一个报文,效率将是极为低下的。引入滑动窗口之后允许发送方一次发送多个报文 但是如果中间某个报文丢失 (没有收到其对应的 ACK) 那么从那个报文开始,其后所有发送过的报文都要被重新发送一次。造成了极大的浪费。
SACK 是一种优化措施,用来避免不必要的重发,告知发送方那些报文已经收到,不用再重发。tcp 的选项中允许带有最多 3 个 SACK 的 options。也就是三个已经收到了得报文区间信息。说了这么多,还是有一些抽象,我们来看一个具体的示例。
示例说明
在下面的这个例子中,我们需要发送报文的顺序是 1,3,5,6,8,4,7,2 也就是测试一下内核 tcp 协议栈的 SACK 逻辑是否如同 RFC 中所描述的一样。
// 初始化部分建立服务器端 socket, 不再赘述 +0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3 +0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0 +0 bind(3, ..., ...) = 0 +0 listen(3, 1) = 0 // Client 端发送 握手报文以及接受服务器响应,不再赘述。这里注意激活了 SACK +.1 S 0:0(0) win 50000 mss 1000, sackOK,nop,nop,nop,wscale 7 +0 S. 0:0(0) ack 1 win 32000 mss 1000,nop,nop,sackOK +0 . 1:1(0) ack 1 win 50000 // Server 端就绪 +.1 accept(3, ..., ...) = 4 // 发送报文 1 +0 . 1:1001(1000) ack 1 win 50000 // 发送报文 3, 报文 2 被调整到最后发送 +0 . 2001:3001(1000) ack 1 win 50000 // 发送报文 5 报文 4 被调整乱序 +0 . 4001:5001(1000) ack 1 win 50000 // 发送报文 6 +0 . 5001:6001(1000) ack 1 win 50000 // 发送报文 8 报文 7 被调整乱序 +0 P. 7001:8001(1000) ack 1 win 50000 // 发送报文 4 +0 . 3001:4001(1000) ack 1 win 50000 // 发送报文 7 +0 . 6001:7001(1000) ack 1 win 50000 // 接收到第一个报文的 ACK +0 . 1:1(0) ack 1001 // 接收到 SACK, 报告收到了乱序的报文 3,但是没报文 2。 +0 . 1:1(0) ack 1001 win 31000 nop,nop,sack 2001:3001 // 接收到 SACK, 报告收到了乱序的报文 3,报文 5,但是没报文 2。没报文 4 +0 . 1:1(0) ack 1001 win 31000 nop,nop,sack 4001:5001 2001:3001 // 接收到 SACK, 报告收到了乱序的报文 3,报文 5,但是没报文 2。没报文 4 +0 . 1:1(0) ack 1001 win 31000 nop,nop,sack 4001:6001 2001:3001 // 接收到 SACK, 报告收到了乱序的报文 3,报文 5,6, 报文 8,但是没报文 2。没报文 4,没报文 7 +0 . 1:1(0) ack 1001 win 31000 nop,nop,sack 7001:8001 4001:6001 2001:3001 // 接收到 SACK, 报告收到了乱序的报文 3,4,5,6, 报文 8,但是没报文 2。没报文 7 +0 . 1:1(0) ack 1001 win 31000 nop,nop,sack 2001:6001 7001:8001 // 接收到 SACK, 报告收到了乱序的报文 3,4,5,6,7,8,但是没报文 2 +0 . 1:1(0) ack 1001 win 31000 nop,nop,sack 2001:8001 // 发送报文 2 至此所有报文完结 +0 . 1001:2001(1000) ack 1 win 50000 +0 . 1:1(0) ack 8001`
随后我们再来用 wireshark 验证一下。
果然完全匹配。
Packet Drill 其实还有非常复杂而且更精巧的玩法,可以充分测试各种边界条件。以后有机会再和大家进一步分享
“Linux Tcp 内核协议栈 Packet Drill 基本原理是什么”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注丸趣 TV 网站,丸趣 TV 小编将为大家输出更多高质量的实用文章!