【実務・中級編】 MTU(Maximum Transmission Unit)とMSS(Maximum Segment Size) – ネットワーク基礎とWebセキュリティ実践ガイド

ネットワークの底で起きている真実:MTUとMSSの不整合がWeb APIを沈める日

「テスト環境では完璧に動いていたのに、本番のクラウド環境にデプロイした途端、巨大なJSONを返すWeb APIだけがタイムアウトする」
「ファイルアップロードのAPIで、なぜか特定のクライアントからだけ途中で接続がリセットされる」

インフラエンジニアやWeb APIの設計・開発に携わっていると、こうした「原因不明の怪現象」に一度や二度は直面したことがあるはずです。アプリケーション層のログを見てもエラーはハンドリングされておらず、まるでパケットが闇に吸い込まれているかのような絶望感。

だいたい、そういう現場の深層をパケットキャプチャ(tcpdumpやWireshark)で覗いてみると、犯人は大体決まっています。そう、MTU(Maximum Transmission Unit)とMSS(Maximum Segment Size)の不整合、そしてそれに伴う「パケットの断片化(フラグメンテーション)」の失敗です。

今回は、数々の修羅場をくぐり抜けてきたシニアネットワークエンジニアの視点から、このネットワークの基礎でありながら、Web APIやクラウドインフラのパフォーマンスを根底から揺るがす「MTUとMSSの深淵」について、実務に直結する知識と共にお伝えしましょう。

—

1. OSI参照モデルのギャップ:リンク層とトランスポート層の果たす役割

まずは、パケットが物理的なワイヤや仮想ネットワークを駆け抜けるとき、OSI参照モデルの階層間で何が起きているのかを整理しておきます。

Webエンジニアの多くは、HTTPリクエストやJSONのシリアライズといったアプリケーション層(第7層)や、TLS/TCPといったトランスポート層(第4層)に意識を奪いがちです。しかし、どれほど美しいAPIを設計しようとも、それを運ぶのはリンク層(第2層)とネットワーク層(第3層)の物理的・論理的な制約を受けた「パケットの断片」です。

MTU(Maximum Transmission Unit)とは何か

リンク層が一度に送信できる最大フレームサイズ(バイト数)を指します。
インターネットの標準的なイーサネットでは、一般的に 1500バイト がMTUとして設定されています。これには、イーサネットヘッダー(14バイト)やFCS(4バイト)などは含まれず、実質的なペイロード(IPパケット)の最大サイズが1500バイトとなります。

MSS(Maximum Segment Size)とは何か

TCP層(トランスポート層)において、1つのTCPセグメントで送信できるペイロード(TCPヘッダーを除くアプリケーションデータ)の最大サイズを指します。

この2つは全く異なるレイヤーの概念ですが、密接に結びついています。
IPパケットの中にTCPセグメントが包まれている(カプセル化されている)ため、基本的な計算式は以下のようになります。

MSS = MTU - (IPヘッダーのサイズ + TCPヘッダーのサイズ)

標準的なIPv4(ヘッダー20バイト)とTCP(ヘッダー20バイト)の場合、
1500 - (20 + 20) = 1460バイト が、標準的なMSSの最大値となります。

—

2. 三大巨頭のすれ違い:TCP 3ウェイハンドシェイクとMSSネゴシエーション

では、クライアントとWeb APIサーバーが通信を始めるとき、このMSSはどのように決まるのでしょうか。ここに、パケットクラッシュの最初の罠が潜んでいます。

通信の開始時、お互いはTCPの3ウェイハンドシェイクを行います。この時、SYNパケットのTCPオプションに「我が受信可能なMSSはこのサイズだ」という値(MSS Option)を載せて相手に伝えます。

[クライアント]                                      [Web APIサーバー]
      |                                                    |
      | ----- SYN (MSS = 1460) --------------------------> |
      |                                                    |
      | <---- SYN-ACK (MSS = 1460) ----------------------- |
      |                                                    |
      | ----- ACK ---------------------------------------> |
      |                                                    |

お互いが「1460バイトまでなら一度に受け取れる」と合意できれば、以後のデータ送信はこのサイズ(または輻輳制御に基づくウィンドウサイズ)に従ってセグメントに分割されます。

実務の罠:VPN、PPPoE、そしてクラウドのトンネリング

しかし、現代のインターネットやクラウド環境は、純粋な1500バイトのイーサネットだけで構成されているわけではありません。

  • 拠点間を結ぶIPsec VPN
  • フレッツ光などのPPPoE環境(PPPoEヘッダーで8バイト圧迫され、MTUが1454バイトになる)
  • AWSのVPCやKubernetesクラスターで使われるオーバーレイネットワーク(VXLANやGeneve、AWSのEC2におけるいくつかのインスタンスタイプやENIの仕様)

