【テクニカル・上級編】 ARPキャッシュテーブルの管理とエントリの更新条件 – ネットワーク基礎とWebセキュリティ実践ガイド

ARPキャッシュの深層:OS内部挙動とGratuitous ARPが織りなすパケットのドラマ

ネットワークエンジニアやインフラアーキテクトであれば、トラブルシューティングの最中に arp -n や ip neighbor show を叩き、カーネルが保持するARPテーブルを睨みつけた経験が一度や二度ではないはずだ。

「なぜ、IPアドレスとMACアドレスの紐付けが突然消失したのか」
「なぜ、機器をリプレイスした直後に通信がブラックホール化するのか」

私たちは普段、OSがよしなに処理してくれるレイヤー2とレイヤー3の橋渡しを過信しがちだが、その裏側では、Linuxカーネルの緻密なステートマシンと、ネットワーク上を飛び交う微小なパケットたちが、緊密なダンスを踊っている。

今回は、このARP(Address Resolution Protocol)キャッシュテーブルの管理メカニズム、エントリのライフサイクル、そしてセキュリティの観点から見逃せない Gratuitous ARP(GARP)の挙動について、パケットレベルの解像度で徹底的に紐解いていこう。

—

1. ARPキャッシュの生命維持:Linuxカーネルにおけるステートマシン

OSは、MACアドレスを解決するたびに無限にそれを保持するわけではない。ARPテーブルは有限の資源であり、ネットワークのトポロジ変更(VLANの移動、NICの交換、DHCPによるIPの再割り当てなど)に追従するため、厳密な生存期間(エイジング)とステート管理が行われている。

Linuxカーネル(Neighbour subsystem)において、ARPエントリは以下の主要なステート(状態)のいずれかに属している。

  • INCOMPLETE: ARPリクエストを送信したが、まだARPリプライを受信していない状態。解決待ちの過渡期。
  • REACHABLE: 正常に通信が可能で、直近で双方向のトラフィックが確認された状態。
  • STALE: エントリは有効だが、最後の通信から一定時間が経過し、期限切れの可能性を示している状態。
  • DELAY: STALE 状態のエントリに対してパケット送信要求が発生した際、即座にARPを撃つのではなく、上位プロトコルの確認を待つタイマー稼働状態。
  • PROBE: DELAY 期間内に応答がない、あるいはタイマーが切れたため、ユニキャストのARPリクエスト(プローブ)を再送して生存確認を行っている状態。
  • FAILED: 最終的に応答が得られず、エントリの解決に失敗した状態。

カーネルパラメータによる生存期間の制御

Linuxのネットワーキングスタックでは、これらのステート遷移を決定づけるタイマー値が /proc/sys/net/ipv4/neigh/ 配下のパラメータとして露出している。実務の現場でパフォーマンスや収束速度(Convergence Time)をチューニングする際、この深淵を覗くことになる。

# デフォルトインターフェースにおけるネイバー(ARP)設定の確認
$ ip -s neigh show
# または sysctl でカーネルパラメータを直接確認
$ sysctl -a | grep net.ipv4.neigh.default

主要なパラメータの意味とチューニングの勘所を以下に整理する。

# /etc/sysctl.conf のチューニング例(高密度なデータセンター環境向け)

# ガベージコレクタが古いエントリの掃除を始める閾値(エントリ数)
net.ipv4.neigh.default.gc_thresh1 = 128

# ソフトリミット:この数を超えると、過去5秒間参照されていないエントリが削除対象になる
net.ipv4.neigh.default.gc_thresh2 = 512

# ハードリミット:この数を超えると、カーネルは新規のARP解決を拒否し始める
net.ipv4.neigh.default.gc_thresh3 = 1024

# REACHABLE状態とみなされる時間(ミリ秒)。デフォルトは通常30秒(30000)
net.ipv4.neigh.default.base_reachable_time_ms = 30000

# ガベージコレクタが実行されるインターバル(秒)
net.ipv4.neigh.default.gc_interval = 30

高可用性(HA)クラスターやフェイルオーバーが頻発する環境では、base_reachable_time_ms をあえて短く設定し、障害発生時のMACアドレスキャッシュの残留時間を最小化することが、パケットのブラックホール化を防ぐ定石となる。

—

2. Gratuitous ARP(GARP)の衝撃:エントリ強制更新のメカニズム

