1.簡介
packetdrill
是一個非常有用的用於測試網絡協議棧的工具,由Google
開發,它常用於對網絡協議棧進行迴歸測試,確保新的功能不會影響原有功能。它支持Linux, FreeBSD, OpenBSD與NetBSD內核。它使用腳本化的語言編寫測試語句,預測協議棧輸出,官方也提供了許多測試腳本的例子。
2.原理
packetdrill
的整體框架如下圖所示
packetdrill
應用內部模擬了一個連接的Remote端
和Local端
。其中Remote端
用作遠端發送到本機報文的通道,我們可以在packetdrill
應用內向tun
設備寫入IP
報文,對內核協議棧來來,這相當於從遠端收到了這個IP
報文,再經過路由,這個報文會上送協議棧。反過來說,內核協議棧的向Remote端
發送的報文會通過這個tun
設備回到packetdrill
應用,這時,我們可以通過比對其輸入,驗證協議棧的功能正確性。
腳本文件是以.pkt
爲後綴的文件,packetdrill
啓動後讀取該文件,腳本解析器
將每一行腳本語句其解析爲運行時event
,腳本運行機
依次執行每個event
。
3.安裝
packetdrill
依賴的 package: gcc
、python
、flex
、bison
從[官方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
中和當前內核中的定義一致,否則也會報錯!
(完)