【実務・中級編】 TCPチェックサムの計算アルゴリズムと擬似ヘッダー(Pseudo Header)の役割 – ネットワーク基礎とWebセキュリティ実践ガイド

はじめに:なぜ、今さら「TCPチェックサム」なのか?

Web APIの設計やインフラのオートスケーリング、コンテナネットワークのチューニング――。日々の開発現場で私たちが向き合うレイヤーは、どうしてもアプリケーション層やせいぜいHTTPヘッダーの解釈までになりがちです。

しかし、深夜の障害対応でパケットキャプチャ(tcpdumpやWireshark)を開いた瞬間、目の前にあるのは冷徹なバイナリの海です。APIが「502 Bad Gateway」を返す背後で、ロードバランサーとバックエンドのコンテナの間を流れるTCPセグメントが、実はトランスポート層で密かに悲鳴を上げているとしたら?

「チェックサムエラーなんて、今の高速なNIC(Network Interface Card)やハードウェアオフロードが全部よしなにやってくれるから関係ないよ」

そう思っていませんか? 確かに普段の開発で意識することは稀です。しかし、VXLANやGeneveといったカプセル化トンネルが飛び交う現代のクラウドネイティブなネットワーク環境や、複雑なeBPF(Extended Berkeley Packet Filter)のプログラムを書くシチュエーションにおいて、「TCPチェックサムがどのように計算され、なぜ『擬似ヘッダー(Pseudo Header)』という奇妙な仕組みが必要なのか」を理解しているか否かは、一流のインフラエンジニアと、単なるツールの使い手を分ける決定的な境界線になります。

今回は、RFC 793が定めたインターネットの根幹を支える整合性検証のメカニズムを、現場の視点から徹底的に紐解いていきましょう。

—

1. TCPチェックサムの基本メカニズムと「1の補数」の哲学

まずは基本のおさらいです。TCPチェックサムは、IPパケットのペイロード(TCPセグメント全体)が、送信元から宛先へ届くまでの間にビット化けや破損を起こしていないかを検証するためのものです。

16ビットの1の補数和(One’s Complement Sum)

現代のモダンなハッシュアルゴリズム(SHA-256やMD5など)に比べると、TCPのチェックサムは非常にプリミティブです。データ群を16ビット(2バイト)ずつ区切り、それらをすべて足し合わせ、最後にその結果の「1の補数(ビット反転)」を取ります。

なぜ、一般的な加算やCRCではなく「1の補数和」なのか?
それは、CPUの演算コストを極限まで下げるためです。1の補数による足し算は、「桁上がり(キャリー)が発生したら、それを最下位ビットに足し戻す(End-Around Carry)」という非常にシンプルなハードウェア回路で実装できます。パケットが毎秒何ギガビットも流れるルーターやNICにとって、この処理の軽さは正義なのです。

しかし、このプリミティブゆえの「弱点」が、実はネットワークエンジニアの頭を悩ませる原因になります。それが、宛先IPアドレスや送信元IPアドレスの改ざん(あるいはルーティングミス)を検知できないというジレンマです。

ここで登場するのが、今回の主役である「擬似ヘッダー」です。

—

2. 擬似ヘッダー(Pseudo Header)の正体と不可欠な役割

TCPセグメント単体をチェックサムの対象にするだけでは、致命的な欠陥があります。もし、ルーターのバグやメモリの故障によって、IPヘッダーに含まれる「宛先IPアドレス」が途中で書き換わってしまった場合、TCPセグメント自体のペイロードが無事であれば、純粋なTCPチェックサムは「正常(整合性あり)」と判断してしまいます。これでは、間違った宛先に届いたデータがそのままアプリケーション層に渡ってしまいます。

これを防ぐために、TCPはチェックサムを計算する瞬間だけ、IPヘッダーの一部の情報を借りてきて仮想的なヘッダー(擬似ヘッダー)を作り、それをTCPセグメントの先頭にくっつけて計算の対象にします。

擬似ヘッダーの構造(IPv4の場合)

IPv4の擬似ヘッダーは、合計12バイトのデータで構成されています。

0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       送信元IPアドレス                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                        宛先IPアドレス                         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  予約済 (ゼロ)  |   プロトコル  |          TCPセグメント長      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

1. 送信元IPアドレス (32ビット): IPヘッダーからそのままコピー。
2. 宛先IPアドレス (32ビット): IPヘッダーからそのままコピー。
3. 予約済 (8ビット): 常に 0x00 で埋められる。
4. プロトコル (8ビット): TCPを示す 6 (0x06)が入る。
5. TCPセグメント長 (16ビット): TCPヘッダーとデータの合計バイト数。

