Switch-Router

packetdrill--测试TCP协议栈行为的利器

Published at 2019-05-15 | Last Update 2019-05-15

摘要:packetdrill是一个非常有用的用于测试网络协议栈的工具,由Google开发,它常用于对网络协议栈进行回归测试,确保新的功能不会影响原有功能。本文主要介绍其基本原理、安装、入门、测试脚本的编写方法。

1.简介

packetdrill是一个非常有用的用于测试网络协议栈的工具,由Google开发,它常用于对网络协议栈进行回归测试,确保新的功能不会影响原有功能。它支持Linux, FreeBSD, OpenBSDNetBSD内核。它使用脚本化的语言编写测试语句,预测协议栈输出,官方也提供了许多测试脚本的例子。

2.原理

packetdrill的整体框架如下图所示

packetdrill应用内部模拟了一个连接的Remote端Local端。其中Remote端用作远端发送到本机报文的通道,我们可以在packetdrill应用内向tun设备写入IP报文,对内核协议栈来来,这相当于从远端收到了这个IP报文,再经过路由,这个报文会上送协议栈。反过来说,内核协议栈的向Remote端发送的报文会通过这个tun设备回到packetdrill应用,这时,我们可以通过比对其输入,验证协议栈的功能正确性。

脚本文件是以.pkt为后缀的文件,packetdrill启动后读取该文件,脚本解析器将每一行脚本语句其解析为运行时event脚本运行机依次执行每个event

3.安装

packetdrill依赖的 package: gccpythonflexbison

官方github下载源代码后,编译即可

> ./configure
> make

4.入门

执行一个测试脚本

> ./packetdrill  tests/linux/fast_retransmit/fr-4pkt-sack-linux.pkt
> 

如果没有任何输出,就表示脚本测试通过了:),否则,它会提示哪一行脚本不满足预期以及错误原因分别是什么

比如在我的机器上(内核版本4.4.0)执行下面脚本的时候出现了错误:

> ./packetdrill  tests/linux/listen/listen-incoming-ack.pkt 
tests/linux/listen/listen-incoming-ack.pkt:17: error handling packet: bad value outbound TCP option 3
script packet:  0.200000 S. 0:0(0) ack 1 <mss 1460,nop,nop,sackOK,nop,wscale 6>
actual packet:  0.201014 S. 0:0(0) ack 1 win 29200 <mss 1460,nop,nop,sackOK,nop,wscale 7>
>

它表示在执行到脚本第17行的时候出现了错误,脚本中remote端预期收到的SYNACK报文中wscale=6,但实际收到的报文中wscale=7

出现这种错误的原因是脚本适合的内核版本的协议栈实现与我本机版本中的不一致!内核版本不一致,协议栈的某些实现就不一致!遇到这种情况时,可以简单的修改脚本以适应我们自己使用的内核版本。

5. 脚本语言

packetdrill并没有使用某一种现成的脚本语言,它的脚本有一些tcpdump影子,又有一些socket编程的踪迹

// Test behavior when a listener gets an incoming packet that has
// the ACK bit set but not the SYN bit set.

0.000 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
0.000 bind(3, ..., ...) = 0
0.000 listen(3, 1) = 0

0.100 < . 0:0(0) win 32792 <mss 1000,sackOK,nop,nop,nop,wscale 7>
0.100 > R 0:0(0) win 0

// Now make sure that when a valid SYN arrives shortly thereafter
// (with the same address 4-tuple) we can still successfully establish
// a connection.

0.200 < S 0:0(0) win 32792 <mss 1000,sackOK,nop,nop,nop,wscale 7>
0.200 > S. 0:0(0) ack 1 <mss 1460,nop,nop,sackOK,nop,wscale 6>

0.300 < . 1:1(0) ack 1 win 320
0.300 accept(3, ..., ...) = 4

我觉得这种脚本最好的学习方法就是学习官方的例子了,在例子上依葫芦画瓢就可以构造出自己需要的脚本了!实在有疑惑还可以稍微翻翻代码!

时间戳

脚本以每行为单位,每一行都是时间戳 + 语句的形式。时间戳表示这条语句执行的时间,packetdrill支持绝对时间相对时间两种格式.

// 相对时间,上一条脚本0.1秒后
+.1  setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0

// 绝对时间,脚本开始运行后0.2秒后
0.200 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0

向协议栈注入报文

时间戳后跟着<符号的语句表示从remote端向协议栈注入报文,默认后面跟的是TCP报文的内容(当然也可以接其他协议,但协议栈的复杂之处大多在TCP)

//注入一个SYN报文(S表示SYN),起始序号和结束序号为0,数据长度为0,通告窗口大小为32792,携带了mss、sack和wscale的选项
0.200 < S 0:0(0) win 32792 <mss 1000,sackOK,nop,nop,nop,wscale 7>

从协议栈接收报文

时间戳后跟着>符号的语句表示remote端预期从协议栈接收报文。这里的预期接收时间是一个范围[ts-tolerance,ts+tolerance],容忍时间tolerance默认是4毫秒(可以通过运行参数改变)

// 预期收到一个SYNACK报文(.表示ACK) ACK序号是1 ,携带了mss、sack和wscale的选项
0.200 > S. 0:0(0) ack 1 <mss 1460,nop,nop,sackOK,nop,wscale 6>

系统调用