これらが絡み合うと、パケットの経路上で「本来のMTU(例えば1500)」より小さなMTU(例えば1400や1300)を強制される区間が生まれます。

クライアントが「MSS=1460で送ってくれ」と要求し、サーバーがそれに従って1460バイトのTCPセグメント(IPパケット全体で1500バイト)を送り出したとしましょう。もし経路上にMTU 1400のルーターが存在した場合、何が起きるでしょうか?

—

3. パケット断片化の悪夢と「Path MTU Discovery(PMTUD)」の機能不全

ルーターは、自身の持つリンク層のMTUを超えるIPパケットを受け取った場合、基本的には次の2つの選択肢のどちらかを選びます。

1. IPパケットを断片化(フラグメンテーション)して転送する
2. パケットを破棄し、送信元に「ICMP Destination Unreachable (Fragmentation Needed)」を返す

なぜフラグメンテーションは悪なのか?

「分割して送ればいいじゃないか」と思うかもしれませんが、ネットワークの世界においてIPフラグメンテーションはパフォーマンスキラーであり、セキュリティ上の脆弱性の温床でもあります。

  • 途中のルーターに余計なCPU負荷がかかる。
  • 断片化されたパケットのどれか1つでも途中でドロップ(ロス)すると、受信側はすべての断片が揃うまで待たされ、最終的にすべての断片を再送要求することになる(再送効率の極悪化)。
  • ファイアウォールやロードバランサー(WAFなど)が、断片化されたパケットの2つ目以降(TCPポート番号などのレイヤー4情報が含まれていない)を正しく検査できず、ドロップしてしまうケースが多々ある。

現代のインフラを蝕む「ICMPブラックホール」

これを防ぐために考案されたのが PMTUD(Path MTU Discovery) です。
IPヘッダーの「Don’t Fragment (DF) フラグ」を立ててパケットを送り、経路上のルーターでMTUオーバーが発生した際に、前述の「ICMPが必要だよ」というメッセージを送信元に返させることで、送信側が自発的にMSS/MTUを縮小していく仕組みです。

しかし、ここで実務上よくあるトラブルが起きます。
セキュリティ上の理由(あるいは安易なファイアウォール設定のミス)により、企業の境界防御やクラウドのセキュリティグループ、プロバイダーのルーターで ICMP(特にType 3 Code 4)をすべてブロック しているケースが後を絶ちません。

ICMPが返ってこないため、送信元サーバーは「パケットが届いているはずだ」と勘違いし続け、巨大なパケットを送り続け、経路上でサイレントにドロップされ、最終的にタイムアウトに至る——これが俗に言う「ICMPブラックホール問題」です。

—

4. 実務での対策:MSSクランプ(MSS Clamping)と適切な設定

こうしたネットワークの深いレイヤーでの悲劇を防ぐため、インフラエンジニアやWeb API開発者が現場で講じるべき具体的なアプローチをいくつか紹介します。

対策1:ルーターやロードバランサーでの「MSSクランプ(MSS Clamping)」

もっとも確実で現場の救世主となるのが、通信経路上のルーターやファイアウォール(Linuxのiptables/nftablesやCiscoルーターなど)で、通過するTCPのSYNパケットを監視し、その中のMSS値を強制的に書き換える(クランプする)手法です。

例えば、Linuxサーバー(ルーターやNATボックス)で、PPPoE環境やVPNトンネルに合わせてMSSを強制的に 1360 に制限したい場合、iptablesでは以下のように設定します。

# iptablesを用いて、FORWARDチェインを通過するTCP SYNパケットのMSSを1360に制限する
# (PPPoE等のオーバーヘッドを考慮した実用的な設定例)
sudo iptables -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1360

※ この設定をゲートウェイやロードバランサーに入れておくだけで、配下のサーバーが知らぬ間に巨大なパケットを生成して断片化・ドロップするのを防ぐことができます。

対策2:Web APIサーバー(Linux OS)でのMTU・MSS確認と調整

APIサーバー自体のネットワークインターフェースのMTUを確認・変更する場合、Linuxでは ip コマンドを使用します。

# 現在のネットワークインターフェース(例: eth0)のMTUを確認する
ip link show eth0

# MTUを標準の1500から、例えばセキュアなオーバーレイネットワーク用の1400に変更する
sudo ip link set dev eth0 mtu 1400