この擬似ヘッダーを含めてチェックサムを計算することで、「このTCPセグメントは、正しい送信元から、正しい宛先へ、正しいプロトコル(TCP)で届くべきものである」という文脈の縛りをチェックサムの中に強制的に焼き付けることができます。

> 現場のTips:
> IPv6の場合は、アドレス長が128ビットに拡張されるため擬似ヘッダーの構造も変わります(フロー情報などが絡むためサイズが大きくなります)。また、IPv6の基本ヘッダーにはトランスポート層のチェックサムを計算するための情報が足りないため、IPv6ではTCPチェックサムの計算が「必須」になりました(IPv4ではオプション扱いだった時代もあります)。

—

3. 実務での検証:Pythonによる擬似ヘッダー&チェックサム計算のシミュレーション

「百聞は一見にしかず」です。実際にTCPチェックサムがどのように計算されているのか、Pythonのコードを使って手を動かして理解しましょう。ネットワークプログラミングや低レイヤーのパケット解析ツール(Scapyなど)の基礎となるロジックです。

以下のスクリプトは、特定の送信元IP、宛先IP、およびTCPペイロードから、擬似ヘッダーを組み立ててチェックサムを算出するシミュレーションです。

import socket
import struct

def compute_ones_complement_sum(data: bytes) -> int:
    """16ビットの1の補数和を計算するヘルパー関数"""
    if len(data) % 2 != 0:
        # 奇数長の場合はパディングとしてゼロバイトを追加
        data += b'\x00'
    
    total_sum = 0
    for i in range(0, len(data), 2):
        # 2バイトずつ16ビット整数として取り出して足し合わせる
        word = struct.unpack('!H', data[i:i+2])[0]
        total_sum += word
        
    # 桁上がり(キャリー)を32ビット目から下位16ビットに折り返す
    while (total_sum >> 16) > 0:
        total_sum = (total_sum & 0xFFFF) + (total_sum >> 16)
        
    return total_sum

def calculate_tcp_checksum(src_ip: str, dst_ip: str, tcp_segment: bytes) -> int:
    """擬似ヘッダーを含めてTCPチェックサムを計算する"""
    # 1. IPアドレスをバイナリ形式に変換
    src_addr = socket.inet_aton(src_ip)
    dst_addr = socket.inet_aton(dst_ip)
    
    # 2. 擬似ヘッダーの各フィールドを構築
    reserved = 0
    protocol = socket.IPPROTO_TCP # 値は 6
    tcp_len = len(tcp_segment)
    
    # 3. 擬似ヘッダーのバイナリを作成 ( ! =ネットワークバイトオーダー/ビッグエンディアン )
    # フォーマット: 4バイト(src), 4バイト(dst), 1バイト(zero), 1バイト(proto), 2バイト(tcp_len)
    pseudo_header = struct.pack('!4s4sBBH', src_addr, dst_addr, reserved, protocol, tcp_len)
    
    # 4. 擬似ヘッダーとTCPセグメントを結合して和を計算
    # ※ 本来のTCPセグメント内のチェックサムフィールドは計算時は 0 で埋めておく必要があるため、
    # 実際のパケット生成時はセグメント側の該当オフセット(16-17バイト目等)を0クリアして渡すこと。
    total_data = pseudo_header + tcp_segment
    
    sum_result = compute_ones_complement_sum(total_data)
    
    # 5. 最終的な1の補数(ビット反転)を取る
    checksum = ~sum_result & 0xFFFF
    return checksum

# --- 実行例 ---
if __name__ == "__main__":
    # テスト用のIPアドレスとダミーのTCPセグメント(ヘッダー+データ)
    source_ip = "192.168.1.10"
    destination_ip = "10.0.0.5"
    
    # ダミーのTCPセグメント(チェックサムフィールドの2バイトは仮に 0x0000 とする)
    # ソートポート(2バイト), 宛先ポート(2バイト), シーケンス番号(4バイト), ...
    dummy_tcp_header_and_data = (
        b'\x04\xd2'  # Source Port: 1234
        b'\x00\x50'  # Destination Port: 80 (HTTP)
        b'\x00\x00\x00\x01' # Sequence Number: 1
        b'\x00\x00\x00\x00' # Acknowledgment Number: 0
        b'\x50'      # Data Offset (Header Length): 5 (20 bytes)
        b'\x02'      # Flags: SYN
        b'\x72\x10'  # Window Size: 29200
        b'\x00\x00'  # Checksum (計算対象時は 0 にする)
        b'\x00\x00'  # Urgent Pointer: 0
        b'Hello API' # Payload
    )
    
    chksum = calculate_tcp_checksum(source_ip, destination_ip, dummy_tcp_header_and_data)
    print(f"[*] 計算されたTCPチェックサム (Hex): 0x{chksum:04x}")

