构造 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
);