通常、ARPリクエストは「誰それのIPアドレスを持つマシンのMACアドレスを教えてくれ」という問い合わせだが、Gratuitous ARP(無償ARP) はその常識を覆す。

GARPとは、「自分自身のIPアドレス」を送信元IPに指定し、宛先IPにも自分自身のIPを記述したARPリクエスト(あるいはARPリプライ)を、ブロードキャスト(ff:ff:ff:ff:ff:ff)でネットワーク上に放流する挙動を指す。

このパケットがワイヤー上を流れた瞬間、スイッチングハブやルーター、そして同じセグメントに存在するすべてのホストのARPキャッシュに強烈なインパクトを与える。

GARPが引き起こすカーネルの挙動

LinuxカーネルがGARPを受信したとき、内部では何が起きているのか。

1. 既存エントリの検索: 受信したGARPパケットの「送信元IPアドレス(SPA)」と「送信元MACアドレス(SHA)」を抽出する。
2. キャッシュの更新(あるいは新規作成):

  • すでにARPテーブルにそのIPアドレスのエントリが存在する場合、カーネルはタイムスタンプをリフレッシュし、MACアドレスを新しいものに即座に書き換える(強制上書き)。
  • エントリが存在しない場合、基本的には無視されることが多い(※カーネルの arp_accept パラメータ設定に依存する)。

セキュリティ上の脅威:ARPスプーフィング(ARPポイズニング)の温床

この「GARPや一方的なARPリプライを無条件に信じてキャッシュを更新してしまう」という設計は、レイヤー2ネットワークにおける最大の弱点、すなわち ARPスプーフィング(ARP Poisoning) の根幹を成している。

悪意ある攻撃者が、実在するデフォルトゲートウェイ(ルーター)のIPアドレスと「攻撃者のNICのMACアドレス」を紐付けた偽のGARPパケットを狂ったようにブロードキャストし続ければ、同一セグメント上の全ホストのARPキャッシュは瞬く間に毒され、すべてのトラフィックが攻撃者のマシンを経由する「中間者攻撃(MitM)」が完成する。

この脆弱性を緩和するため、モダンなLinuxディストリビューションでは厳格な検証モードが用意されている。

# カーネルパラメータでARPの学習挙動を厳格化する
# arp_announce: 送信パケットにおける送信元IPアドレスの選択ポリシー(0: 自由, 1: 同一サブネット優先, 2: 最適なローカルアドレス)
# arp_ignore: ARPリクエストに対する応答ポリシー

# 外部からの怪しいARPを簡単に受け入れないための設定例
$ sudo sysctl -w net.ipv4.conf.all.arp_accept=0
$ sudo sysctl -w net.ipv4.conf.all.arp_announce=2

—

3. 通信発生時のパケット挙動:L2からL4、そしてTLSハンドシェイクへの接続

では、ARPキャッシュが一度 STALE になり、そこから実際にWebサーバーへHTTPSリクエストを送る際のパケットの連鎖を、極限まで解像度を上げて追ってみよう。

ステップ1:ルーティングとネイバーのルックアップ

アプリケーションが https://example.com(例: 192.168.10.50)へのTCP接続(ポート443)を要求する。
Linuxのネットワークスタックは、まずルーティングテーブルを引く。

$ ip route get 192.168.10.50
192.168.10.50 dev eth0 src 192.168.10.10 uid 1000 
    cache

宛先が同一セグメント、あるいはデフォルトゲートウェイ経由であることが判明すると、カーネルはネクストホップのIPアドレスに対応するMACアドレスをARPキャッシュから探す。

もし該当エントリが STALE であれば、カーネルはパケットをいったん保留(または送信しつつ DELAY ステートへ移行)し、速やかに ユニキャスト(またはブロードキャスト)のARPリクエスト を送出する。

ステップ2:ARP解決の完了とレイヤー2フレームの構築

宛先からARPリプライが返ってくると、カーネルはエントリを REACHABLE に格上げし、ようやく目的のIPパケット(IPv4ヘッダー + TCPセグメント)をイーサネットフレームにカプセル化する。

ここで、イーサネットヘッダーの宛先MACアドレスに、先ほど取得した実機のMACアドレスが書き込まれる。

[ Ethernet Header ] -> Destination: AA:BB:CC:DD:EE:FF / Source: 11:22:33:44:55:66
  [ IP Header ]      -> Source: 192.168.10.10 / Destination: 192.168.10.50
    [ TCP Header ]   -> Source Port: 54321 / Destination Port: 443 / SYN

