L3, L4ヘッダのチェックサム計算を調べてもよく分からなかった。友人に聞かれて疑似コードを書いたので、備忘録として残しておく。
環境はLinuxを想定して、ヘッダの構造体はLinuxカーネルのものを使う。あとC言語。
チェックサム
チェックサムは該当データを2バイトごとに加算した後に論理否定を取ったもの。誤り検出に用いている。
IPv4ヘッダ、TCPヘッダのチェックサムフィールドはどちらも2バイトで、オーバーフローしたら2バイト単位で分割して足し合わせる。
最後に論理否定を取って0と1を反転させる。
IPv4ヘッダのチェックサム
IPヘッダ先頭からヘッダ末尾までを2バイトごとに足し合わせる。IPv4ヘッダの長さは4の倍数なので簡単。
IPv4ヘッダ長はihl
フィールドを2ビット左シフト(4倍)することで計算できる。
また、符号なし整数は2バイトで0〜255を表せるので、256個程度(512バイト分)の加算なら32ビットに収まる。32ビット符号なし整数で加算して、最後にオーバーフロー分を処理する方針で実装する。
// LinuxのIPv4ヘッダを想定 #include <linux/ip.h> struct iphdr *iph; /* IP チェックサム計算 */ uint16_t *pos = (uint16_t*)iph; uint32_t csum = 0; // チェックサムフィールドを加算対象から除くため事前に0埋め iph->check = 0; // 16ビットごとに加算 for (int i = 0; i < (iph->ihl << 2); i += 2) { csum += *pos; pos++; } // 16ビットから溢れた分を加算 csum = (csum & 0xFFFF) + (csum >> 16); if(csum >> 16) { csum = (csum & 0xFFFF) + (csum >> 16); } // 論理否定を取って16ビットにキャスト iph->check = (uint16_t)~csum;
TCPヘッダのチェックサム
TCPヘッダ場合は、IP疑似ヘッダとTCPヘッダの先頭からTCPペイロード末尾までを加算する。
そのためIPヘッダの情報が必要。チェック対象が重複することもあり、IPv6ヘッダにチェックサムフィールドはない。
IP疑似ヘッダのtotal_length
(便宜上)の部分にはIPv4ペイロード長が入る。
IPv4ヘッダのtot_len
フィールドはIPv4ヘッダ先頭からIPペイロード末尾なので、IPv4ペイロード長はtot_len
フィールドとihl
フィールドから計算できる。tot_len
はビックエンディアン(ネットワークバイトオーダー)での格納であることに注意。
また、IPv4のチェックサムと異なりペイロードも加算対象であることから奇数バイト長である可能性がある。末尾1バイトはリトルエンディアンであればビットシフトせずに8ビット符号なし整数として足し合わせればよい。
// LinuxのTCPヘッダを想定 #include <linux/ip.h> #include <linux/tcp.h> struct iphdr *iph; struct tcphdr *tcph; /* TCP チェックサム計算 */ uint16_t *pos = (uint16_t*)tcph; uint32_t csum = 0; // チェックサムフィールドを加算対象から除くため事前に0埋め tcph->check = 0; // 疑似IPヘッダを加算 /* struct pseudo_hdr { * __be32 saddr; * __be32 daddr; * __u8 _dummy; * __u8 protocol; * __be16 total_length; // L3ペイロード長(IPヘッダ含まない) * } */ csum += (iph->saddr & 0xFFFF) + (iph->saddr >> 16); csum += (iph->saddr & 0xFFFF) + (iph->saddr >> 16); // リトルエンディアンの場合、dummyの8ビット分シフト csum += iph->protocol << 8; // tot_lenはネットワークバイトオーダーなので変換が必要 uint16_t total_length = ntohs(iph->tot_len) - (iph->ihl << 2); csum += htons(total_length); // 16ビットごとに加算 for (int i = 0; i < total_length; i += 2) { csum += *pos; pos++; } // ペイロードが奇数バイトの場合、末尾8ビットを加算 // リトルエンディアンであればビットシフト不要 if (total_length % 2 == 1) { uint16_t *tail = (uint16_t*)pos; csum += *tail; } // 16ビットから溢れた分を加算 csum = (csum & 0xFFFF) + (csum >> 16); if(csum >> 16) { csum = (csum & 0xFFFF) + (csum >> 16); } // 論理否定を取って16ビットにキャスト tcph->check = (uint16_t)~csum;
コメント