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