コメントを投稿するにはログインが必要です
ログインページへ記事ができると、章ごとの要約と、聴きどころへ飛べる目次がここに並びます。いまは音声で聴けます。
APIを叩くとき、レスポンスを確認するとき、エンジニアなら誰もが一度は使うcurl。でも `curl -V` の出力をじっくり読んだことはありますか? 今回は、HTTP/3やECHの挙動確認のためにcurlを自前でビルドしているcatatsuyが、普段何気なく使っているcurlの裏側を掘り下げます。
HTTP/3対応のcurlをビルドしようとすると、nghttp3やngtcp2、TLSライブラリなど、さまざまなOSSが登場します。当時の公式手順ではquictlsが使われていましたが、BoringSSL互換のAPIを持つLibreSSLでも動くはずだと考えて試したところ、問題なく利用できました。依存ライブラリを毎回ビルドするのは面倒なのでHomebrewのFormulaで自動化しました。その後、ECHの検証のためにOpenSSL 4系へ移行し、OpenSSL 4.0.0では動かなかったHTTP/3とECHの組み合わせが、4.0.2では動くようになっていた話も紹介します。
普段使いのcurlにも意外な落とし穴があります。以前はTLS 1.1を指定する挙動だった `--tlsv1.1` が、TLS 1.1以上を使う指定に変わり、TLS 1.2で通信できてしまった経験を紹介します。TLS 1.1に限定するには `--tls-max 1.1` も必要です。また、`--resolve` を使えばHostヘッダーやSNIを維持したまま、接続先IPアドレスだけを差し替えられます。さらに、IPv6で問題が起きていてもIPv4へのフォールバックによって通信できてしまうHappy Eyeballsや、非同期DNSライブラリのc-aresなど、一見単純なHTTPリクエストの裏側にある仕組みを見ていきます。
特に深掘りするのが、HTTPSレコードを使ったHTTP/3接続です。HTTP/2で通信した後、レスポンスに含まれるAlt-SvcヘッダーでHTTP/3の接続先を知る流れを紹介します。curlでも `--alt-svc` を使えば、その情報をファイルに保存して次回の接続に利用できます。一方、DNSのHTTPSレコードでHTTP/3対応を知ることができれば、最初からHTTP/3で接続できるはず。ところが実際には、たまにHTTP/3で接続できても、その後は何度試してもHTTP/2になってしまう。しばらく悩んだ末に分かった原因は、OSのDNSキャッシュでした。検証した環境では、A/AAAAレコードをgetaddrinfo経由でOSのDNSキャッシュから素早く取得できる一方、HTTPSレコードはc-aresで別途問い合わせていたため、HTTP/3の情報が届く前にHTTP/2で接続していたのです。DNSキャッシュを消すと再びHTTP/3で接続できるようになるという、キャッシュが効くことで新しいプロトコルが選ばれにくくなる現象でした。
最後は、curlの接続処理からWebアプリケーションの安定性の話へ。libcurlの接続タイムアウトはデフォルトで300秒、通信全体のタイムアウトはデフォルトで制限なし。PHPのcURL拡張やGuzzleを使っていても、外部APIの障害によってPHP-FPMのワーカープロセスが外部APIの応答待ちで埋まり、自分のサービスまで応答できなくなる可能性があります。`--connect-timeout` と `--max-time` の違いから、外部通信のタイムアウト設計まで話を広げます。
## 関連リンク
- curl: https://curl.se/
- curl HTTP/3ビルド手順(HTTP3 with curl): https://curl.se/docs/http3.html
- curl HTTPSレコード対応ドキュメント: https://github.com/curl/curl/blob/master/docs/HTTPSRR.md
- curl ECHドキュメント: https://github.com/curl/curl/blob/master/docs/ECH.md
- curl man page(--resolve / --tls-max / --alt-svc / --connect-timeout / --max-time): https://curl.se/docs/manpage.html
- curl Alt-Svc: https://curl.se/docs/alt-svc.html
- libcurl: https://curl.se/libcurl/
- libcurl CURLOPT_CONNECTTIMEOUT: https://curl.se/libcurl/c/CURLOPT_CONNECTTIMEOUT.html
- libcurl CURLOPT_TIMEOUT: https://curl.se/libcurl/c/CURLOPT_TIMEOUT.html
- catatsuy/homebrew-tap(HTTP/3・ECH対応curlのFormula): https://github.com/catatsuy/homebrew-tap
- Homebrew: https://brew.sh/
- nghttp2: https://nghttp2.org/
- nghttp3: https://github.com/ngtcp2/nghttp3
- ngtcp2: https://github.com/ngtcp2/ngtcp2
- c-ares: https://c-ares.org/
- OpenSSL: https://www.openssl.org/
- LibreSSL: https://www.libressl.org/
- BoringSSL: https://boringssl.googlesource.com/boringssl/
- quictls: https://github.com/quictls/quictls
- RFC 8305 Happy Eyeballs Version 2: https://www.rfc-editor.org/info/rfc8305/
- Happy Eyeballs Version 3(Internet-Draft): https://datatracker.ietf.org/doc/draft-ietf-happy-happyeyeballs-v3/
- RFC 7838 HTTP Alternative Services(Alt-Svc): https://www.rfc-editor.org/info/rfc7838/
- RFC 9460 SVCB and HTTPS Resource Records: https://www.rfc-editor.org/info/rfc9460/
- RFC 9849 TLS Encrypted Client Hello(ECH): https://www.rfc-editor.org/info/rfc9849/
- RFC 8484 DNS Queries over HTTPS(DoH): https://www.rfc-editor.org/info/rfc8484/
- getaddrinfo(3): https://man7.org/linux/man-pages/man3/getaddrinfo.3.html
- PHP cURL: https://www.php.net/manual/ja/book.curl.php
- Guzzle Request Options(connect_timeout / timeout): https://docs.guzzlephp.org/en/stable/request-options.html
- Go net/http: https://pkg.go.dev/net/http
─────────────
YouTube: https://youtu.be/sYigIRmcQSg
Web: https://yaoyorozu-oss.henteko07.com/
X: https://x.com/yaoyorozu_oss