このサイトは Cloudflare で Astro の静的コンテンツを配信する形で公開しています.移行が推奨されていたので,先日 Pages から Workers (Static Assets) にホスティングを変更しました.おそらくその前後から携帯電話 (iPhone) で自分のサイトを開くと,ときどき「この接続は安全ではありません」とかいう警告が出るようになりました.
色々と観測した範囲では正常に接続できているようで,警告された不具合は無いようでした.とりあえず,対症療法で警告が出ない状態にはできましたが,どうして偽陽性の判定が出てしまうのか,その根本的な理由まではわかりませんでした.
この記事は,どんな感じで調べていたかの記録です (詳しいひと助けて).
起きていたこと
だいたい,こんな感じでした.
- このサイトの URL (silasol.la) を踏んだとき,たまに警告が出る
- よくある「この接続は安全ではありません」「この Web サイトは HTTPS での安全な接続に対応していません。クレジットカード、電話番号、パスワードを含む、この Web サイトで閲覧したり入力したりする情報は、ほかの人に読み取られたり変更されたりする可能性があります。」って汎用メッセージが全画面で
- 観測した範囲では Twitter や Bluesky のネイティブアプリから,スマートフォン (iPhone) で開いたときに出る
- 毎回出るわけではなく,何度かリンクを押していると出たり出なかったり
- リロードするだけで警告は消える
- うまくいった次の回にまた出ることもある
- PC のブラウザでは一度も出たことが無い
- Wi-Fi でも携帯回線でも出ている
- 証明書は有効で,期限も発行元も問題ない
ちょっと,調べるのがめんどくさいやつです.
HTTP まわりの確認
安全でない接続だと言われていますが,リロードするだけでページ自体は普通に開くし,よくある証明書まわりではなさそうです.
とりあえず,リンクが http:// からのリダイレクトになっていないかを見てみました.Twitter だと見た目は普通のリンクでも強制的に短縮 URL (t.co) から転送していたりするので,そのあたりを確認します.
curl -sI "https://t.co/..." | grep -i location
# => location: https://silasol.la/
普通に https:// へ転送していて問題なさそうです.
ここで設定をこねていたら少し進展があって,(モバイルで安定していないのかなってことで) HTTP/3 1 を Cloudflare 側で切ったら警告が出なくなりました (逆に ON にしたら警告が復活する).QUIC まわりかなってことで,このへんをよく調べることにしました.
サイト自体が HTTP/3 で失敗していないか確認するために,手元にある Mac の curl から --http3-only で HTTP/3 を強制 2 してリクエストを送ります.
curl --http3-only -sI https://silasol.la/
# => HTTP/3 200
応答は正常で,サイト側は特に問題なさそうです.
ネットワークの問題でもなさそう
ネットワーク経路の問題なのかなとも思いましたが,携帯をセルラー回線に切り替えても出るし,同じ Wi-Fi に繋いでいる Mac からは何度 curl でリクエストを送っても成功しています.
当初は Twitter のアプリ内ブラウザでばかり出ていたので,クライアントの実装や WebKit 固有の問題なのかなとも思っていました.しかしながら,どうやら Bluesky のリンク (こちらは直リンク) から遷移した Safari でも出るようで,Twitter に限った問題ではなさそうです.手元にある iPhone でしか試していないので,これが広く起きる話なのか,端末や環境に固有なのかまでは分かりませんが.
ここまで,外部から確認できる範囲では,どこも正常に動いているようでした.
パケットを見てみる
通信の中身についても見ておこうと思って,パケットをキャプチャしてみました.アプリ内は DevTools とかで見られないので,端末の通信自体を記録することにします.iPhone を Mac に繋ぐと RVI 3 を作ることができて,そこに流れる通信を丸ごとキャプチャします.
# rvi0 という仮想インターフェースができる
rvictl -s '<iPhone の UDID>'
# ファイルに書き込み
sudo tcpdump -i rvi0 -n 'port 443' -w trace.pcap
これを Wireshark (tshark コマンド) で読んでみました.まずは QUIC 接続先 (ClientHello の SNI 4) の確認からです.
tshark -r trace.pcap -Y 'quic && tls.handshake.type == 1' -T fields -e tls.handshake.extensions_server_name
silasol.la
silasol.la
silasol.la
silasol.la
それぞれの行が QUIC 接続 1 つの ClientHello です.接続先はすべて silasol.la でした (ブラウザが接続を複数張っていて複数行ある).いずれも平文で見えており,ECH で接続先 SNI を暗号化しているわけでもなさそうです.
サーバ (エッジ) からレスポンスが返っているかについても確認します.ipv6.src をエッジの IP アドレスに絞ると,サーバから端末へのパケットを出せます.
tshark -r trace.pcap -Y 'quic && ipv6.src == <エッジ IP アドレス>' -T fields -e frame.time_relative -e _ws.col.Info
2.134493 Initial, SCID=0124…, PKN: 0, ACK
2.134494 Initial, SCID=0124…, PKN: 1, ACK, CRYPTO
2.136298 Protected Payload (KP0)
2.136299 Protected Payload (KP0)
2.148406 Initial, SCID=01da…, PKN: 1, ACK, CRYPTO
2.148407 Protected Payload (KP0)
2.174419 Handshake, SCID=0124…
Initial … ACK, CRYPTO からサーバがクライアントに応答している 5 こと,Handshake からハンドシェイクが進行していることが分かります.Protected Payload (KP0) は,ハンドシェイクが完了して実際に流れている暗号化されたデータ (ページの内容) です.もしも接続が失敗していれば,こういった行は出てこないはずです.
そういうわけで,警告が表示されている最中でもサーバは応答しており,ちゃんと暗号化データも届いていました.エラーも切断も発生しておらず,接続自体は成功しているようです.
このまま Cloudflare の設定で HTTP/3 を切ってみると,クライアントは以前の alt-svc 6 を頼りに QUIC を試し,サーバ側で拒否されて TCP にフォールバックしました.その TCP も成立してページも表示されますが,その瞬間は警告が出てしまいます (アプリを再起動すると出なくなる).接続の成否自体が原因ではないようでした.
警告が出た回と出なかった回のキャプチャも比較しましたが,QUIC の成否も,ハンドシェイクの再送も,接触したホストも,エラーの有無も,どうやら差は無いようでした.観測する限り,警告が出ているのは間違いで,実際の通信に問題は起きていないようです.
他のサイトとの比較
同じ構成 (Cloudflare + HTTP/3) で個人サイトを Twitter で公開している人がいたので,同じように iPhone のアプリから開いてみましたが,自分のサイトのような警告は出ませんでした.私のサイトと比べてみると,どうやら 0-RTT (Early Data) 7 の設定が異なるようです (その人のサイトは無効で,私のサイトは有効).これは実際に TLS 1.3 の Early Data を送信して確認できます.
# セッションの取得
printf 'GET / HTTP/1.1\r\nHost: silasol.la\r\nConnection: close\r\n\r\n' \
| openssl s_client -connect silasol.la:443 -servername silasol.la \
-tls1_3 -sess_out sess.pem -ign_eof >/dev/null 2>&1
# Early Data を送信できるか試す
printf 'GET / HTTP/1.1\r\nHost: silasol.la\r\nConnection: close\r\n\r\n' > req.txt
openssl s_client -connect silasol.la:443 -servername silasol.la \
-sess_in sess.pem -early_data req.txt </dev/null 2>&1 | grep -i 'early data'
# => Early data was accepted
ここでの確認は TCP の TLS 1.3 Early Data ですが,パケットキャプチャでは QUIC の 0-RTT パケットの送信も確認しています.
そういうわけで,HTTP/3 は有効のまま 0-RTT だけを Cloudflare で無効にしてみたら,警告は出なくなりました.めでたしめでたし.
いや普通に後味は悪くて,0-RTT をやめると出なくなるのは良いのだが,どうして 0-RTT で,しかも毎回ではなく間欠的に誤判定されるのか,理由は謎です 8.最終的な判定はクライアントで行われるので,外からは見えずじまいといったところでしょうか.
最終的な対処
今後は 0-RTT を OFF にしておきます.静的サイトだしそれほど惜しくはありません.HTTP/3 を維持できてよかった.
0-RTT の Early Data には,通常の通信より弱い面もあるようです.RFC 9846 には,Early Data が前方秘匿性を持たず,接続を跨いだ再送に関して保証しないと明記されていました 9.今回のケースと関係するかは分かりませんが,まあ OFF にしても実害は無いでしょう.
同じような現象に心当たりのある方がいれば,根本的な原因や対処方法など教えていただけると喜びます.m(_ _)m
脚注
-
TCP ではなく QUIC (UDP を利用) で通信する HTTP です.同じ 443 ポートでも土台の通信が異なります. ↩
-
HTTP3 が利用できるようにビルドされた curl が必要です.
curl -VのFeatures:にHTTP3があるかで判別できます.macOS に同梱されたものは対応しておらず Homebrew で入れました. ↩ -
端末の通信を Mac の仮想インターフェース (Remote Virtual Interface) に写せます.コマンド類は Xcode で入ります. ↩
-
TLS ハンドシェイクにおける最初のメッセージを ClientHello と言います.このパケットの中で,クライアントは SNI (Server Name Indication) という拡張機能を用いて,これからアクセスしようとしているドメインを伝えます. ↩
-
ACKは確認応答でCRYPTOは TLS ハンドシェイクの中身です. ↩ -
サーバがクライアントに対して,別のプロトコル (HTTP/3 など) やホストで接続できることを通知する仕組みです.ブラウザや端末で (最大 24 時間) キャッシュします. ↩
-
再接続したときに,ハンドシェイクを完了する前からでもデータを送り始めることができる TLS 1.3 の仕組みです. ↩
-
2.3 節の IMPORTANT NOTE にあります. ↩