Linuxネットワーク診断: HTTP、TCP queue、ルーティング、MTU
対応環境: Linux、Python 3.9以降、curl、iproute2。ルーティング演習にはローカルDocker Engineを使う。 最終更新日: 2026年9月15日
Linuxでリクエストを発行し、socketと次ホップを特定してから、コンテナnamespaceをまたぐ同じパケット経路を説明します。この演習を通して、KubernetesやAWSネットワークを調べる前に、アプリケーション、トランスポート、ルーティングの各証拠を区別します。
CLI、IP設定、DNS、SSH、ファイアウォールから練習するには、まず初心者向け8レッスンを完了してください。その後、このガイドでsocket、ICMP、PMTUの観察を深めます。
演習と併せて、IPv4と次ホップの基礎とHTTPメッセージ構造を読みます。
スコープと準備
このリポジトリをcheckoutした場所からコマンドを実行します。指示があれば端末を分けてください。
| 演習 | 要件 | 範囲 |
|---|---|---|
| HTTP | Python 3.9以降、curl | 127.0.0.1:18080でサーバーを1つ起動し、自動停止 |
| TCP観察 | HTTPサーバー、Python、iproute2のss | ローカルテストポートの読み取り専用観察 |
| ルーティングとMTU | Linux Docker Engine、Docker CLI、ホストのiproute2、pull済みイメージ | 一時コンテナ3つと専用bridge 2つ |
補助スクリプトはhttp_lab.pyとrouting_lab.pyです。実行前に、それぞれの契約とexamples READMEを読んでください。
コマンドは手順を示します。HTTP応答の抜粋は説明用です。ルーティングの抜粋で明示されたものは、記録済み参照実行からのものであり、あなたのマシンの実行結果ではありません。自分の実行は、記載された基準とrouting helperのJSON記録に照らして評価してください。
HTTP helperは教材用の小さなHTTP/1.1サーバーであり、本番サーバーでもRFCの完全実装でもありません。リクエストbodyは最大4096 byte、対応するContent-Length framingだけを受け付け、transfer codingは拒否します。
Routing helperはunix:///var/run/docker.sockだけを使い、リモートDocker host/context環境変数の選択は無視します。ローカルLinux Engineとそのsocketへのアクセスが必要です。リモートcontextや場所の異なるrootless socketでは代用できません。
IMAGE定数で次の参照イメージを固定しています。実行対象のEngineにあらかじめ存在している必要があります。
nicolaka/netshoot:v0.16@sha256:b09d9b21381f47a79b3cbcb30da25266dc17186ea00ae65e99fdc51396f48e70helperはイメージをpullしません。リソース作成前に、固定サブネットが見えるホストIPv4経路や既存Docker networkの範囲と衝突しないか確認します。ルーティング演習は一度に1つだけ実行します。衝突したら停止し、共有ホストの経路を変更しないでください。
ラボ1: HTTPメッセージを読む
目的: request/status line、fields、header終端、content byteを区別します。
端末Aでサーバーを起動します。
python3 examples/networking/foundations/http_lab.py --port 18080 --duration 120127.0.0.1だけにbindし、120秒後に終了します。許可される最大時間は600秒です。ポートが使用中なら、別の空きローカルポートを選び、コマンドとfilterで一貫して使います。無関係なlistenerを停止しないでください。
端末BでGETとHEADを送ります。
curl --disable --noproxy '*' --max-time 5 --http1.1 -i http://127.0.0.1:18080/lesson
curl --disable --noproxy '*' --max-time 5 --http1.1 -I http://127.0.0.1:18080/lesson先頭の--disableはcurlの既定設定を読み込まないための指定です。--noproxy '*'は設定済みproxyを回避します。-iは応答headerを含めます。-IはHEADを送るもので、bodyを隠した通常のGETではありません。
説明用のGET応答抜粋を示します。生成されたDateとServer fieldは省略しています。
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 6
hello実際のbodyはb"hello\n"、16進数では68 65 6c 6c 6f 0aです。最後のLFも含めて6 byteです。HTTP/1.1のwire上ではheader行とheader/bodyの区切りにCRLFを使います。このMarkdown表示ではLFを使います。
同じ6 byteをecho endpointに送ります。
printf 'hello\n' | curl --disable --noproxy '*' --max-time 5 --http1.1 -i \
-H 'Content-Type: text/plain' --data-binary @- \
http://127.0.0.1:18080/echo--data-binary @-はstdinからLFを取り除かず読み込み、POSTを選択します。helperは受け取ったbyteをそのままContent-Type: application/octet-streamで返します。テキスト、JSON、formは解析しません。
| 観察 | 期待する基準 | 解釈 |
|---|---|---|
GET /lesson | HTTP/1.1 200、length 6、contentは6 byte | lengthはstart lineとheaderを含まない |
HEAD /lesson | HTTP/1.1 200、length 6、応答bodyなし | fieldは対応するGETのrepresentationを表す |
POST /echo | HTTP/1.1 200、length 6、LF付きhelloを変更せず返す | byte保存とmedia typeの解釈は別 |
非ASCII文字はUTF-8で複数byteになることがあります。見える文字数ではなく、実際の改行を含めて送信されたencoding後のbyte数を数えてください。
クライアントのdebug traceからrequest lineと送信fieldを確認します。
curl --disable --noproxy '*' --max-time 5 --http1.1 --trace-ascii - \
--output /dev/null http://127.0.0.1:18080/lessonこれはcurlのクライアント側debug出力であり、パケットキャプチャではありません。注釈はwire byte、TCP segment境界、ルーターが見た内容の証拠ではありません。trace収集にはデモデータだけを使います。
HTTPSでは、クライアントがHTTPの平文を表示できても、受動的なネットワークキャプチャからはsession secretなしに読み取れません。HTTP/2とHTTP/3は別のbinary framingを使います。ツールが読みやすく表示しても、raw encodingとは異なります。
クリーンアップ: 端末AでCtrl+Cを押すか、timerが切れるまで待ってサーバーを停止します。永続的なアプリケーションデータは作りません。timer切れ後に次の演習でも使う場合は、再起動してください。
ラボ2: TCP queueとbufferを観察する
目的: 1つの持続接続をsocket状態、stream queue、メモリ計上に関連付けます。
端末Aで同じ120秒サーバーを起動します。端末Bで、実行時間を限定した次のクライアントを動かします。
python3 - <<'PY'
import http.client
import time
connection = http.client.HTTPConnection("127.0.0.1", 18080, timeout=2)
try:
for number in range(30):
connection.request("GET", "/lesson")
response = connection.getresponse()
body = response.read()
assert response.status == 200
assert response.getheader("Content-Length") == "6"
assert body == b"hello\n"
if number == 0:
print("Client endpoint:", connection.sock.getsockname(), flush=True)
time.sleep(1)
finally:
connection.close()
PYクライアントは各応答を読み切ってから接続を再利用します。1秒間隔で小さなGETを30回送り、個別に短命なcurlプロセスを使うより、確立済みsocketを観察しやすくします。
動作中に端末Cで実行します。
ss -ltn 'sport = :18080'
ss -tinm state established '( sport = :18080 or dport = :18080 )'2つ目のコマンドを何度か繰り返します。現在のnetwork namespaceを調べ、テストポートに絞ります。loopback接続のローカル側両端が表示される場合があるため、サーバーポートとクライアントのephemeral portを区別してください。
期待する基準: 127.0.0.1:18080でlistenerが存在し、クライアントの実行中に確立済み接続が見えること。queue、RTT、memoryの具体値は異なります。この小さな負荷でqueueが0でも正常です。
| field/状態 | 意味 |
|---|---|
ESTABLISHED Recv-Q | 順序どおり受信済みだが、アプリケーションがまだ読み取っていないbyte |
ESTABLISHED Send-Q | 未送信分も含む、まだacknowledgeされていないstream byte |
LISTEN Recv-Q | accept()待ちの接続数。payload byteではない |
LISTEN Send-Q | accept backlogの最大値。送信bufferサイズではない |
skmem r/rb | 受信memory計上/受信memory予算 |
skmem w/tb | queue内の送信memory/送信memory予算 |
rtt、mss、cwnd | RTT、TCP dataサイズ、輻輳window情報。cwndの単位は通常segment |
socket memoryには管理情報も含まれるため、stream queueの列と一致するとは限りません。listener backlogは未完了のSYN queueとも別です。各fieldはss(8)マニュアルを参照してください。
値を変更せず、現在のbufferポリシーを読み取ります。
sysctl net.ipv4.tcp_rmem net.ipv4.tcp_wmem net.ipv4.tcp_moderate_rcvbuftcp_*memの3値は最小/既定/自動調整上限を表し、各socketの現在使用量ではありません。受信autotuningやアプリケーションが指定するsocket optionも結果に影響します。予算値は広告するreceive windowやcwndではありません。
受信queueが増え続ける場合はreaderの処理進捗を調べます。送信queueが継続するなら、アプリケーション、acknowledgement、経路の証拠を調べてください。1回の観察やbuffer拡大だけではスループット改善を証明できません。
この6 byte負荷は通常動作を観察するものであり、backpressureを意図的に起こしたり最大スループットを測定したりはしません。調整を提案する前に、socketとbufferの考え方を読んでください。
クリーンアップ: クライアントは30回の反復後、または中断時のfinallyで接続を閉じます。端末Aのサーバーを停止するか、timer切れを待ちます。sysctl変更を戻す必要はありません。
ラボ3: ルーティングとICMPを追う
目的: 選択された次ホップを証明し、名前解決と転送を区別し、TTL制限プローブとICMP応答を照合します。
Routing helperは次の一時トポロジーを作成します。
client namespace router namespace server namespace
192.0.2.2/29 --- left --- 192.0.2.3/29
198.51.100.2/29 --- right --- 198.51.100.3/29
left bridge: 192.0.2.0/29 right bridge: 198.51.100.0/29専用bridgeはどちらもcom.docker.network.bridge.enable_ip_masquerade=falseを設定します。helperは明示的なラボ経路を追加する前に、3つのコンテナ内にあるすべてのIPv4 default routeを削除し、connected routeは残します。
コンテナから全capabilityを取り除いた後、NET_ADMINとNET_RAWだけを追加します。router namespace内ではforwardingを有効にします。helperはprivileged containerもhost networkingも使わず、portを公開せず、AWSリソースも作成しません。
Dockerはホストbridgeを作り、通常のnetwork ruleを管理します。helperはホストroute、sysctl、iptables ruleを手動変更しません。ただし、ホストに影響がない、またはair gapができるという意味ではありません。
クライアントには192.0.2.3経由で198.51.100.0/29への経路を追加し、サーバーには198.51.100.2経由で192.0.2.0/29への戻り経路を追加します。
存在しない出力パスを選びます。
python3 examples/networking/foundations/routing_lab.py \
--output ./routing-observation-01.jsonhelperは既存ファイルの上書きを拒否します。この1回の実行で、後述のMTU演習も実行します。その節のために2回目の並行実行を開始しないでください。
以下はhelperがclient namespace内で実行するコマンドです。ホストシェルの手順ではなく、意味を理解するための表示です。
ip -j route get 198.51.100.3
ip -j neigh show to 192.0.2.3
ip -j neigh show to 198.51.100.3
ping -n -c 1 -W 1 -t 1 198.51.100.3
traceroute -n -I -m 4 -q 1 -w 1 198.51.100.3また、生成したrouterコンテナ名をDocker DNSで解決します。target名は実行prefixで変わります。DNS応答だけでは到達性を証明できません。
| JSONの観察値 | 期待する基準 | 解釈 |
|---|---|---|
defaultRoutes | client、router、serverのリストがすべて空 | 3コンテナにIPv4 default routeが残っていない |
route | gateway 192.0.2.3、source 192.0.2.2 | 宛先にはrouter経由で到達する |
dns | 左ネットワーク上でrouter名が192.0.2.3に解決される | Dockerの名前解決が動作している |
nextHopNeighbor | 192.0.2.3のMACアドレス | ARPで次ホップが解決された |
remoteNeighbor | 空 | リモートserverはclientと同一Ethernet上のneighborではない |
ttlOne | probeは失敗し、captureにICMP Time Exceededがある | 意図どおりrouterでTTLが尽きた |
traceroute | 第1 hopが192.0.2.3、宛先が198.51.100.3 | ICMP probeによるtraceがIP hopを2つ通る |
ttlOneでは、helperがrouter namespace内でicmp[0] == 11のfilterを使って実際のtcpdumpを起動し、capture開始を待ってからTTL-1 pingを送ります。想定される応答は、元のprobeを引用するIPv4 ICMP Type 11 Code 0です。
ここでpingが失敗することは、captureで一致する応答が確認されれば診断刺激の成功です。Probe自体はICMP Echoで、Time Exceededは別の戻りメッセージです。
Tracerouteの-IはICMP Echo probeを使います。UDP/TCP方式では異なるfilterに遭遇し、宛先から異なる応答が返る場合があります。*は待機中に対応する応答がなかったことを記録するだけで、end-to-end lossを単独で証明しません。RTTには応答の戻り経路も含まれます。
helper終了後に証拠ファイルを調べます。
python3 - <<'PY'
import json
from pathlib import Path
record = json.loads(Path("routing-observation-01.json").read_text())
print("Status:", record["status"])
print("Checks:", record.get("checks", {}))
print("Cleanup:", record.get("cleanup", "not reported"))
PY成功基準: helperが正常終了してstatus: passedを記録し、名前付きの8項目がすべてtrueで、cleanupも報告されていること。経路とDNSの成功で、パケット観察の失敗を補うことはできません。
クリーンアップ: 正常終了、観察失敗、処理可能な中断では、helperは作成したcontainerとnetworkのIDを削除しようとします。cleanupエラーは実行失敗です。突然終了するとcleanupできないことがあるため、記録済みIDで残存物を特定してください。共有Dockerリソースに対する広範なpruneはしないでください。次節の解釈にも使うのでJSONを保持します。
記録済み参照実行
examples検証記録にリンクされた2026年9月14日の実行はLinux Docker Engine 25.0.16上でPASSし、8項目すべてを確認しました。3コンテナのdefault routeリストはすべて空で、作成IDは削除されました。tracerouteでは192.0.2.3の次に198.51.100.3が観測されました。
時刻とinterface prefixを省略した、実測TTL-error packetの詳細:
IP 192.0.2.3 > 192.0.2.2: ICMP time exceeded in-transit, length 92これはその環境における機能的な証拠です。別ホストで同一のタイミング、interface名、MACアドレス、互換性になる保証はありません。
ラボ4: MTU、PMTU、MSSを区別する
目的: ICMPエラーの実測を使ってサイズ依存の失敗を説明し、ICMP payloadサイズをTCP MSSと取り違えないようにします。
同じrouting invocationとJSON記録を使います。helperはroute lookupでserver方向のrouter interfaceを見つけ、そのコンテナinterfaceのMTUを1280に設定します。egress interface名がeth1だとは仮定しません。
最初のclient probeではrouterでicmp[0] == 3 and icmp[1] == 4をcaptureしてから、小さい方のprobeを送信します。
ping -n -c 1 -W 1 -M do -s 1372 198.51.100.3
ping -n -c 1 -W 1 -M do -s 1200 198.51.100.3-M doはIPv4のDon't Fragment動作を要求します。ここで使う標準20-byte IPv4 headerと8-byte ICMP headerの場合:
| Probe | IP packetサイズ | 期待する基準 |
|---|---|---|
| ICMPデータ1372 byte | 1372 + 8 + 20 = 1400 byte | MTU 1280のegressには大きすぎる。captureされたICMP Type 3 Code 4を伴って失敗する |
| ICMPデータ1200 byte | 1200 + 8 + 20 = 1228 byte | そのegressに収まる。Echo Replyが成功する |
pmtu.probeOutput、pmtu.capture、smallerPacket、routeAfterFeedbackを確認します。重要な証拠は、特定のRTTや出力文言ではなく、Fragmentation Needed応答と小さいpacketの成功です。
同じ記録済み実行で観測されたping行:
From 192.0.2.3 icmp_seq=1 Frag needed and DF set (mtu = 1280)1200-byte data probeは応答を受信し、routeAfterFeedbackはMTU 1280を報告しました。これらはその実行の観察結果であり、固定された時間やcache有効期限の保証ではありません。
応答後、Linuxは宛先のPMTUをcacheする場合があります。その後に大きすぎるpacketを送ると、再びrouterに到達する前にローカルで「message too long」として失敗する場合があります。routeAfterFeedbackに学習したMTU情報が表示されることがありますが、fieldと期限切れ動作は異なります。
helperのcaptureは、一時client namespaceで意図的に起こした最初の失敗に対応します。後からICMPメッセージが見えないことだけでは、先行する応答の証拠は否定されません。
MSSは別の値です。TCP data byteの上限です。Ethernet MTU 1500、基本IPv4/TCP headerの場合、基本MSSは1500 − 20 − 20 = 1460です。IP/TCP optionと経路条件で、1 packetあたりの実際のdataは小さくなることがあります。
上記の1372と1200はICMP dataサイズであり、実測MSSではありません。loopbackは独自のMTUとkernel動作を持つため、loopback TCP演習でMSS 1460が必ず表示されるわけでもありません。
TSO/GSO/GROやchecksum offloadによって、ホストcaptureが物理リンク上のpacketと異なる場合があります。大きなcapture単位や見かけ上のchecksum errorをwire障害と判断する前に、interface、namespace、観察地点を確認してください。
IPv6 routerは転送packetをfragmentせず、ICMPv6 Packet Too Bigを使います。古典的PMTUDとpacketization-layer probingでは応答への依存が異なります。ICMPの基礎を参照してください。
クリーンアップ: routing helperは、作成した他のIDと一緒に一時routerを削除し、MTU変更も取り除きます。ホストの物理interface MTUは変更しません。解析後は、自分の証拠ファイルだけを保持または削除してください。
Dockerのパケット経路を説明する
目的: 観察結果をLinux namespace、仮想Ethernet link、route lookupに結び付けます。
既存のrouting記録を使います。追加containerは不要です。route、neighborの観察結果、ICMP capture、記録されたコマンドを比較します。
- Client namespaceは
192.0.2.3経由で198.51.100.0/29を選択します。明示的経路が次ホップを決めます。 - ClientはARPでrouter左側のMACを解決します。IP宛先は
198.51.100.3のままです。 - veth pairと左bridgeがEthernet frameをrouterへ運びます。L2 bridgingではIP TTL hopを消費しません。
- Router namespaceがIPv4 forwardingを行い、TTLを減らし、server経路をlookupしてegress MTUを確認します。
- 右側networkではrouterがserverのMACを解決し、新しいEthernet frameを送ります。意図したrouting経路では送信元/宛先IPは変換されません。
- Serverの明示的な戻り経路は
198.51.100.2を使います。要求が届いても、返信経路が動作する必要があります。
期待する基準: 記録を見て、それぞれのlookupのnamespace、IP宛先、ローカルARP対象、返信経路を特定できること。別の実行におけるinterface名やMAC値を推測してはいけません。
Docker DNSによる名前解決とLinux packet forwardingは別の観察結果です。DNS応答が正しくても、経路、forwarding policy、戻り経路が壊れている場合があります。
各network namespaceに独自のloopback deviceがあります。コンテナ内の127.0.0.1はホストのHTTP helperに届きません。この演習ではhost-network modeを使って挙動を変えません。
ここではmasqueradingを無効にし、3コンテナにIPv4 default routeを設けず、routerを明示した専用user-defined bridgeを2つ使います。これは意図したラボ経路を定義するもので、一般的なセキュリティ分離の保証ではありません。既定のDocker Internet NAT、公開port転送、Internet throughputは測定しません。
クリーンアップ: 完了済みの記録を再利用します。routing helperはリソースcleanupをすでに試みています。失敗/不完全な記録は該当段階を調べる理由であり、共有firewall制御を無効にする理由ではありません。
コンテナ技術、次にCore Services and Networking Labへ進みます。Kubernetes CNI、Service変換、AWS VPC routingにはそれぞれ独自のルールがあります。Dockerトポロジーをそのままコピーせず、適切な層で調べてください。
参考資料とレビュー
- HTTP semantics: RFC 9110、HTTP/1.1 framing: RFC 9112
- curlマニュアル: HTTP version、HEAD、binary request data、trace
ss(8)、socket(7)、Linux TCP buffer policy- ip-route(8)、ip-neighbour(8)、network_namespaces(7)
- iputils pingマニュアル、traceroute(8)
- ICMP: RFC 792、IPv4 routerの動作: RFC 1812、MSS: RFC 6691
- Docker bridge networkingとdriver option