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_replyはstd::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.cpp・icmp.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.cpp・icmp.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>へ暗黙変換できるため、どちらの呼び出し方でもそのまま渡せます。
参考リンク
- RFC 792: Internet Control Message Protocol —— ICMPのメッセージフォーマットとtype/codeの定義です
- IPv4ヘッダのチェックサム検証はIPv4ヘッダをパースしてチェックサムを検証するで扱っています