IPv4ヘッダをパースしてチェックサムを検証する
ARPパケットと違い、IPv4ヘッダには誤り検出用のチェックサムが含まれています。そのため単にフィールドを読み取るだけでなく、「このヘッダは壊れていないか」を検証する処理が必要です。
型設計
// ip-icmp-udp/include/ipicmpudp/ipv4.hpp
struct Ipv4Header
{
std::uint8_t header_length;
std::uint8_t dscp;
std::uint8_t ecn;
std::uint16_t total_length;
std::uint16_t identification;
bool dont_fragment;
bool more_fragments;
std::uint16_t fragment_offset;
std::uint8_t ttl;
IpProtocol protocol;
Ipv4Address source;
Ipv4Address destination;
};
enum class Ipv4ParseError
{
PacketTooShort,
UnsupportedVersion,
InvalidHeaderLength,
ChecksumMismatch,
};
std::expected<Ipv4Packet, Ipv4ParseError> parse_ipv4_packet(std::span<const std::byte> packet);Ipv4Headerにはバージョン番号とヘッダチェックサムそのものの値を持たせていません。パース時に検証はしますが、versionは「4であることを確認済み」、チェックサムは「一致することを確認済み」という事実だけが重要で、値そのものを後段のコードが使うことはないためです。チェックサム不一致はChecksumMismatchという通常の構造的パースエラーとして扱い、他のエラーと同じくstd::expectedで返します。ヘッダの形は正しいが中身が壊れているパケットを「パース成功として扱い、呼び出し側に検証させる」という設計も選べますが、他のパースエラーと同じ土俵に乗せる方がインターフェースとしてシンプルです。
インターネットチェックサムと「そのまま計算して0になればOK」というトリック
IPv4のヘッダチェックサムは、ヘッダを16bit単位に区切って1の補数和を取り、それをさらに1の補数反転したものです。
// ip-icmp-udp/src/ipv4.cpp
std::uint16_t compute_checksum(std::span<const std::byte> data)
{
std::uint32_t sum = 0;
for (std::size_t i = 0; i + 1 < data.size(); i += 2)
{
sum += read_u16(data.subspan(i, 2));
}
if (data.size() % 2 != 0)
{
sum += std::to_integer<unsigned>(data.back()) << 8;
}
while (sum >> 16)
{
sum = (sum & 0xFFFF) + (sum >> 16);
}
return static_cast<std::uint16_t>(~sum & 0xFFFF);
}std::uint32_tに足し込んでいるのは、16bit同士の和が16bitに収まらずキャリーが出ることがあるためです。このキャリーを上位ビットに捨てず下位16bitへ足し戻す(while (sum >> 16)のループ)のが「1の補数和」特有の演算で、これを「エンドアラウンドキャリー」と呼びます。
面白いのは検証側の実装で、パケット構築時と受信時で同じ関数をそのまま使い回せる点です。
// ip-icmp-udp/src/ipv4.cpp(抜粋)
if (compute_checksum(packet.subspan(0, header_length)) != 0)
{
return std::unexpected(Ipv4ParseError::ChecksumMismatch);
}構築側ではチェックサムフィールドを0にした状態でこの関数を呼び、返り値をチェックサムフィールドに書き込みます。一方、受信したヘッダを検証する側では、そのヘッダをチェックサムフィールドの値も含めてそのままcompute_checksumに渡すだけで、正しければ結果が必ず0になります。1の補数和の性質上、「チェックサムフィールドを含めた全体の和」は、正しいチェックサムが書き込まれていれば全ビット1(0xFFFF)になり、それをビット反転すると0x0000になるためです。フィールドを個別に0クリアしてから計算し直す、といった手間が要りません。
仕様書通りの構造体は存在しない
Ipv4Headerにはdscp/ecnという2つのフィールドがありますが、これはIPv4ヘッダの2バイト目そのものではありません。この1バイトは元々RFC 791で「Type of Service」(優先度3bit + 遅延/スループット/信頼性を表す3つのフラグ)として定義されたものですが、1998年のRFC 2474で上位6bitがDSCPとして再定義され、2001年のRFC 3168で残りの下位2bitがECNとして再定義されました。現在の実際のOS・ネットワーク機器はこのDSCP/ECN解釈で動いているため、コード側もそちらに合わせています。
.dscp = static_cast<std::uint8_t>(std::to_integer<unsigned>(packet[1]) >> 2),
.ecn = static_cast<std::uint8_t>(std::to_integer<unsigned>(packet[1]) & 0x03),RFCはビット位置とサイズをASCII図とテキストで定義しているだけで、dscpやfragment_offsetといったフィールド名自体はどこにも書かれていません。これらはビット図を読んでこちらで名付けたものです。仕様書を読むときは「この構造体のこのフィールド」に一対一で対応する記述を探すのではなく、ビット単位の図から自分で組み立てる必要があります。
read_u16とテストによる検証
16bitを読み出す補助関数read_u16は、std::byteを2つto_integerで整数化してから結合します。
// ip-icmp-udp/src/ipv4.cpp
std::uint16_t read_u16(std::span<const std::byte> bytes)
{
return static_cast<std::uint16_t>(
(std::to_integer<unsigned>(bytes[0]) << 8 | std::to_integer<unsigned>(bytes[1]))
);
}std::byteのoperator<<は結果もstd::byte(8bit)のままなので、シフト演算はto_integerでunsigned(32bit)へ変換した後に行う必要があります。8bitのまま8bit左シフトすると、ビットが上位に溢れて消えてしまうためです。
このread_u16を含むcompute_checksumが期待通りに動くことは、実際の値を手計算した期待値と突き合わせるテストで検証しています。
// ip-icmp-udp/tests/ipv4_parse_test.cpp
TEST_CASE("compute_checksum: classic textbook vector")
{
constexpr std::array<std::byte, 20> header = {
std::byte{0x45}, std::byte{0x00},
std::byte{0x00}, std::byte{0x3c},
std::byte{0x1c}, std::byte{0x46},
std::byte{0x40}, std::byte{0x00},
std::byte{0x40}, std::byte{0x06},
std::byte{0xb1}, std::byte{0xe6},
std::byte{0xac}, std::byte{0x10},
std::byte{0x0a}, std::byte{0x63},
std::byte{0xac}, std::byte{0x10},
std::byte{0x0a}, std::byte{0x0c},
};
CHECK(ipicmpudp::compute_checksum(header) == 0);
}パースが失敗するかどうかだけを見るテスト(PacketTooShortやUnsupportedVersionなど)とは異なり、このテストはread_u16が返す値そのものを、独立に手計算した期待値と突き合わせています。エラー種別の一致だけを見るテストでは検出できない意味的な誤りも、実際の値の一致を検証することで拾えます。
参考リンク
- RFC 791: Internet Protocol —— IPv4ヘッダの基本フォーマットを定義する一次資料です
- RFC 1071: Computing the Internet Checksum —— 1の補数和・エンドアラウンドキャリーによるチェックサム計算方法の仕様です
- RFC 2474: Definition of the Differentiated Services Field (DS Field) —— 「Type of Service」オクテットをDSCPとして再定義した文書です
- RFC 3168: The Addition of Explicit Congestion Notification (ECN) to IP —— 同オクテットの残り2bitをECNとして定義した文書です