積雲が映像制作したMV『RANGEFINDER』公開中
専門88IO

【C言語】IPv4ヘッダとTCPヘッダのチェックサム計算

専門

L3, L4ヘッダのチェックサム計算を調べてもよく分からなかった。友人に聞かれて疑似コードを書いたので、備忘録として残しておく。

環境はLinuxを想定して、ヘッダの構造体はLinuxカーネルのものを使う。あとC言語。

スポンサーリンク

チェックサム

チェックサムは該当データを2バイトごとに加算した後に論理否定を取ったもの。誤り検出に用いている。

IPv4ヘッダ、TCPヘッダのチェックサムフィールドはどちらも2バイトで、オーバーフローしたら2バイト単位で分割して足し合わせる。

  1. 0xFFFF + 0xFFFF = 0x0001FFFE (オーバーフロー)
  2. 0x0001 + 0xFFFE = 0x00010000 (2バイトで分割して加算)
  3. 0x0001 + 0x0000 = 0x0001 (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疑似ヘッダ構造

struct pseudo_hdr {
    __be32 saddr;
    __be32 daddr;
    __u8   _dummy;
    __u8   protocol;
    __be16 total_length;
}

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;

コメント

タイトルとURLをコピーしました