Perl Language Pack and unpack Constructing an IPv4 header


Sometimes you have to deal with structures defined in terms of C data types from Perl. One such application is the creation of raw network packets, in case you want to do something fancier than what the regular socket API has to offer. This is just what pack() (and unpack() of course) is there for.

The obligatory part of an IP header is 20 octets (AKA "bytes") long. As you can see behind this link, source and destination IP address make up the last two 32-bit values in the header. Among the other fields are some with 16 bits, some with 8 bits, and a few smaller chunks between 2 and 13 bits.

Assuming we have the following variables to stuff into our header:

my ($dscp, $ecn, $length,
    $id, $flags, $frag_off,
    $ttl, $proto,

Note that three fields from the header are missing:

  • The version is always 4 (it's IPv4 after all)
  • IHL is 5 in our example as we don't have an options field; length is specified in units of 4 octets so 20 octets gives a length of 5.
  • The checksum can be left at 0. Actually we'd have to calculate it but the code to do this doesn't concern us here.

We could try and use bit operations to construct e.g. the first 32 bits:

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

This approach only works up to the size of an integer though, which is usually 64 bits but can be as low as 32. Worse, it depends on the CPU's endianness so it will work on some CPUs and fail on others. Let's try pack():

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

The template first specifies H2, a 2-character hex string, high nybble first. The corresponding argument to pack is "45"—version 4, length 5. The next template is B8, an 8-bit bit string, descending bit order inside each byte. We need to use bit strings to control layout down to chunks smaller than a nybble (4 bits), so the sprintf() is used to construct such a bit string from 6 bits from $dscp and 2 from $ecn. The last one is n, an unsigned 16-bit value in Network Byte Order, i.e. always big-endian no matter what your CPU's native integer format is, and it is filled from $length.

That's the first 32 bits of the header. The rest can be built similarly:

B16sprintf("%03b%013b", $flags, $frag_off)Same as DSCP/ECN
C2$ttl, $protoTwo consecutive unsigned octets
n0 / $checksumx could be used to insert a null byte but n lets us specify an argument should we choose to calculate a checksum
N2$src_ip, $dst_ipuse a4a4 to pack the result of two gethostbyname() calls as it is in Network Byte Order already!

So the complete call to pack an IPv4 header would be:

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