packetdrill入门笔记

使用 packetdrill 做一些有趣的TCP网络实验!


致谢

笔记的主要来源为 深入理解 TCP 协议:从原理到实战

image


环境搭建

实验环境 Ubuntu 16.04

安装相关的依赖

1
sudo apt-get install -y git make bison flex

下载工具源码

1
git clone https://github.com/google/packetdrill.git

修改源码内容:

  • 修改文件 gtests/net/packetdrill/netdev.c ,注释掉 set_device_offload_flags 函数以及调用它的地方
  • 修改文件 gtests/net/packetdrill/Makefile,去掉最后的 -static

编译项目

1
2
./configure
./make

编译成功的话在此目录下会生成可执行文件 packetdrill ,测试其是否能运行

1
2
# sudo ./packetdrill <文件名>
sudo ./packetdrill

第一个例子

这个例子中的流程是:

  1. packetdrill 模拟建立一个虚假的server
  2. packetdrill 操作协议栈模拟一个client发的报文
  3. 三次握手建立连接
  4. server向client发送一段数据
  5. client主动断开连接
  6. 四次挥手解除连接

pkt文件如下

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

// 三次握手
+0 < S 0:0(0) win 4000 <mss 100>
+0 > S. 0:0(0) ack 1 <...>
+.1 < . 1:1(0) ack 1 win 1000
+0 accept(3, ..., ...) = 4

// 服务端发送给客户端10字节数据包
+0.2 write(4, ..., 10) = 10
+0.0 > P. 1:11(10) ack 1
+0.0 < . 1:1(0) ack 11 win 1000

// 客户端主动断开,四次挥手
+0 < F. 1:1(0) ack 11 win 1000
+0 > . 11:11(0) ack 2
+0.1 close(4)=0
+0 > F. 11:11(0) ack 2 <...>
+0.01 < . 2:2(0) ack 12 win 1000

+0 `echo finish`
+0 `sleep 1000000`

这里一行一行地分析,顺便复习一下Linux网络编程相关知识

服务端启动

1
0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3

表示第0秒,执行系统调用 socket ,并断言获得文件描述符,Linux中文件句柄为整形数字,这里为3,因为之前已经存在了三个默认的文件句柄,分别是标准输入0,标准输出1,错误输出2

对于系统调用 socket ,函数原型如下,可以使用man查看

1
2
3
4
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

第一次参数 domain 确定通信的类型,常用的如下

domain 描述
AF_INET IPv4因特网域
AF_INET6 IPv6因特网域
AF_UNIX UNIX域
AF_UPSPEC 未指定

对于 AF_INETAF_UNIX 区别详细见这篇文章: AF_INET域与AF_UNIX域socket通信原理对比

这里摘录一些内容:

AF_INET:跨机器传输数据

image

AF_UNIX:本机传输数据

image

第二个参数 type 确定套接字类型,部分类型摘录如下

类型 描述
SOCK_DGRAM 固定长度的、无连接的、不可靠的报文传递
SOCK_RAW IP协议的数据报接口(在POSIX.1中为可选)
SOCK_SEQPACKET 固定长度的、有序的、可靠的、面向连接的报文传递
SOCK_STREAM 有序的、可靠的、双向的、面向连接的字节流

第三个参数 protocol 默认为0表示为给定的域和套接字类型选择默认协议。 AF_INET 中, SOCK_STREAM 默认为TCP( IPPROTO_TCP ), SOCK_DGRAM 默认为UDP( IPPROTO_UDP )。

在这里第一个参数使用 ... 是指使用默认的类型,也就是 AF_INET


1
+0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0

对socket进行设置,地址复用,断言成功,返回值为0,函数原型如下。

1
2
int setsockopt(int sockfd, int level, int optname,
const void *optval, socklen_t optlen);

第一个sockfd 为socket的文件句柄,之前已经知道为3,第二个参数 level 根据官方文档,需要填写为 SOL_SOCKETTo manipulate options at the sockets API level, level is specified as SOL_SOCKET ) ,第三个参数 optval 为设置的参数,这里将其设置为1,表示打开这个配置,最后一个为传入参数的长度, int 类型一般长度为4。


1
+0  bind(3, ..., ...) = 0

表示绑定文件句柄3值指定的IP地址,函数原型如下:

1
2
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);

第二、三个参数使用默认值,为本地8080端口,断言执行成功,函数返回0。


1
+0  listen(3, 1) = 0

表示开始监听端口,并将数据输出到文件句柄1,也就是标准输出,断言执行成功,函数返回0。


TCP三次握手

1
2
3
4
5
// 三次握手
+0 < S 0:0(0) win 4000 <mss 100>
+0 > S. 0:0(0) ack 1 <...>
+.1 < . 1:1(0) ack 1 win 1000
+0 accept(3, ..., ...) = 4

