構造 IPv4 標頭

有時你必須處理根據 Perl 的 C 資料型別定義的結構。一個這樣的應用程式是建立原始網路資料包,以防你想要做一些比常規套接字 API 提供的更好的東西。這正是 pack()(當然還有 unpack())的用武之地。

IP 報頭的必需部分是 20 個八位位元組(AKA位元組)長。正如你在此連結後面看到的那樣,源和目標 IP 地址構成標頭中的最後兩個 32 位值。其他欄位中有一些是 16 位,一些是 8 位,還有一些小塊,位於 2 到 13 位之間。

假設我們有以下變數填充到標題中:

my ($dscp, $ecn, $length,
    $id, $flags, $frag_off,
    $ttl, $proto,
    $src_ip,
    $dst_ip);

請注意,標題中的三個欄位缺失:

  • 版本總是 4(畢竟是 IPv4)
  • 國際人道法在我們的例子中是 5,因為我們沒有選項欄位; 長度以 4 個八位位元組為單位指定,因此 20 個八位位元組的長度為 5。
  • 校驗和可以保留為 0.實際上我們必須計算它,但這樣做的程式碼與我們無關。

我們可以嘗試使用位操作來構造例如前 32 位:

my $hdr = 4 << 28 | 5 << 24 | $dscp << 18 | $ecn << 16 | $length;

這種方法只能達到整數的大小,通常是 64 位但可以低至 32.更糟糕的是,它取決於 CPU 的位元組順序, 因此它可以在某些 CPU 上執行而在其他 CPU 上執行失敗。我們來試試 pack()

my $hdr = pack('H2B8n', '45', sprintf("%06b%02b", $dscp, $ecn), $length);

模板首先指定 H2,一個 2 字元的十六進位制字串,首先是高 nybble 。pack 的對應引數是 45 - 版本 4,長度為 5.下一個模板是 B8,一個 8 位的位字串,每個位元組內的降序位。我們需要使用位字串來控制佈局到小於 nybble(4 位)的塊,所以 sprintf() 用於構造來自 $dscp 的 6 位和來自 $ecn 的 2 位的字串。最後一個是 n網路位元組順序中無符號 16 位值,即無論你的 CPU 的原生整數格式是什麼,總是大端,並且它是從 $length 填充的。

這是標題的前 32 位。其餘的可以類似地構建:

模板 爭論 備註
n $id
B16 sprintf("%03b%013b", $flags, $frag_off) 與 DSCP / ECN 相同
C2 $ttl, $proto 兩個連續的無符號八位位元組
n 0 / $checksum x 可用於插入空位元組,但 n 允許我們指定一個引數,如果我們選擇計算校驗和
N2 $src_ip, $dst_ip 使用 a4a4 打包兩個 gethostbyname() 呼叫的結果,因為它已經在網路位元組順序中了!

因此,打包 IPv4 標頭的完整呼叫將是:

my $hdr = pack('H2B8n2B16C2nN2',
    '45', sprintf("%06b%02b", $dscp, $ecn), $length,
    $id, sprintf("%03b%013b", $flags, $frag_off),
    $ttl, $proto, 0,
    $src_ip, $dst_ip
);