LinuxTCPKernelProtocolStack是一个非常复杂的实现,不仅积累了过去20年的设计和实现,而且还在不断更新。相关的RFC和优化工作还在进行中。如何研究和学习LinuxTCP内核协议栈这样的硬骨头,成了一个大问题。当然,最重要也是最基础的还是阅读相关的RFC以及内核中的代码实现。这是最基本的要求。仅仅浏览和静态分析代码并不足以驯服像TCP内核协议栈这样的怪物。因为整个实现充满了各种边界条件和异常处理(这部分是由于TCP协议本身的设计),尤其是TCP是一个有状态的协议,很多边界条件的触发需要一系列的消息来Composition,但是还需要满足延迟等其他条件。幸运的是,谷歌在2013年为大家解决了这个问题。谷歌在2013年发布了TCP内核协议栈测试工具PacketDrill,这个工具名副其实,大大简化了TCP内核协议栈的学习和测试难度。基本上,您可以随心所欲地接触TCP内核协议栈的每一个细节。谷歌的这个工具真的造福了人类。PacketDrillGitHub链接:https://github.com/google/packetdrill/使用PacketDrill,用户可以随意构造数据包序列,并可以指定所有数据包格式(类似于tcpdump语法),然后通过TUN接口和TCP与目标系统内核协议栈进行通信,检查从目标系统TCP内核协议栈接收到的报文,判断是否通过测试。进一步结合wireshark+PacketDrill用户可以获得最直观具体的体验。每一条消息的每一个细节都尽在掌控之中,飞扬而去,人生瞬间达到巅峰。PacketDrillTUN网络设备基本原理TUN是Linux下的一个虚拟网络设备,可以直接连接到网络层。这使应用程序能够直接发送和接收IP数据包。PacketDrill脚本分析/执行引擎首先,PacketDrill脚本必须被解析分解为通过传统socket接口收发消息的部分和通过TUN接口收发消息的部分,对传统的接口执行相应的操作插座接口。在TUN接口上执行相应的动作,比较接收到的数据。在本文中,socket接口主要扮演服务器端的角色。TUN接口扮演着客户端的角色。因此,我们可以完全控制我们将通过TUN接口发出的IP数据包,并接收来自TCP协议栈的反馈。并将其与预设数据进行比较。PacketDrill语法介绍相对时间顺序PacketDrill每个事件(发送/接收/启动系统调用)都有一个相对于前后事件的时间。一般用+number来表示。例如+0是在上一个事件结束后立即启动的。+.1表示在上一次结束后0.1秒发起。以此类推,PacketDrill中集成了系统调用,可以通过脚本完成socket、bind、read、write、getsocketoption等系统调用。熟悉socket编程的同学很容易理解和使用。消息通过内核堆栈端发送和接收。消息的发送和接收可以通过调用系统调用read/write来完成。但是因为tcp是有状态的协议栈,内核栈本身也会根据协议栈的状态发送数据包(比如ACK/SACK)。TUN设备端。PacketDrill使用<发送数据包,并使用>接收数据包arts。数据包格式说明数据包格式的表达与tcpdump类似。比如S0:0(0)win1000表示synpacketwinsize为1000,tcpoptionmss(maxsegmentsize)为1000,如果对报文格式不熟悉可以先复习一下.《TCP/IP协议详解》Volume1.Further更多信息请参考DrillingNetworkStackswithpacketdrill:https://storage.googleapis.com/pub-tools-public-publication-data/pdf/41848.pdf实例让我们进一步了解通过下面的2个示例进行握手和拆解。包钻的脚本回顾了这个经典的流程。第一选择是回顾一下TCP协议标准的握手和taredown过程。接下来结合包钻脚本重现整个过程//创建服务端socket,服务端socket会通过内核协议栈进行通信//注意这里使用的是传统的系统调用0socket(...,SOCK_STREAM,IPPROTO_TCP)=3//设置相应的socketoptions//注意这里使用的是传统的系统调用+0setsockopt(3,SOL_SOCKET,SO_REUSEADDR,[1],4)=0//bindsocket//注意这里使用的是传统系统调用+0bind(3,...,...)=0//listenonthesocket//注意这里使用的是传统系统调用+0listen(3,1)=0//clientside(TUN)sendsthefirstmessageofthesynhandshake//注意这里的语法synseq都是相对的,从0开始。+0//客户端(TUN)期望收到的消息格式为syn+ackandack.no=ISN(c)+1//参考最后一部分标准流程图的<...>意味着任何tcpoption都可以//这里是握手的第二步+0>S.0:0(0)ack1<...>//客户端(TUN)发送ackmessageseq=ISN(c)+1,ack=ISN(c)+1//这是握手的第三步+.1<.1:1(0)ack1win1000//握手成功,服务端sidesocketreturnsestablishedsocket//此时,通过accept系统调用获取到这个stream的socket+0accept(3,...,...)=4//server端向stream写入10bytes//完成通过系统调用写操作+0write(4,...,10)=10//客户端期望收到receive10bytes+0>P.1:11(10)ack1//客户端回复ack表示ithasreceived10bytes+.0<.1:1(0)ack11win1000//客户端关闭连接发送fin包+0.11:11(0)ack2//server通过系统调用关闭连接+0close(4)=0//client期望收到fin包format+0>F.11:11(0)ack2//client发送server端fin包的响应ack包+0<.2:2(0)ack12win4000至此,我们完成了所有的发起和处理手动关闭连接的过程。然后我们使用wireshark通过组合packetdrill和wireshark来验证每一步都在我们的控制之下,而SACK我们将使用packetdrill来探索一些更复杂的情况。比如内核协议栈响应SACK中的各种排列组合。SACK是TCP协议中优化重传机制的一个重要选项(该选项一般在头部的options部分)。在最原始的情况下,如果发送方在发送下一条消息之前收到每条消息的ACK,效率会极低。引入滑动窗口后,允许发送方一次发送多条消息,但是如果中间一条消息丢失(没有收到对应的ACK),那么从这条消息开始,所有后续发送的消息都会重发一次.造成了极大的浪费。SACK是一种优化措施,用来避免不必要的重传,并通知发送方那些消息已经收到,不需要重传。tcp选项中允许最多包含3个SACK的选项。即收到的三个消息的间隔信息??。说了这么多,还是有些抽象,我们来看一个具体的例子。实例说明下面的例子中,我们需要按照1,3,5,6,8,4,7,2的顺序发送数据包,即测试内核tcp协议栈的SACK逻辑是否如描述的那样RFC相同。//初始化部分建立server端socket,不再赘述+0socket(...,SOCK_STREAM,IPPROTO_TCP)=3+0setsockopt(3,SOL_SOCKET,SO_REUSEADDR,[1],4)=0+0bind(3,...,...)=0+0listen(3,1)=0//客户端发送握手报文,接受服务器响应,不再赘述。注意这里SACK+.1+0>S.0:0(0)ack1win32000+是activated0<.1:1(0)ack1win50000//Serverready+.1accept(3,...,...)=4//发送消息1+0<.1:1001(1000)ack1win50000//发送消息3、message2调到最后send+0<.2001:3001(1000)ack1win50000//发送message5message4调整乱序+0<.4001:5001(1000)ack1win50000//发送message6+0<.5001:6001(1000)ack1win50000//发送消息8和消息7乱序+0.1:1(0)ack1001//收到SACK,上报该包3被乱序接收,但数据包2没有。+0>.1:1(0)ack1001win31000//收到SACK,报告收到乱序包3和5,但没有包2。没有包4+0>.1:1(0)ack1001win31000//收到SACK,报收到乱序包3,包5,但没有包2。无消息4+0>.1:1(0)ack1001win31000//收到SACK,报告收到乱序消息3、消息5、6、消息8,但没有收到消息2。Nomessage4,nomessage7+0>.1:1(0)ack1001win31000//收到SACK,报告收到乱序消息3,4,5,6、数据包8,但没有数据包2。无消息7+0>.1:1(0)ack1001win31000//收到SACK,报告收到乱序消息3,4,5,6,7,8,butnomessage2+0>.1:1(0)ack1001win31000//发送消息2至此所有消息结束+0<.1001:2001(1000)ack1win50000+0>.1:1(0)ack8001`那我们用wireshark验证一下。果然,绝配。PacketDrill其实有一个非常复杂和更精巧的玩法,可以充分测试各种边界条件。以后有机会我会进一步把参考资料示例脚本的链接分享给大家:https://gitee.com/block_chainsaw/linux-kernel-tcp-study.git关注下方二维码。转载本文请联系Linux代码阅读领域公众号。