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

圖片描述

摘要: packetdrill是一個很是有用的用於測試網絡協議棧的工具,由 Google開發,它經常使用於對網絡協議棧進行迴歸測試,確保新的功能不會影響原有功能。本文主要介紹其基本原理、安裝、入門、測試腳本的編寫方法。

1.簡介

packetdrill是一個很是有用的用於測試網絡協議棧的工具,由Google開發,它經常使用於對網絡協議棧進行迴歸測試,確保新的功能不會影響原有功能。它支持Linux, FreeBSD, OpenBSDNetBSD內核。它使用腳本化的語言編寫測試語句,預測協議棧輸出,官方也提供了許多測試腳本的例子。python

2.原理

packetdrill的總體框架以下圖所示linux

arch

packetdrill應用內部模擬了一個鏈接的Remote端Local端。其中Remote端用做遠端發送到本機報文的通道,咱們能夠在packetdrill應用內向tun設備寫入IP報文,對內核協議棧來來,這至關於從遠端收到了這個IP報文,再通過路由,這個報文會上送協議棧。反過來講,內核協議棧的向Remote端發送的報文會經過這個tun設備回到packetdrill應用,這時,咱們能夠經過比對其輸入,驗證協議棧的功能正確性。git

腳本文件是以.pkt爲後綴的文件,packetdrill啓動後讀取該文件,腳本解析器將每一行腳本語句其解析爲運行時event腳本運行機依次執行每一個eventgithub

3.安裝

packetdrill依賴的 package: gccpythonflexbison編程

官方github下載源代碼後,編譯便可bash

> ./configure
> make

4.入門

執行一個測試腳本

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

若是沒有任何輸出,就表示腳本測試經過了:),不然,它會提示哪一行腳本不知足預期以及錯誤緣由分別是什麼網絡

好比在個人機器上(內核版本4.4.0)執行下面腳本的時候出現了錯誤:app

> ./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框架

出現這種錯誤的緣由是腳本適合的內核版本的協議棧實現與我本機版本中的不一致!內核版本不一致,協議棧的某些實現就不一致!遇到這種狀況時,能夠簡單的修改腳本以適應咱們本身使用的內核版本。socket

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

(完)

相關文章
相關標籤/搜索