上面的报文语句是站在remote端的角度的,系统调用是站在local端看的,packetdrill支持以下的系统调用

struct system_call_entry system_call_table[] = {
	{"socket",     syscall_socket},
	{"bind",       syscall_bind},
	{"listen",     syscall_listen},
	{"accept",     syscall_accept},
	{"connect",    syscall_connect},
	{"read",       syscall_read},
	{"readv",      syscall_readv},
	{"recv",       syscall_recv},
	{"recvfrom",   syscall_recvfrom},
	{"recvmsg",    syscall_recvmsg},
	{"write",      syscall_write},
	{"writev",     syscall_writev},
	{"send",       syscall_send},
	{"sendto",     syscall_sendto},
	{"sendmsg",    syscall_sendmsg},
	{"fcntl",      syscall_fcntl},
	{"ioctl",      syscall_ioctl},
	{"close",      syscall_close},
	{"shutdown",   syscall_shutdown},
	{"getsockopt", syscall_getsockopt},
	{"setsockopt", syscall_setsockopt},
	{"poll",       syscall_poll},
	{"cap_set",    syscall_cap_set},
	{"open",       syscall_open},
	{"sendfile",   syscall_sendfile},
	{"epoll_create", syscall_epoll_create},
	{"epoll_ctl",    syscall_epoll_ctl},
	{"epoll_wait",   syscall_epoll_wait},
	{"pipe",         syscall_pipe},
	{"splice",       syscall_splice},
};

与我们习惯的的系统调用不一样的地方是,packetdrill中的系统调用中有一些参数是不可以更改的,我们需要填写...(脚本运行机会帮我们填上),另外还要设置其返回值。

// socket系统调用,返回 fd = 3,这里的3只在脚本范围中有效,运行时返回的描述符的值由框架内部维护,框架会维护它们的对应关系
0.000 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3

// setsockopt系统调用,第三个参数[1]表示一个指向数值1的指针
0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0

// listen系统调用,后两个参数由框架确定
0.000 bind(3, ..., ...) = 0
0.000 listen(3, 1) = 0

assert

有时我们还需要窥测TCP运行时的更多信息,比如双方协商的MSS是多少,当前的窗口大小cwnd是多少,慢启动阈值ssthresh是多少。这时我们可以使用assert语句来预期其状态

// 预期此时的状态信息
0.300 %{
assert tcpi_reordering == 3
assert tcpi_unacked == 10
assert tcpi_sacked ==  1
}%

packetdrill支持预期TCP信息如下:

/* packetdrill/gtests/net/packetdrill/tcp.h */
struct _tcp_info {
	__u8	tcpi_state;
	__u8	tcpi_ca_state;
	__u8	tcpi_retransmits;
	__u8	tcpi_probes;
	__u8	tcpi_backoff;
	__u8	tcpi_options;
	__u8	tcpi_snd_wscale:4, tcpi_rcv_wscale:4;
	__u8	tcpi_delivery_rate_app_limited:1;

	__u32	tcpi_rto;
	__u32	tcpi_ato;
	__u32	tcpi_snd_mss;
	__u32	tcpi_rcv_mss;

	__u32	tcpi_unacked;
	__u32	tcpi_sacked;
	__u32	tcpi_lost;
	__u32	tcpi_retrans;
	__u32	tcpi_fackets;

	/* Times. */
	__u32	tcpi_last_data_sent;
	__u32	tcpi_last_ack_sent;     /* Not remembered, sorry. */
	__u32	tcpi_last_data_recv;
	__u32	tcpi_last_ack_recv;

	/* Metrics. */
	__u32	tcpi_pmtu;
	__u32	tcpi_rcv_ssthresh;
	__u32	tcpi_rtt;
	__u32	tcpi_rttvar;
	__u32	tcpi_snd_ssthresh;
	__u32	tcpi_snd_cwnd;
	__u32	tcpi_advmss;
	__u32	tcpi_reordering;

	__u32	tcpi_rcv_rtt;
	__u32	tcpi_rcv_space;

	__u32	tcpi_total_retrans;

	__u64	tcpi_pacing_rate;
	__u64	tcpi_max_pacing_rate;
	__u64	tcpi_bytes_acked;    /* RFC4898 tcpEStatsAppHCThruOctetsAcked */
	__u64	tcpi_bytes_received; /* RFC4898 tcpEStatsAppHCThruOctetsReceived */
	__u32	tcpi_segs_out;	     /* RFC4898 tcpEStatsPerfSegsOut */
	__u32	tcpi_segs_in;	     /* RFC4898 tcpEStatsPerfSegsIn */

	__u32	tcpi_notsent_bytes;
	__u32	tcpi_min_rtt;
	__u32	tcpi_data_segs_in;	/* RFC4898 tcpEStatsDataSegsIn */
	__u32	tcpi_data_segs_out;	/* RFC4898 tcpEStatsDataSegsOut */
	__u64   tcpi_delivery_rate;

	__u64	tcpi_busy_time;      /* Time (usec) busy sending data */
	__u64	tcpi_rwnd_limited;   /* Time (usec) limited by receive window */
	__u64	tcpi_sndbuf_limited; /* Time (usec) limited by send buffer */
};

需要特别注意是,在使用assert时,我们要确定struct _tcp_info结构在packetdrill中和当前内核中的定义一致,否则也会报错!

(完)