Linux Tcp 内核协议栈学习三种武器 之 Packet Drill

系统 Linux
Linux TCP 内核协议栈是一个非常复杂的实现, 不但沉淀了过去20多年的设计与实现,同时还在不停的更新。相关的RFC与优化工作一直还在进行中。如何研究和学习Linux TCP内核协议栈这样一块硬骨头就成了一大难题。

[[351019]]

Linux TCP 内核协议栈是一个非常复杂的实现, 不但沉淀了过去20多年的设计与实现,同时还在不停的更新。相关的RFC与优化工作一直还在进行中。如何研究和学习Linux TCP内核协议栈这样一块硬骨头就成了一大难题。

当然最重要也是最基本的还是要阅读相关的RFC和内核中的代码实现。这个是最最基本的要求。想要驯服TCP 内核协议栈这样的monster 仅仅浏览和静态分析代码是完全不够的。因为整个实现中充斥着各种边界条件和异常的处理(这里有部分原因是因为TCP协议本身设计造成的),尤其是TCP是有状态的协议, 很多边界条件的触发需要一系列的报文来构成,同时还需要满足时延等其它条件。

幸运的是Google在2013年替大家解决了这个难题。Google 在2013 年发布了TCP 内核协议栈 测试工具 Packet Drill。这个工具是名副其实,大大的简化了学习和测试TCP 内核协议栈的难度。基本可以随心所欲的触摸TCP 内核协议栈的每个细节。Google的这件工具真是造福了人类。PacketDrill GitHub link:

https://github.com/google/packetdrill/

使用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. 如果不熟悉报文格式, 可以先复习一下《TCP/IP协议详解》 卷1.

进一步的信息请参考 Drilling Network Stacks with packetdrill:

https://storage.googleapis.com/pub-tools-public-publication-data/pdf/41848.pdf

实战示例

下面我们通过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 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.

至此, 我们纯手动的完成了全部的发起和关闭连接的过程。然后我们用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` 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.

随后我们再来用wireshark 验证一下。

 

果然完全匹配。

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

参考信息

例子脚本的链接:

https://gitee.com/block_chainsaw/linux-kernel-tcp-study.git

本文转载自微信公众号「Linux阅码场」,可以通过以下二维码关注。转载本文请联系Linux阅码场公众号。  

 

责任编辑:武晓燕 来源: Linux阅码场
相关推荐

2010-08-31 11:14:32

2010-09-06 13:06:08

DB2 9.7

2010-06-13 14:54:40

TCP IP协议栈linux

2018-05-01 09:00:21

比特币数字货币区块链

2025-02-24 08:00:28

2019-09-30 09:28:26

LinuxTCPIP

2023-03-06 15:43:56

2021-07-06 21:29:16

TCPIP协议栈

2019-07-01 08:51:49

TCPIPLinux

2010-08-03 17:23:00

DB2 9.7三种武器

2021-07-09 08:55:23

LinuxTCPIP

2009-11-10 13:19:09

动态路由协议

2020-07-09 08:14:43

TCPIP协议栈

2010-09-08 15:11:36

TCP IP协议栈

2019-08-21 05:48:06

TCPIP协议栈

2019-09-18 08:53:55

2020-01-06 11:22:06

TCPLinux内核

2009-12-23 17:10:26

2011-11-08 16:32:24

LinuxFreeBSDTCP

2009-12-10 15:46:22

动态路由协议
点赞
收藏

51CTO技术栈公众号