【テクニカル・上級編】 IPヘッダー:プロトコル番号(Protocol Number)の定義と上位層の識別 – ネットワーク基礎とWebセキュリティ実践ガイド

夜な夜なWiresharkのキャプチャ画面を眺め、レイヤー3とレイヤー4の狭間で繰り広げられるパケットの息吹に酔いしれる——インフラエンジニアであれば、一度はそんなマニアックな夜を過ごしたことがあるはずだ。

現代のクラウドネイティブなアーキテクチャやゼロトラストの要塞において、私たちは日々、Kubernetesのネットワークポリシーや、複雑なEnvoyのルーティングルール、さらにはeBPFを用いた超高速な観測性に感嘆している。しかし、どれほど抽象化レイヤーが積み上がろうとも、すべての通信はベアメタル上の物理NICを叩き、IPv4やIPv6のヘッダーを纏ったビット列として荒野を駆け巡っている。その事実が揺らぐことはない。

今回は、そのIPパケットの心臓部にあって、上位層への運命の分岐点をつかさどる極めて重要な8ビット、「プロトコル番号(Protocol Number)」の深淵へと迫る。

—

1. パケット解剖学:IPヘッダーにおける「プロトコル番号」の正体

IPv4パケットを受け取ったLinuxカーネルのネットワーキングサブシステムは、その脳髄で瞬時に何を考えているだろうか。NICのリングバッファから引き上げられたパケットは、イーサネットフレームの剥離を経て、L3の入り口であるIP層(ip_rcv関数あたりだ)に到達する。

ここでIPv4ヘッダーを斜め上から俯瞰してみよう。

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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version|  IHL  |Type of Service|          Total Length         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         Identification        |Flags|      Fragment Offset    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Time to Live |   Protocol    |         Header Checksum       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       Source IP Address                       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Destination IP Address                     |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

この固定長20バイト(オプションなしの場合)のヘッダーの中央付近、ちょうどTTLの直後に位置する8ビットのフィールドこそが Protocol である。IANA(Internet Assigned Numbers Authority)によって厳密に管理されているこの数値は、ペイロード(L4以降のデータ)がどのプロトコルに属しているかを一意に指し示す。

  • 6 : Transmission Control Protocol (TCP)
  • 17 : User Datagram Protocol (UDP)
  • 1 : Internet Control Message Protocol (ICMP)
  • 50 : IPsec Encapsulating Security Payload (ESP)
  • 41 : IPv6 encapsulation (IPv6)

カーネルはこの8ビットの整数値を読み取った瞬間、内部のディスパッチテーブル(inet_protosハッシュテーブルなど)を参照し、次の処理を担当する関数群、すなわちトランスポート層のプロトコルスタックへポインタを華麗にパスする。TCPであれば tcp_v4_rcv へ、UDPであれば udp_rcv へ。この一瞬のルーティングこそが、OSがマルチプレックストランスポートを実現するための根幹なのだ。

—

2. カーネル内部のディスパッチメカニズムとeBPFの衝撃

かつてのレガシーなLinuxカーネルでは、このプロトコル番号に基づくハンドラの登録は静的または限定的な動的機構で行われていた。しかし、現代のハイパフォーマンスなデータプレーンにおいて、このハンドリングはさらに進化している。

例えば、XDP(eXpress Data Path)やTC(Traffic Control)フックを利用したeBPFプログラムを記述する際、私たちは直接このプロトコル番号を検証し、パケットをカーネルの重厚なスタックに到達させる前にドロップしたり、別のリングバッファへ迂回させたりすることができる。

以下は、受信したIPパケットのプロトコル番号をeBPFでインスペクトし、不正な、あるいは許可されていないプロトコル(例えば許可されていないプロトコルや特定のカプセル化パケット)を検知・処理するC言語風のeBPFコードの骨子だ。

#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <bpf/bpf_helpers.h>

