ICMP Echo Request/Replyのパースとバイト変換ヘルパーの共通化

ICMPは経路情報の通知やエラー報告など多くのメッセージ種別を持ちますが、ここではpingの土台となるEcho Request(8)/Echo Reply(0)だけを対象にします。IPv4のUnsupportedVersionと同じように、それ以外のtypeはUnsupportedTypeとして一律エラーにします。

型設計

// ip-icmp-udp/include/ipicmpudp/icmp.hpp
enum class IcmpType: std::uint8_t
{
    EchoReply = 0,
    EchoRequest = 8,
};

struct IcmpEchoMessage
{
    IcmpType type;
    std::uint16_t identifier;
    std::uint16_t sequence;
    std::span<const std::byte> payload;
};

enum class IcmpParseError
{
    MessageTooShort,
    UnsupportedType,
    ChecksumMismatch,
};

std::expected<IcmpEchoMessage, IcmpParseError> parse_icmp_echo_message(std::span<const std::byte> message);
std::vector<std::byte> build_icmp_echo_request(
    std::uint16_t identifier,
    std::uint16_t sequence,
    std::span<const std::byte> payload
);

ARPのbuild_arp_requestはペイロードが常に28バイト固定だったためstd::arrayで返せましたが、ICMP Echoのデータ部は可変長です(Linuxのpingコマンドはデフォルトで56バイトを載せますが、これはツール側の慣習でありプロトコル上の制約ではありません)。そのためbuild_icmp_echo_request/build_icmp_echo_replystd::vector<std::byte>を返します。

チェックサムはIPv4ヘッダとは対象範囲が違う

チェックサムの計算アルゴリズム自体はIPv4ヘッダのものと同じ1の補数和なので、ipv4.hppで公開しているcompute_checksumをそのまま再利用します。

// ip-icmp-udp/src/icmp.cpp(抜粋)
if (compute_checksum(message) != 0)
{
    return std::unexpected(IcmpParseError::ChecksumMismatch);
}

IPv4ヘッダのチェックサムはヘッダ部分(オプション含む)だけが対象で、その先に続くペイロードは対象外でした。一方ICMPのチェックサムは、8バイトのヘッダに加えてデータ部まで含めたメッセージ全体が対象です。compute_checksumは「渡されたバイト列全体」を計算するだけの関数なので、どちらの用途にも同じ関数がそのまま使えます。対象範囲の違いは、呼び出し側がどのspanを渡すかだけで表現されています。

重複していたバイト変換ヘルパーを内部ヘッダへ切り出す

16bit整数をビッグエンディアンで読み書きする処理は、ARPパケットの実装(arp.cpp)にも、IPv4ヘッダの実装(ipv4.cpp)にも、ほぼ同じ内容がそれぞれ個別に存在していました。ICMPの実装でも同じものが必要になったところで、ip-icmp-udp内の2ファイル間(ipv4.cppicmp.cpp)に限定して共通化しました。

// ip-icmp-udp/src/byte_io.hpp
namespace ipicmpudp::detail
{
    inline std::uint16_t read_uint16(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]))
        );
    }

    inline void write_uint16(std::span<std::byte> dest, std::uint16_t value)
    {
        dest[0] = static_cast<std::byte>(value >> 8);
        dest[1] = static_cast<std::byte>(value & 0xFF);
    }
}

このヘッダはinclude/ipicmpudp/ではなくsrc/直下に置いています。ライブラリの公開APIではなく、ipv4.cppicmp.cppという実装側だけが使う内部ヘルパーだからです。複数の.cppから同じヘッダを#includeするため、関数にはinlineを付けています。付けないと、インクルードしたそれぞれの翻訳単位に同じ関数の実体ができてしまい、リンク時に多重定義エラーになります。

引数はstd::span<const std::byte>という動的extentのままにしています。呼び出し側にはpacket.subspan(2, 2)のような動的なsubspanも、bytes.subspan<2, 2>()のような静的extentのsubspanも混在していますが、静的extentのspan<T, 2>は動的extentのspan<T>へ暗黙変換できるため、どちらの呼び出し方でもそのまま渡せます。

参考リンク