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;

コメント