また、LinuxカーネルレベルでPath MTU Discoveryの振る舞いを調整することも可能です。/etc/sysctl.conf に以下のパラメータを記述し、適切にチューニングします。

# /etc/sysctl.conf の設定例

# ICMPブラックホール対策として、PMTUD失敗時に自動でDFフラグを外して再送を試みる機能の有効化
net.ipv4.tcp_mtu_probing = 1

tcp_mtu_probing = 1(または環境によっては 2)を有効にしておくと、PMTUDが機能せずにパケットロスが頻発しているとカーネルが検知した場合、自動的に小さなMSSでの送信にフォールバックしてくれます。これはモダンなWeb APIサーバーにおいて非常に強力な保険となります。

—

5. アプリケーション層からのアプローチ:コードやクライアントからの検証

インフラ側だけでなく、Web APIを設計・実装するエンジニアも、この挙動を意識したコードやデバッグ手法を持っておく必要があります。

例えば、Python(requestsライブラリ)や curl を使って、特定のMTU環境や制限下での動作をシミュレートしたり、HTTP通信のヘッダーや挙動をデバッグしたりすることが可能です。

以下は、PythonでAPIリクエストを送信する際に、タイムアウトやコネクションエラーを適切にハンドリングし、ネットワーク層の問題を切り分けるためのシンプルなスニペットです。

import requests
from requests.exceptions import RequestException

def fetch_large_payload_api(api_url):
    """
    巨大なJSONペイロードをやり取りするAPIクライアントの例。
    MTU/MSSの不整合によるタイムアウトやコネクションリセットを適切にキャッチする。
    """
    headers = {
        "Accept": "application/json",
        "User-Agent": "SecureAPIClient/1.0"
    }
    
    try:
        # 接続タイムアウト(3秒)、読み込みタイムアウト(10秒)を明示的に設定
        # ネットワーク層のブラックホールによる無限待ちを防ぐ
        response = requests.get(api_url, headers=headers, timeout=(3.0, 10.0))
        
        # ステータスコードのチェック
        response.raise_for_status()
        
        return response.json()

    except requests.exceptions.Timeout as e:
        print(f"[-] ネットワークタイムアウトが発生しました。PMTUDの失敗やMSSの不整合の可能性があります: {e}")
    except requests.exceptions.ConnectionError as e:
        print(f"[-] コネクションエラーです。途中のルーターでパケットが拒否されたか、RSTを受け取りました: {e}")
    except RequestException as e:
        print(f"[-] その他のリクエストエラー: {e}")

if __name__ == "__main__":
    # テスト用のエンドポイント
    target_api = "https://api.example.com/v1/heavy-payload"
    # data = fetch_large_payload_api(target_api)

デバッグの武器:curl と tcpdump の合わせ技

現場で「あ、これMTUの問題だな」と確信する一番手っ取り早い方法は、コマンドラインでパケットの挙動を覗き見ることです。

例えば、curl で詳細なトレースを取りつつ、通信を行います。

# curlでレスポンスヘッダーと通信の詳細(IPやTCPのネゴシエーションを含む)を出力する
curl -v https://api.example.com/v1/heavy-payload

さらに、サーバー側や踏み台サーバーで tcpdump を回し、SYNパケットの mss オプションの値を確認します。

# インターフェース eth0 を通過する、SYNフラグが立ったTCPパケットをキャプチャし、MSSの値を確認する
sudo tcpdump -nn -i eth0 'tcp[tcpflags] & (tcp-syn) != 0'

出力結果の中に mss 1460 や mss 1360 といった記述が見つかります。クライアントからのリクエストとサーバーからのレスポンスで、このMSSの値に乖離がないか、あるいは経路上で不自然なフラグメンテーションが起きていないかを追うのが、プロのトラブルシューティングの作法です。

—

まとめ

MTUとMSSは、普段のWebアプリケーション開発では意識のすみのほうに追やりがちな、いわば「縁の下の力持ち」です。しかし、クラウドネイティブな環境、複雑なコンテナネットワーク、マルチクラウドやVPNを掛け合わせたエンタープライズの現場では、この数バイトの差がシステム全体の生死を分ける致命的な地雷となります。

「テスト環境では動くのに、本番で死ぬ」という難解なバグに出会ったとき、アプリのコードばかりを疑うのではなく、ぜひ視線を少しだけ下げて、リンク層やトランスポート層のパケットの息づかいに耳を傾けてみてください。そこには、いつだって明確で論理的な「パケットたちの真実」が隠されています。

コメント

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