HTTP はテキストだ — そして層を順に見る
一言でいうと
HTTPリクエストは、リクエスト行・ヘッダー・空行の3つの部分から成るテキストなので、ncで手書きできます。レスポンスのステータスコードは「どの層まで成功したか」を示します。404はTCPもHTTPも成功したあとの答えで、refusedはサーバーがないという意味です。
なぜ必要なのか
ブラウザーやSDKがHTTPを隠してくれている間は楽です。しかし、プロキシがヘッダーを書き換えたり、Hostが間違って見当違いのサイトが表示されたり、リダイレクトがA→B→Aと回ったりするときは、隠されているものを見る必要があります。リクエストを一度手で書いたことのある人は、curl -vの>と<が何かを知っています。それがAPIデバッグの半分です。
そしてネットワークの診断は、結局のところ層を順番に切り分ける作業です。リンク → アドレス → ネイバー → 経路 → 名前 → ポート → ファイアウォール → レスポンス。各層には、その層だけを見るコマンドがあります。順序なしに手当たり次第コマンドを打つと、「すべて正常なのにつながらない」に閉じ込められます。
どう動くのか
リクエストは、GET /docs/guide.txt HTTP/1.1の1行、Host: localhostのようなヘッダー数行、そして空行です。行末はCRLFです。空行が「ヘッダーの終わり」なので、書き忘れるとサーバーはヘッダーがまだ続くと思って待ち続けます。HostはHTTP/1.1で唯一の必須ヘッダーです。1台のサーバーが複数のサイトを受け持つので、どのサイトかをこれで選びます。Connection: closeはレスポンスのあとで切れという意味で、なければ接続を再利用します(keep-alive)。
レスポンスは、HTTP/1.0 200 OKのステータス行、ヘッダー、空行、本文です。2xxは成功、3xxは別の場所にある(Locationをたどってください)、4xxはリクエスト側の誤り、5xxはサーバー側の誤りです。HEADはヘッダーだけを受け取り、ディレクトリをスラッシュなしで要求すると、301でスラッシュ付きの場所を指します。
診断の順序をコマンドで書くと、次のとおりです。ip -br link(リンクがUPか) → ip -br addr(アドレスがあるか) → ip neigh(ネイバーがFAILEDか) → ip route get(どこへ出ていくか) → getent hosts(名前が解決できるか、digでサーバーを別に確認) → ss -ltn(ポートが開いているか) → nft list rulesetのcounter(ファイアウォールが数えているか) → curl -v(レスポンスは何か)。前の段階が合っていて初めて、後ろの段階に意味があります。
現場での姿
pingは通るのに開かない。pingはICMPで、サービスはTCPです。ファイアウォールはプロトコルごとに扱いが違います。逆に、pingが通らないからといって切断されているわけでもありません。このコースの最後のラボは、設計上ICMPをブロックします。診断は、実際のサービスと同じプロトコル・ポートで行います。
digは通るのにcurlは通らない。/etc/hostsに古いアドレスが残っています。digはサーバーに直接尋ね、curlはlibcを経由してhostsを先に見ます。同じ名前に2つの答えがあるのです。
ステータスコードで層を切り分ける
「つながらない」という報告を受けたとき、返ってきた結果1つで、どこまで成功したかを判定できます。この対応を覚えておくと、調査範囲がすぐに絞れます。
| 結果 | どこまで届いたか | 次に見る場所 |
|---|---|---|
| 名前が見つからない | DNSも越えられていない | getent hosts、dig、/etc/hosts |
| Connection refused | 相手まで届いたが、そのポートに誰もいない | ss -ltnでサービスが起動しているか |
| タイムアウト(応答なし) | どこかで静かに捨てられた | ファイアウォール、セキュリティグループ、経路 |
| TLSハンドシェイクの失敗 | TCPは成功 | 証明書の期限・名前・中間証明書 |
| 502 / 504 | プロキシまでは成功、後ろが問題 | 上流サービスとそのタイムアウト |
| 404 | HTTPまで全部成功 | パス・Hostヘッダー・ルーティングルール |
| 401 / 403 | サーバーが自分を認識して拒否 | 認証情報と権限 |
refusedとタイムアウトの違いは、特に役に立ちます。refusedは、相手のホストが生きていて「そのポートはない」と即座に答えたものなので、ネットワーク経路は正常だという意味で、タイムアウトは答えがまったく来なかったものなので、途中で捨てられた可能性が高いです。ファイアウォールを疑うべきなのは後者であって、前者ではありません。
502と504も分けて見る必要があります。502は、上流に接続したのにおかしな答えが来たか、接続が切れたものです。504は、上流が時間内に答えなかったものです。前者は上流が死んでいるか再起動中のとき、後者は上流が生きているのに遅いときに出ます。プロキシのタイムアウトが、後ろのサービスの処理時間より短いと、正常なリクエストまで504になるので、2つの値を合わせて設定するのが基本です。
最後に、curlで試すときは、実際のクライアントと同じ条件を作る必要があります。Hostヘッダー、プロトコル(HTTP/1.1か2か)、そしてプロキシの環境変数。サーバー上ではcurlが通るのにブラウザーは通らない状況の多くは、この3つのうちのどれかが原因です。
次のラボですること
python3 -m http.serverを起動し、ncでGET・HEAD・404・301を手で作って受け取り、curl -vと突き合わせます。そのあと総合ラボで、2つのサブネット・ルーター・dnsmasq・forwardファイアウォール・マスカレードを1台のVMに構築し、「開かない」という報告をどの順序で調べるかを、レポートとして残します。