ステップ3:トランスポート層(TCP)とTLSの最適化

レイヤー2のハードルを越えたパケットが相手に届くと、いよいよTCPの3ウェイ・ハンドシェイク(SYN, SYN-ACK, ACK)が始まる。

ここでインフラアーキテクトが意識すべきは、RTT(Round Trip Time)の削減 と TCPウィンドウサイズ・バッファのチューニング だ。

レイヤー2のARP解決にもたつくと、TCPの初期ハンドシェイクに遅延が生じ、クライアント側の体感速度(TTFB: Time To First Byte)に悪影響を及ぼす。特にコンテナ環境や仮想化基盤(KubernetesのCNIなど)では、仮想ブリッジやvethペアを経由する過程でARP/NDP(IPv6の場合)のキャッシュ枯渇やフラッディングが発生しやすいため、ネイバーテーブルのサイズやGC(ガベージコレクション)の閾値を適切にサイジングすることが極めて重要となる。

さらに、TCPハンドシェイクが完了した直後の TLS 1.3ハンドシェイク においては、1-RTT(またはZero-RTT)での暗号化確立が求められる。ネットワークの基礎であるARP層が正常かつ高速にキャッシュを維持していることこそが、上位レイヤーにおける秒速のTLSセキュア通信の土台となっているのである。

—

4. 現場で使える!ARPトラブルシューティングと自動化スクリプト

最後に、実務の現場で遭遇する「ARPに起因するパケットロスや接続不良」を迅速に切り分けるためのPythonスクリプトと、確認用コマンドを紹介しよう。

トラブルシューティングの鉄板コマンド

# 特定のインターフェースにおけるARPキャッシュの状態をリアルタイム監視
$ watch -n 1 "ip neigh show"

# 特定のIPに対するカーネルのネイバー状態を強制的に期限切れ(STALE)にする
# (ルーターのMAC変更直後や、障害切り分け時に極めて有効)
$ sudo ip neigh flush dev eth0 to 192.168.10.1

Pythonによる簡易ARPスキャナ&キャッシュチェッカー

セキュリティ診断や、ネットワークセグメント内のゾンビIP・MACの不整合を検知するために役立つ、Scapyを使用したスクリプトの断片を提示する。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from scapy.all import ARP, Ether, srp
import sys

def scan_arp_network(target_ip_range):
    """
    指定されたサブネットに対してARPリクエストをブロードキャストし、
    アクティブなホストのIPとMACアドレスのペアを収集して表示する。
    """
    print(f"[*] Scanning network range: {target_ip_range} ...")
    
    # イーサネットブロードキャストフレームとARPリクエストを結合
    ether = Ether(dst="ff:ff:ff:ff:ff:ff")
    arp = ARP(pdst=target_ip_range)
    packet = ether / arp

    # パケットを送受信し、応答を得る
    result = srp(packet, timeout=3, verbose=0)[0]

    devices = []
    for sent, received in result:
        devices.append({'ip': received.psrc, 'mac': received.hwsrc})

    print(f"[*] Found {len(devices)} active devices.\n")
    print("IP Address\t\tMAC Address")
    print("-" * 40)
    for device in devices:
        print(f"{device['ip']}\t\t{device['mac']}")

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python3 arp_scanner.py <IP_Range>")
        print("Example: python3 arp_scanner.py 192.168.10.0/24")
        sys.exit(1)
    
    # ルート権限(CAP_NET_RAW)が必要な点に注意
    scan_arp_network(sys.argv[1])

—

まとめ

ARPキャッシュという、一見すると地味なOSの内部コンポーネント。しかしその裏側には、Linuxカーネルの緻密なステートマシン、GARPによるネットワーク間の動的な同期、そしてレイヤー2からレイヤー7のTLSハンドシェイクに至るまでの壮大なパケットのバトンリレーが存在している。

インフラストラクチャの規模が拡大し、クラウドネイティブやマイクロサービスアーキテクチャが主流となった現代においても、このネットワークの「足回り」の挙動を深く理解しているかどうかが、障害発生時の切り分けスピードとシステム全体の信頼性を大きく左右する。

パケットが流れる足元を疑い、カーネルの鼓動に耳を傾けること――それこそが、真のネットワーク・セキュリティスペシャリストの条件なのだ。

コメント

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