工作中需要经常和协议栈打交道,有两种非常简单快速的方法可以对协议栈进行黑盒测试,帮助自己理解协议栈。
1. packetdrill
packetdriil就是为了测试linux协议栈而生的,由谷歌开源,在谷歌内部主要是用来对网络协议栈进行回归测试。
packetdrill以测试脚本的形式执行,安装packetdrill很简单,最基本的源码编译即可:
git clone https://github.com/google/packetdrill.git
cd packetdrill
./configure
make
在packetdrill/gtests/net/packetdrill/tests/linux/fast_retransmit目录,官方给了一个sample,在看这个例子之前,先来了解一下packetdrill的语法:
packetdrill脚本主要有四部分组成:数据包、系统调用、shell语句、python语句
- 数据包
数据包由尖括号开头,<代表输入,packetdrill会构造一个真实的数据包,注入协议栈;>代表输出,packetdrill会将其与真实输出的数据包做对比,如果不一致,则会报错。尖括号前面的+号,代表时间戳,即用户可以自己控制脚本中每个语句执行的时间或者预期发生的时间,带加号的是相对时间;不带加号的是绝对时间。如果在指定时间内,预期的行为没有发生,则packetdrill也会报错。 - 系统调用
系统调用可以调用linux内核的一些函数,例如bind(3, …, …)=0,3代表文件描述符,省略号允许脚本省略不重要的信息,等于号表示预期返回值,0即返回调用成功 - shell语句
shell语句在反引号里面 - python语句需要在百分号里面,经常用来做断言assert
下面我们来看下官方的例子,这里的快速重传指的是基于sack的快速重传,即收到三个连续的相同确认序列号的报文后,内核就会进行一次快速重传。
// Test fast retransmit with 4 packets outstanding, receiver sending SACKs.
// In this variant the receiver supports SACK.
// Establish a connection.
// 系统调用起一个监听端口
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
// 发送一个syn包,预期接收到一个synack包,再发送一个ack包,即三次握手。0:0(0)的含义是这个报文的起始序列号是0,终止序列号是0,总长度为0。ack即确认序列号。
+0 < S 0:0(0) win 32792 <mss 1000,sackOK,nop,nop,nop,wscale 7>
+0 > S. 0:0(0) ack 1 <...>
+.1 < . 1:1(0) ack 1 win 257
// 系统调用accept
+0 accept(3, ..., ...) = 4
// Send 1 data segment and get an ACK, so cwnd is now 4.
// 系统调用,写入(发送)一个1000字节包长的报文,预期收到这个报文并返回ack。
+0 write(4, ..., 1000) = 1000
+0 > P. 1:1001(1000) ack 1
+.1 < . 1:1(0) ack 1001 win 257
// Write 4 data segments.
// 写入4000字节的报文,由于mss是1000,所以会分成4个1000字节的tcp段发送
+0 write(4, ..., 4000) = 4000
+0 > P. 1001:5001(4000) ack 1
// Get 3 SACKs.
// 模拟丢包场景,发送三个sack,sack确认序列号为2001-5001,ack确认序列号为1001不变,也就是告诉对方1001-2001这个报文段我没有收到。
+.1 < . 1:1(0) ack 1001 win 257 <sack 2001:3001,nop,nop>
+0 < . 1:1(0) ack 1001 win 257 <sack 2001:4001,nop,nop>
+0 < . 1:1(0) ack 1001 win 257 <sack 2001:5001,nop,nop>
// We've received 3 duplicate ACKs, so we do a fast retransmit.
// 这样对方就会进行快速重传1001-2001这个这个报文段
+0 > . 1001:2001(1000) ack 1
// Receiver ACKs all data.
// 收到重传之后,在发送确认序列号6001,告诉对方全部报文已收到。
+.1 < . 1:1(0) ack 6001 win 257
执行./packetdrill fr-4pkt-sack-linux.pkt即可运行测试脚本,如果无输出则说明测试通过。执行脚本前可以开启tcpdump,过滤8080端口进行抓包,便于分析。
2. scapy
scapy不是一个专门用来测试linux协议栈的工具,它的强大之处在于可以简单轻松的配置每个包头的字段,常用于网络安全测试、畸形报文构造等。相较于packetdrill,这也是scapy最大的优势,因为packetdrill只能构造符合协议栈规范的报文,无法构造异常报文。
scapy采用类似OSI模型的分层结构来构造报文,我们可以使用ls命令来查看每一层支持配置的字段,安装scapy之后,命令行输入scapy即可进入交互模式。
‘/’操作符在每一层之间起到组合的作用,使用‘/’连接IP层和TCP层,我们可以轻松构造一个tcp报文:
之后执行send()即可发送报文:
scapy除了命令行模式之外,还可以在python代码里直接调用库,以下是一个第三次握手发送synack报文的异常测试的case:
from scapy.all import *
def test(ip, port):
# 第一次握手,发送SYN包
# 请求端口和初始序列号随机生成
# 使用sr1发送而不用send发送,因为sr1会接收返回的内容
ans = sr1(IP(dst=ip) / TCP(dport=port, sport=RandShort(), seq=RandInt(), flags='S'), verbose=False)
# 判断第二次握手包:ACK+SYN
assert ans[TCP].flags == 'SA', '端口未开放'
# 对方回复的目标端口,就是我方使用的请求端口(上面随机生成的那个)
sport = ans[TCP].dport
s_seq = ans[TCP].ack
d_seq = ans[TCP].seq + 1
# 第三次握手,发送异常确认包
ans = sr1(IP(dst=ip) / TCP(dport=port, sport=sport, ack=d_seq, seq=s_seq, flags='SA'), verbose=False)
# 判断是否回reset
assert ans[TCP].flags == 'R'
if __name__ == '__main__':
tcp_test("10.10.10.10", 80)
使用场景
- packetdrill发包速度快、可以在单机上测试,可以很方便的测试协议栈的复杂场景,scapy每一个收到的包都需要独自校验,不适用于复杂场景。
- scapy对时延不敏感,不适用于对时延敏感的场景;scapy可以简单构造异常报文,适用于对网络设备或者协议栈的流量攻击测试。