SEC("xdp")
int xdp_protocol_inspector(struct xdp_md *ctx) {
    void *data = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;

    // イーサネットヘッダーの境界チェック
    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end)
        return XDP_PASS;

    // IPv4以外は今回はそのままスルー(IPv4のEthertypeは 0x0800)
    if (eth->h_proto != bpf_htons(ETH_P_IP))
        return XDP_PASS;

    // IPヘッダーの境界チェック
    struct iphdr *iph = (void *)(eth + 1);
    if ((void *)(iph + 1) > data_end)
        return XDP_PASS;

    // IPヘッダーから「プロトコル番号」を抽出
    __u8 proto = iph->protocol;

    // 例:TCP(6)とUDP(17)以外はロギングして特定の処理を行う(セキュリティ監査の文脈)
    if (proto != 6 && proto != 17) {
        // ここで非標準プロトコル(ICMPやGRE、カスタムトンネルなど)を検知
        // bpf_trace_printk("Detected non-standard L4 protocol: %d\n", proto);
        
        // 厳格なゼロトラスト境界では、未知のプロトコルを即座にドロップすることも可能
        // return XDP_DROP;
    }

    return XDP_PASS;
}

char _license[] SEC("license") = "GPL";

このコードが示すように、IPヘッダーの Protocol フィールドは、単なるOS内部のルーティングキーにとどまらず、エッジセキュリティにおける強力な「最初の関所」として機能する。

—

3. ヘッダー圧縮と次世代ネットワークへの応用

クラウドネイティブやIoT、さらには5G/6Gといった超低遅延が求められる世界では、わずか20バイトのIPv4ヘッダー、あるいは40バイトのIPv6ヘッダーすらも、限られた無線帯域やマイクロ秒単位のレイテンシー最適化においてボトルネックになり得る。

ここで登場するのが、ヘッダー圧縮技術(ROHC: RObust Header Compression – RFC 3095 / RFC 5795など)だ。
ROHCは、TCP/IPやUDP/IPのヘッダーが持つ「セッションを通じてほとんど変化しないフィールド」と「変化するフィールド(シーケンス番号やIPIDなど)」を動的に識別し、送信側と受信側でコンテキストを共有することで、ヘッダーサイズを数バイト(場合によっては1〜2バイト)にまで圧縮する。

この圧縮・復元プロセスにおいて、IPヘッダーの Protocol(IPv6の場合は Next Header)フィールドは、ペイロード構造のパターンを決定づける極めて重要なキーとなる。
TCP(6)であれば輻輳制御ウィンドウや確認応答番号の差分デルタ符号化を適用し、UDP(17)であればポート番号の変化パターンに特化した圧縮アルゴリズムを適用する。プロトコル番号の誤認は、圧縮コンテキスト全体の同期崩壊(Desynchronization)を招き、パケット損失の連鎖を引き起こすため、データリンク層に近い極限のレイヤーでもこの8ビットの正確な解釈が求められるのだ。

—

4. セキュリティインシデントの回避:プロトコル番号を悪用した攻撃手法

攻撃者は常に、このプロトコル番号のディスパッチ機構の「隙」を突こうと狙っている。セキュリティ専門家として、私たちは単にパケットを通すだけでなく、プロトコル偽装やトンネリングによる境界防御のバイパスに警戒しなければならない。

1. プロトコル不整合(Protocol Mismatch)によるファイアウォールバイパス

レガシーなステートフル・インスペクション型ファイアウォールの一部は、ポート番号(例: TCP/UDP 53番など)だけでトラフィックを信頼し、IPヘッダーのプロトコル番号を厳密に検証しないか、あるいは上位層のパーサーとの解釈の齟齬をつく設計上の脆弱性を抱えている場合があった。
例えば、UDPのポートを使うべきトラフィックを、カスタムのプロトコル番号や悪意あるカプセル化(RAW IPソケットを用いた不正なパケットインジェクションなど)で送りつけることで、IDS/IPSのシグネチャをすり抜ける手法がこれに該当する。

2. プロトコル 41 (IPv6-in-IPv4) を利用した covert channel(隠れチャネル)