这里就是TCP的三次握手了。

第一行的 < 表示输入的数据包(input packets), packetdrill 会构造一个真实的数据包,注入到内核协议栈。

之后, S 表示为 SYN 。其他的一些参数如下,这也是tcpdump中的表示方式

缩写 全称 描述
S SYN 开始同步
S. SYN+ACK SYN应答报文
F FIN 发送方完成数据发送
F. FIN+ACK
R RST 复位连接
P PSH 尽可能快地将数据送往接收进程
. 以上四个标志比特均置0

0:0(0) 冒号前后的两个数据表示数据包起止的index,括号中的为数据包的长度。 win 4000 表示窗口的大小。

尖括号中的值为TCP头部尾部的可选项,对于mssSYN 包也必须带上自身的 MSS (最大段大小)选项,这里的 MSS 大小为 1000


第二行的 > 表示断言协议栈会立刻回复包的内容,这里断言会返回 SYN+ACK 包,数据包长度为0.

返回的ack值这里是服务端告诉客户端,下一次发数据包是起始的index为1,服务器数据区index为0的地方已经有值了。

至于为什么明明 SYN 数据包长度为0但是却要占一位的原因如下:不占用序列号的段是不需要确认的(都没有内容确认个啥),比如 ACK 段。SYN 段需要对方的确认,需要占用一个序列号。后面讲到四次挥手那里 FIN 包也有同样的情况。


第三行,在0.1秒后向协议栈中压入模拟的报文,完成三次握手。

根据前一个ack的告知,这一次发送的数据包起始index为1,同时由于上一个数据包为 SYN 类型,所以客户端也告知服务端下一次发数据包是起始的index为1。


1
+0 accept(3, ..., ...) = 4

最后,断言 accept 系统调用执行成功,返回句柄4。函数原型如下

1
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

除了第一个传入socket句柄外,别的都使用默认的值。最后获取的句柄4,之后就使用这个句柄于远端的socket进行数据交互。


数据传输

1
2
3
4
// 服务端发送给客户端10字节数据包
+0.2 write(4, ..., 10) = 10
+0.0 > P. 1:11(10) ack 1
+0.0 < . 1:1(0) ack 11 win 1000

就像注释所言,这里的操作是服务端向客户端发送长度为10字节的数据。

第一行使用系统调用写长度为10的数据。

第二行断言协议栈中会产生相应的报文。

第三行模拟客户端返回ask报文,表示数据收到,并告知服务端下一次发送数据的起始index为11


TCP四次挥手

1
2
3
4
5
6
// 客户端主动断开,四次挥手
+0 < F. 1:1(0) ack 11 win 1000
+0 > . 11:11(0) ack 2
+0.1 close(4)=0
+0 > F. 11:11(0) ack 2 <...>
+0.01 < . 2:2(0) ack 12 win 1000

客户端主动断开,四次挥手结束连接。

第一行为 FIN 类型报文,模拟客户端主动断开连接。

第二行为服务端应答报文,表示知道了客户端断开连接的请求。

第三行为服务端执行系统调用 close ,关闭当前文件句柄,结束与远端socket的连接,断言执行成功。

第四行和第五行为服务端通知客户端结束连接。


至此,第一个测试文件分析完毕。


抓包

在使用 packetdrill 测试这份文件前,先启动 tcpdump 进行抓包,命令如下

1
2
3
4
# -t 表示不输出时间戳
# -i any 表示监听任意网卡
# port 表示监听端口8080
sudo tcpdump -t -i any port 8080

也可以加上 -XX 查看报文的原始数据。-c n 可以截取 n 个报文,然后结束

抓包结果如下

1
2
3
4
5
6
7
8
9
IP 192.0.2.1.38285 > bogon.http-alt: Flags [S], seq 0, win 4000, options [mss 100], length 0
IP bogon.http-alt > 192.0.2.1.38285: Flags [S.], seq 3016883410, ack 1, win 29200, options [mss 1460], length 0
IP 192.0.2.1.38285 > bogon.http-alt: Flags [.], ack 1, win 1000, length 0
IP bogon.http-alt > 192.0.2.1.38285: Flags [P.], seq 1:11, ack 1, win 29200, length 10: HTTP
IP 192.0.2.1.38285 > bogon.http-alt: Flags [.], ack 11, win 1000, length 0
IP 192.0.2.1.38285 > bogon.http-alt: Flags [F.], seq 1, ack 11, win 1000, length 0
IP bogon.http-alt > 192.0.2.1.38285: Flags [.], ack 2, win 29200, length 0
IP bogon.http-alt > 192.0.2.1.38285: Flags [F.], seq 11, ack 2, win 29200, length 0
IP 192.0.2.1.38285 > bogon.http-alt: Flags [.], ack 12, win 1000, length 0

前三行为三次握手,中间两行为数据传输,最后四行为四次挥手