packetdrill:測試TCP協議棧行爲的利器

在這裏插入圖片描述

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][3]下載源代碼後,編譯即可

> ./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中和當前內核中的定義一致,否則也會報錯!

(完)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章