Linux Tcp内核协议栈Packet Drill基本原理是什么

16次阅读
没有评论

本篇内容介绍了“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 报文。

Linux Tcp 内核协议栈 Packet Drill 基本原理是什么

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 流程

Linux Tcp 内核协议栈 Packet Drill 基本原理是什么

接下来我们结合 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 来验证一下

Linux Tcp 内核协议栈 Packet Drill 基本原理是什么

通过结合 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 验证一下。

Linux Tcp 内核协议栈 Packet Drill 基本原理是什么

果然完全匹配。

Packet Drill 其实还有非常复杂而且更精巧的玩法,可以充分测试各种边界条件。以后有机会再和大家进一步分享

“Linux Tcp 内核协议栈 Packet Drill 基本原理是什么”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注丸趣 TV 网站,丸趣 TV 小编将为大家输出更多高质量的实用文章!