企業ネットワークの境界防御において、IPv4のトラフィックは厳重に監視されているものの、IPv6への移行期であるためにIPv6トンネル(プロトコル番号 41)が無条件で通過できるよう設定されているファイアウォールやルーターが存在する。
悪意あるインサイダーやマルウェアは、この Protocol: 41 を利用して、本社ネットワークから外部への機密データ持ち出し(Data Exfiltration)のトンネルを構築することがある。パケットの外側からは単なるIPv6のトンネリングに見えるため、通常のL4ファイアウォールでは中身を検知できない。

対策としてのLinuxカーネルパラメータのハードニング例:
不要なプロトコルスタックや、ルーティングを伴うトンネル機能を明示的に無効化、あるいは厳格なフィルタリングを行うことが、エンプロイ環境における鉄則だ。

# /etc/sysctl.d/99-security-hardening.conf
# 意図しないソースルーティングパケットの無効化(IPスプーフィング対策)
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0

# ICMPリダイレクトメッセージの受け入れを拒否(中間者攻撃対策)
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0

# 未知または不要なIPプロトコル(例: 構文違反やカスタムプロトコル)に対する
# カーネルレベルでのロギングと制限の強化(nftablesやiptablesの活用)

また、現代のモダンなファイアウォール(nftablesなど)では、以下のようにプロトコル番号を明示的にホワイトリスト方式で制御することが求められる。

# nftablesのサンプル:許可するIPプロトコル(TCP=6, UDP=17, ICMP=1)以外をドロップ
table inet filter {
    chain input {
        type filter hook input priority filter; policy drop;

        # 確立済みの接続は許可
        ct state established,related accept

        # ループバックは無条件に許可
        iif "lo" accept

        # 許可されたIPプロトコル番号のみを上位のトランスポート層へ通す
        meta l4proto { tcp, udp, icmp } accept

        # それ以外の未知のプロトコル番号(例: 50, 41など、用途が限定されていないもの)はドロップ&ロギング
        log prefix "DROPPED_UNKNOWN_L4_PROTO: " flags all drop
    }
}

—

5. 極限のパフォーマンスチューニング:バッファとプロトコルスタックの調和

プロトコル番号によってディスパッチされたパケットは、次にTCPやUDPのバッファ管理機構へと渡される。ここでRTT(Round Trip Time)の最小化とスループットの最大化を実現するためには、OSのトランスポート層パラメータチューニングが欠かせない。

特に高スループット・高遅延ネットワーク(Long Fat Networks: LFN)において、TCPバッファの動的チューニングは生命線だ。

# /etc/sysctl.d/99-tcp-performance.conf

# TCP送受信バッファの最小値、デフォルト値、最大値(バイト単位)
# メモリを潤沢に使い、BDP (Bandwidth-Delay Product) の拡大に対応する
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216

# 輻輳制御アルゴリズムにBBRを採用(高損失・高遅延環境でのパフォーマンス劇的改善)
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr

# TCPウィンドウのスケーリングを有効化(RFC 1323)
net.ipv4.tcp_window_scaling = 1

# タイムスタンプオプションを有効化し、RTTの正確な計測とPAWS(Wrapped Sequence Numbers保護)を実現
net.ipv4.tcp_timestamps = 1

パケットがIPヘッダーの Protocol: 6 を経由して tcp_v4_rcv に飛び込んだ瞬間、これらのバッファとBBRアルゴリズムの息吹がパケットの運命を決定づける。細部へのこだわりこそが、インフラ全体のパフォーマンスを何倍にも跳ね上げる原動力となるのだ。

—

結びにかえて

たった8ビットの「プロトコル番号」。
それは、レイヤー3という広大なネットワーク層から、レイヤー4のトランスポート層、そしてその上のTLSやアプリケーション層へとバトンを繋ぐ、極めて美しく、かつ厳格なインターフェースである。

普段私たちが何気なく叩く curl や、ブラウザでアクセスするHTTPSの裏側では、この小さな数字が毎秒何百万、何億回と解釈され、パケットの行き先を寸分たがわず導き出している。

この泥臭くも洗練されたパケットの挙動に思いを馳せる時、ネットワークエンジニアリングの本当の醍醐味が見えてくる。さあ、今夜もWiresharkを開き、その8ビットの輝きをその目で確かめてみようではないか。

コメント

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