問題
- Qiita CLIの
npx qiita loginが失敗してしまっていた - エラー文
TypeError: fetch failed
[cause]: Error: read ECONNRESET
原因
IPv6での通信がなぜか接続リセットされていた(IPv4なら通信成功した)
原因特定までの道のり
① Qiita CLI が失敗
↓
fetch failed / ECONNRESET
② ほかのサイト(Google / GitHub)にHTTPS接続
↓
成功
↓
「PC全体のネットワークが死んでいるわけではなさそう」
「Qiita固有の問題っぽい」と範囲を絞る
③ curl で Qiita に HTTPS 接続
↓
curl -I https://qiita.com
↓
Connection reset
↓
Qiita CLI固有ではなく、QiitaへのHTTPS通信でも再現
④ TLS 1.2を指定して再試行
↓
curl --tlsv1.2 -I https://qiita.com
↓
やっぱり Connection reset
↓
「TLSのバージョンが原因とは考えにくい」
⑤ ここで「どの段階で切られてる?」を考える
↓
ECONNRESET = TCP接続がRSTでリセットされた
↓
少なくともTCPレベルの接続が途中でリセットされている
※ただし、ECONNRESETだけでは
TCP確立直後なのかTLS中なのかまでは断定できない
⑥ Qiitaの名前からIPアドレスを調べる
↓
nslookup qiita.com
↓
IPv4もIPv6も存在
⑦ IPv4とIPv6を明示的に分けて接続
↓
curl -4 -I https://qiita.com → 成功
curl -6 -I https://qiita.com → Connection reset
↓
「IPv6経路では失敗し、IPv4経路では成功」
と切り分けられた
⑧ Node.jsがどちらを選んでいるか確認
↓
Node.js DNS lookup → IPv6を選択
⑨ IPv4を優先させる
↓
NODE_OPTIONS=--dns-result-order=ipv4first
↓
Qiita CLI login → 成功
エラー文解析
TypeError: fetch failed
[cause]: Error: read ECONNRESET
- fetch は、Node.jsがHTTP/HTTPS通信を行うための機能
-
fetch failedってことはQiita CLIがQiita APIに対して、
Node.js(Qiita CLI)
↓
HTTPS通信(ここで問題発生)
↓ ×
Qiita API
をしようとして、通信処理が失敗したという意味
-
ECONNRESET→既存のTCP接続がリモート側などからRST(TCP Reset)によって強制的に切断された - 接続リセットとはブラウザとサーバー間の通信が途中で強制的に切断された状態
-
[cause]: Error: read ECONNRESET
→つまり、ネットワーク通信で何か問題発生していた原因は、
通信していたTCP接続が、相手側または通信経路上の何らかの機器によってRST(接続リセット)され、Node.jsがデータを読み取れなくなったから
「Qiita CLI固有の問題なのか?」を切り分ける
Qiita CLIからの通信ではなく、curlからQiitaサイトへ直接HTTPS通信してみる
curl https://qiita.com
→結果:
curl: (35) Recv failure: Connection was reset
→結論:Qiita CLIからの通信だけが失敗しているわけではない
HTTPSそのものの問題か確認
前提:HTTP(S)通信についての詳細
リンク
curl -I https://qiita.com
→失敗
- -I はHTTP HEADリクエスト(レスポンスのヘッダーだけ要求)を使う指定
- 今回の目的はQiitaのページを取得することではなく、QiitaへのHTTPS通信が成立するか確認するかどうかの確認だけなのでこれを指定
- さらに、以下も試した:
curl: (35) Send failure: Connection was reset
curl --tlsv1.2 -I https://qiita.com
→いずれも失敗
つまり、
- HTTPS通信は失敗
- TLSバージョン指定しても失敗
- エラーコード
[cause]: Error: read ECONNRESETより、そもそもTLSハンドシェイクするまえの
TCP接続の時点で失敗している可能性が高い
インターネットアクセスに問題はないか?
curl -I https://www.google.com
curl -I https://github.com
も実行
結果:
Google → 成功
GitHub → 成功
Qiita → 失敗
つまり、
PC
↓
インターネット
↓
Google OK
GitHub OK
Qiita NG
なので、「PCのインターネット接続そのものが死んでいる」わけではない。
Qiitaの名前解決結果を確認
次に、
nslookup qiita.com
を実行
※nslookupはDNSサーバーに問い合わせてドメイン名からIPアドレスを調べたり、逆にIPアドレスからドメイン名を調べるためのコマンドラインツール
→Qiitaには、
- IPv6アドレス(2600:9000:...)
- IPv4アドレス(99.84.48....)
の2つの住所が存在することが分かった
つまり、
qiita.com(サーバープログラム)
↓ DNS
├── IPv6アドレス
└── IPv4アドレス
という状態
IPv4 & IPv6とは?
どちらもIPアドレスのバージョン(規格)
- IPv4
- 例:99.84.48.46
- 昔からの規格
- IPb6
- 例:2600:9000:208e:6000:8:e1de:4d80:93a1(めちゃ長い)
- IPv4よりも、はるかにたくさんの住所を作れる
- 最終的には同じサービス(プログラム)に到達するものの、IPv4とIPv6は別々のIPアドレスなので、ネットワーク上では別の宛先
qiita.com
│
┌─────────┴─────────┐
↓ ↓
IPv4アドレス IPv6アドレス
99.84.48.xxx 2600:9000:...
↓ ↓
IPv4経路 IPv6経路
↓ ↓
└──── Qiitaのサービス ────┘
IPv4とIPv6を分けて試す
まず、
curl -4 -I https://qiita.com
- 意図:IPv4を使ってQiitaにアクセス
- 結果:成功
次に、
curl -6 -I https://qiita.com
- 意図:IPv6を使ってQiitaにアクセス
- 結果:失敗(Connection reset)
→今回の通信失敗は、Qiita CLIがQiitaへIPv6で接続しようとした場合に発生している可能性が高い
Qiita CLIが実際にIPv6を選んでいるか確認
- Qiita CLIはNode.js上で動いている
- そこでNode.jsに、
node -e "require('dns').lookup('qiita.com', (err, address, family) => console.log(err || {address, family}))"
を実行させる
結果:
{
address: '2600:9000:...',
family: 6
}
つまり、この環境ではNode.jsのDNS lookupが
qiita.com
↓
IPv6アドレス
を返していた。
つまり、
Qiita CLI
↓
Node.js
↓
DNS
↓
IPv6を選択(×)
↓
Qiita
↓
Connection reset
という今回の失敗経路がかなり明確になった。
Node.jsに「IPv4を先に選ばせる」
set NODE_OPTIONS=--dns-result-order=ipv4first
→そのコマンドプロンプトのセッションでは、Node.jsがIPv4を優先する状態Node.jsがDNSからIPv4とIPv6の両方を受け取ったとき、IPv4の結果を優先させるようにした
もう一度Qiita CLIでログイン
そして、
npx qiita login
を再実行。
→今度は、ログイン成功🎉
なぜIPv6ではだめだったのか?
今回はそこまでは調べてないが、候補としては例えば、
- 自宅ルーターのIPv6設定
- ISP(インターネット回線事業者)のIPv6接続
- ISP側のIPv6経路
- Qiita側/CDN側のIPv6対応
- その間のネットワーク機器
などがある