このコードを動かすと、IPアドレスが変わった瞬間にチェックサムの値がガラリと変わるのが確認できます。これが「擬似ヘッダーがIPルーティングの整合性を担保している」という物理的な証拠です。

—

4. トラブルシューティング:なぜチェックサムエラーが起きるのか?

実務でインフラを構築していると、次のような奇怪な現象に遭遇することがあります。

> 「Web APIサーバーへのリクエストは正常に届いているはずなのに、特定の大きなペイロード(POSTリクエストなど)を送信すると、途中で接続がリセット(RST)される、あるいはパケットが完全にドロップする」

この原因をパケットキャプチャで追っていくと、往々にして「TCPチェックスム・オフロード(Checksum Offload)の不整合」にぶつかります。

チェックサム・オフロードの罠

現代のOSやNICは、CPUの負荷を下げるために「TCPのチェックサム計算やパケットの分割(TSO: TCP Segmentation Offload)」をハードウェア(NIC)に肩代わりさせています。

しかし、次のような環境ではこの最適化が裏目に出ます。
1. 仮想化環境・コンテナ (Docker/Kubernetes)

  • veth ペアやブリッジネットワークを経由してパケットが転送される際、ホストOSのNICドライバと仮想インターフェースの間でチェックサム計算のタイミングが狂う。

2. ロードバランサー (ALB/NLB) やファイアウォールの手前

  • パケットを途中で改変(NATやSNAT)した際、ルーターやロードバランサーがTCPチェックサムを正しく再計算(Recalculate)し損ねると、次の受信ノードで「Checksum Incorrect」として容赦なくパケットが捨てられます。

現場でのデバッグ手法:tcpdump と ethtool

もしパケットロスや理由不明のコネクションリセットに悩まされたら、まずは手元のインターフェースでオフロード機能の状態を確認し、必要に応じて切り分けてみましょう。

Linux環境であれば、ethtoolコマンドでNICのオフロード設定を確認できます。

# eth0インターフェースのオフロード設定を確認する
$ sudo ethtool -k eth0

# 出力例(一部抜粋)
# tcp-segmentation-offload: on
# tx-checksum-ipv4: on
# rx-checksum-ipv4: on

もし特定の仮想ルーターやVPNトンネルの配下でパケットが化ける場合、一時的にオフロード機能を無効化(off)して挙動が変わるかテストするというのが、歴戦のエンジニアが使う古典的かつ確実なデバッグ手法です。

# 送信側のTCPチェックサムオフロードを一時的に無効化する
$ sudo ethtool -K eth0 tx off

*(※ 本番環境でこれをやるとCPU使用率が跳ね上がるので、あくまで検証用ウィンドウで行いましょう)*

また、Wiresharkやtcpdumpでキャプチャする際、「ローカルでキャプチャしたパケットのチェックサムが不正(Bad Checksum)に見える」というトラップにも注意してください。これは、送信側のOSが「あとでNICが計算してくれるから、今はチェックサムフィールドにゴミが入ったままでいいや」とサボった状態でパケットがキャプチャされてしまうために起きる現象です(OSがNICに処理を投げる手前でキャプチャしているため)。実際のネットワーク線路上に流れる際にはNICが正しく書き換えているため、実害がないことがほとんどです。この「見かけ上のエラー」に惑わされないことも、実務では重要なスキルとなります。

—

おわりに:バイナリの裏側にある「設計の美しさ」

普段私たちが何気なく叩いている fetch() や curl の背後では、今回紹介した擬似ヘッダーという「ほんの一手」が、インターネット全体の信頼性を静かに支えています。

# curlでAPIを叩くその瞬間も、レイヤー4では擬似ヘッダーが計算されている
$ curl -X POST "https://api.example.com/v1/data" \
     -H "Content-Type: application/json" \
     -d '{"status": "active"}'

AIがコードを書き、クラウドがインフラを自動構築してくれる現代だからこそ、こうした「なぜその仕様になっているのか」というパケットレベルのバックグラウンドを知っているエンジニアは強い。いざという時のトラブルシューティングの引き出しの数が、そのままシステムの可用性に直結するからです。

ネットワークの海に迷ったときは、思い出してください。パケットの頭には常に、宛先と送信元を見つめる「擬似ヘッダー」という名の羅針盤が隠されているということを。

コメント

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