オンライン面接の最中に、家のネットが切れた。10 時から 11 時までの面接で、切れたのは 10:00 と 10:53 の 2 回、どちらも 3〜4 分。よりによって、という最悪のタイミングだった。
この家の回線が「ping は通るのに Web だけ開かない」形で落ちるのは初めてではない。この連載で何度も書いてきた症状で、10 月 8 日に IIJ へ問い合わせて、ようやく正体が分かっていた。IPv4 over IPv6(DS-Lite)では、IPv4 の通信に使えるポートが 1 回線あたり 1024 本まで。それを家の中の機械が使い切っていた。
今日やったことは 2 つある。家の中で一番ポートを使っていた巡回ツールの接続を半分以下に絞ったことと、UniFi Cloud Gateway Ultra の「単一ネットワーク」で LAN に IPv6 を配り、IPv6 で届く通信をトンネルの外に逃がしたことだ。後者は、最初は API の設定項目が 1 つ空のせいで配られず、管理画面を見てようやく原因が分かった。調べ物と設定の作業は Claude Code が行い、私はその途中で何度も質問を挟んだ。その質問が流れを変えたところもあるので、会話の順に書いていく。
結論から書く。
- 原因: DS-Lite の出口(AFTR)で IPv4 のポートが 1024 本を超えた。面接の時間帯は、毎時の巡回ツールと開発機の作業が重なって、ルーターから見えるだけで 644〜1,157 本に達していた
- 対策 1: 巡回ツールのページを開く間隔を 4 秒あけ、広告・計測の通信を止め、DNS をルーター経由にした。巡回中のピークは 511〜857 本 → 282 本
- 対策 2: UCG Ultra の WAN で「単一ネットワーク」を選び、受け取った /64 を LAN に配った。IPv6 の通信は AFTR を通らないので、ポートを消費しない。開発機の外向きの IPv4 の接続は 0 本になり、同じ E2E テストが走っていても家全体の山は 1,000〜1,233 本 → 250〜470 本になった
- 落とし穴: API で設定するときは
ipv6_wan_delegation_typeだけでなくsingle_network_lan(配る先のネットワーク)も入れる。管理画面は空でも「Default」と表示する - 守り: 各端末が外から届くアドレスを持つので、外から入れないことを Cloudflare Workers から実際に接続して確かめた
何が起きていたか: 1024 本の上限
まず仕組みを図にする。DS-Lite では、家の IPv4 の通信を IPv6 に包んで ISP の網まで運び、出口の AFTR で包みを開いて、多くの家で共有している IPv4 アドレスに変換して外に出す。家ごとに使えるポートの数は、この AFTR で決まっている。
1024 本を使い切ると、新しい接続だけが AFTR で拒否される。すでに張ってある接続は切れず、ping(ICMP)も通る。この連載で「TCP だけ死ぬ」と書いてきた症状はこれだった。以前の記事では「AFTR の装置が不調になる」と仮説を立てていたが、IIJ の回答で、装置の不調ではなく枠の使い切りだと分かった。AFTR を隣のアドレスに切り替えると即座に直るのは、割り当てが空の AFTR から始め直せるからだ。
ここで大事なのは、閉じた接続もすぐには枠を返さないことだ。通信を終えた接続も、しばらくは TIME_WAIT としてポートを持ったままになる。短い接続を大量に張って閉じると、本数はすぐに積み上がる。
面接の時間帯に、誰が使っていたか
10 月 9 日には、宅内の監視が自分で張る接続を 1 分あたり約 224 本から約 32 本に減らしていた。それでも切れたので、ルーターの接続の一覧(conntrack)を 30 秒ごとに記録する仕組みを 12 日に入れてあった。今日の記録を、断の時刻で引いた。
| 時刻 | 断 | 家全体の IPv4 接続 | 一番使っていた機械 |
|---|---|---|---|
| 09:53〜09:56 | 3 分 | 869 本 | Raspberry Pi 511 本(巡回ツール) |
| 10:00〜10:03 | 3 分 | 644〜729 本 | 開発機 約 250 本・Raspberry Pi 約 210 本・Mac 105〜210 本 |
| 10:53〜10:57 | 4 分 | 1,051〜1,157 本 | Raspberry Pi 757〜857 本(巡回ツール) |
毎時 53 分の断は、Raspberry Pi で動かしている自作の巡回ツールが主因だった。懸賞の情報を集めて応募ページを読むツールで、1 時間に 1 回、ヘッドレスブラウザで数十のページを開く。10:00 の断は、その巡回の残りと、開発機の作業(Google 宛てに 208 本)と、面接中の Mac が重なっていた。本数は 53 分より少なく、1024 本に届く前に切れている。閉じた接続がルーターの一覧から消えたあとも、AFTR 側ではまだ枠を押さえている、と考えると説明がつくが、AFTR の中は見えないので確かめようがない。
この巡回ツールが主な消費元だということは、前日の分析で分かっていた。「並列度を下げる」と結論を出しておきながら、手を入れないまま今朝を迎えた。
対策 1: 巡回ツールの接続を絞る
Claude がコードを開いて最初に分かったのは、巡回ツールはもともと 1 ページずつ順番に開いていて、並列ではなかったことだ。並列度を下げる余地は無い。本数が膨らむ理由は別にあった。
接続の一覧を、巡回中の Raspberry Pi の分だけ取り出して数えると、こうなっていた。
- 1 ページ開くたびに、広告・計測・SNS 埋め込みのサーバーにも接続していた(断の最中、Facebook の配信網だけで 20〜31 本)
- DNS の問い合わせが全部 8.8.8.8 に直接出ていた。巡回中は UDP だけで 90〜150 本
- 2〜3 分に数十ページを詰めて開くので、閉じた接続の TIME_WAIT がその間に積み上がる
DNS の件は Docker の挙動が原因だった。Raspberry Pi の /etc/resolv.conf は 127.0.0.1(自分の中の DNS)を向いている。コンテナの中から 127.0.0.1 は使えないので、Docker は既定の 8.8.8.8 に切り替える。名前を 1 回引くたびに、ポートを 1 本使っていたわけだ。
直したのは 3 か所で、どれも小さい。
// ページを開く間隔を最低 4 秒あける(テストでは 0)
let lastOpen = 0;
async function pace() {
const gap = Number(process.env.PAGE_GAP_MS ?? 4_000);
const wait = lastOpen + gap - Date.now();
if (wait > 0) await new Promise((r) => setTimeout(r, wait));
lastOpen = Date.now();
}
export async function newPage(browser) {
await pace();
const page = await browser.newPage();
// 画像・動画・フォントと、広告・計測・SNS 埋め込みは読まない
await page.route("**/*", (route) => {
const req = route.request();
return SKIP.has(req.resourceType()) || isTracker(req.url()) ? route.abort() : route.continue();
});
return page;
}
広告・計測の一覧には、Facebook・Google タグマネージャー・DoubleClick などのホストを並べた。ただし reCAPTCHA の通信は止めていない。応募フォームに CAPTCHA があるかどうかの判定に使うからだ。3 つ目の DNS は、コンテナの定義に dns: [ルーターの IP] を足して、ルーターに問い合わせるようにした。ルーターがキャッシュするので、外への問い合わせが減る。
同じページを、止める前と後で開いて接続先を数えた。
| ページ | 止める前 | 止めた後 |
|---|---|---|
| PR TIMES の記事 | 13 ホスト | 3 ホスト |
| 関西の情報サイト | 48 ホスト | 10 ホスト |
反映した直後の巡回では、Raspberry Pi のピークが 282 本(対策前は 511〜857 本)、家全体では 574 本に収まり、外形監視の断も無かった。巡回にかかる時間は 2〜3 分から約 6 分に延びたが、1 時間に 1 回の作業なので問題はない。
1.1.1.1 はどこから来たのか
接続の一覧には、ルーター自身が 1.1.1.1 に DNS を問い合わせる UDP も常に数十本あった。私は「なんでそんな設定になっているのか、何のためか」と聞いた。
調べると、誰かが選んだ設定ではなかった。WAN の DNS は「自動」のままで、DS-Lite では回線側が IPv4 の DNS サーバーを配らないので、欄が空になる。そこで UniFi が予備の 1.1.1.1 を使っていた。この問い合わせもトンネルを通る。
WAN の IPv6 側の DNS に Cloudflare と Google の IPv6 アドレスを手で入れると、IPv6 での問い合わせが流れ始めた。ところが 1.1.1.1 宛ての IPv4 も残った。IPv4 側の欄を空にする変更は、UniFi に api.err.MissingDnsServer で拒否された。効果は半分ほどにとどまっている。
ここで私は、「1.1.1.1 は IPv4 だから、IPv4 にしか対応していないサイトを引くのに残っているのは自然では?」と聞き返した。Claude の答えは、DNS の問い合わせを IPv4 で送るか IPv6 で送るかと、答えに IPv4 のアドレス(A レコード)をもらうか IPv6 のアドレス(AAAA レコード)をもらうかは別の話、というものだった。IPv6 の窓口に問い合わせても、IPv4 だけのサイトの A レコードはきちんと返ってくる。1.1.1.1 が残るのは、ルーターの中の DNS が登録された窓口に振り分けて送っているからで、問い合わせの中身で送り分けているわけではない。
IPv6 で直接つなげば、トンネルを通らないのでは
ここで私は、根本的な質問をした。「1024 ポートを通れないのはトンネルの話だよね。IPv6 で直接つなげば、このトンネルを通らないという理屈だよね」
その理屈で合っていた。IPv6 の通信は AFTR で変換されないので、ポートの枠を使わない。Google・YouTube・Cloudflare・Facebook・Apple など、家の通信の多くの行き先は IPv6 を持っている。
問題は、家の中に IPv6 を配っていなかったことだ。ONU に UCG Ultra を直結してから、LAN の端末は IPv4 しか持っていない。実は 12 日にも同じ案を私が出していて、そのとき Claude は「回線側からプレフィックス委任(PD)が来ないので配れない」と書いて止めていた。NTT 東の光回線では、ひかり電話を契約してホームゲートウェイを置かないと、PD で家の中用の範囲がもらえない。ひかり電話の無いうちでは、WAN に /64 が 1 つ来るだけだ。
今回改めて調べると、12 日の結論は調べ不足だった。UniFi には 「単一ネットワーク」(Single Network) という設定がある。WAN で受け取った /64 を、1 つの LAN にそのまま使う機能だ。ひかり電話なしで /64 が 1 つだけ、という状況にちょうど合う。
「1 つしか割り当てられていない」のに、なぜ配れるのか
私は「本来 1 つしか割り当てられていないものを、どうやって家中で使うのか」と聞いた。
/64 はアドレス 1 個ではなく、アドレスの範囲だ。中には 2 の 64 乗、約 1,800 京個のアドレスがある。数は問題にならない。問題は、回線側がその範囲を「WAN の線にぶら下がっている」と思っていることだ。NTT 側のルーターは、その範囲宛ての通信が来ると、WAN の線に向かって「このアドレスの人はいますか?」と尋ねる(近隣探索、ND)。
単一ネットワークでは、ルーターがこの問いに代わりに答える。
NTT 側から見ると、家の端末が全部 WAN の線に直接つながっているように見える。日本の家庭用ルーター(NEC の Aterm やバッファローなど)は、この「IPv6 ブリッジ」「パススルー」にあたる機能をたいてい最初から持っている。/64 が 1 つだけの状況を想定していない海外製の UniFi が、珍しく対応していた、というのが実際のところだ。
デメリットは何か
私はデメリットも聞いた。Claude が挙げたのは次の 5 つだった。
- 外からの入口の守りが、ファイアウォールだけになる。IPv4 では NAT のおかげで外から端末に直接届かなかったが、IPv6 では各端末が外から届くアドレスを持つ
- IPv6 を使える LAN は 1 つだけ(範囲が 1 つなので)。うちは LAN が 1 つなので影響なし
- 「IPv4 では通るが IPv6 では通らない」という新しい種類の不調が起こりうる
- 回線をつなぎ直して /64 が変わると、家中の IPv6 アドレスが一斉に変わる
- 古い家電は MAC アドレスから IPv6 アドレスを作るので、識別されやすい
私は「それって UCG にファイアウォールがあれば大丈夫ってことでは」と聞いた。そのとおりで、UniFi の既定は「家の中から始めた通信の返事だけ通し、外から始まる通信は止める」だ。ただ、LAN に IPv6 を配ったことが一度も無いので、その規則は一度も試されていない。だから「はずだ」ではなく、外から実際に試すことにした。
試行錯誤: API で入れても配られない
UCG Ultra の操作は、これまでも管理画面ではなく API で行ってきた。設定値の名前が分からないので、最初に使っていない WAN(Internet 2)で試した。でたらめな値を入れると api.err.InvalidValue で拒否され、single_network は受け付けられた。値の検査があるので、受け付けられたら有効な値だと読める。
# 使っていない WAN2 で、値の名前が正しいかだけ確かめる
curl -sk -X PUT -H "X-API-KEY: $KEY" -H 'Content-Type: application/json' \
"$BASE/api/s/default/rest/networkconf/$WAN2_ID" \
-d '{"ipv6_wan_delegation_type":"zzz_invalid"}' # → api.err.InvalidValue
curl ... -d '{"ipv6_wan_delegation_type":"single_network"}' # → rc: ok
本番の WAN に single_network を入れ、IPv4 が切れないことを 10 秒ごとに見張った。切れなかった。ところが、家の中の端末に IPv6 が付かない。開発機で 70 秒待ち受けても、ルーターからの案内(RA)が 1 つも流れてこない。LAN 側にも ipv6_interface_type に single_network を入れ、設定の再適用(force-provision)をかけ、WAN の IPv6 を一度オフにしてオンに戻した。どれも変わらなかった。
API が受け付ける値を当て推量で入れ続けても先が無いので、ここで手を止めた。管理画面に何が出ているかを、Cowork(ブラウザを操作する Claude)に書き写してもらった。設定を変えないこと、保存を押さないことを依頼書に書いて渡した。
返ってきた報告で、2 つのことが分かった。
- WAN 側には「プレフィックス委任 / 単一ネットワーク」の選択肢があり、単一ネットワークの下に 「ネットワーク: Default」という配る先の欄がある
- LAN 側の選択肢は「なし / プレフィックス委任(灰色で選べない)/ スタティック」で、単一ネットワークという選択肢は無い。API で入れた LAN 側の値は、画面では「なし」と表示されていた
配る先は、LAN 側ではなく WAN 側で指定する作りだった。WAN の設定を API で読み直すと、single_network_lan という項目があり、中身が空だった。管理画面は、空でも先頭の「Default」を表示していたことになる。
ここに Default の ID を入れ、LAN 側に入れていた値は画面の想定どおり「なし」に戻した。入れた次の 10 秒で、ルーターの LAN に /64 が割り当てられ、開発機に IPv6 のアドレスと既定の経路が付いた。設定した瞬間に IPv4 が 10 秒ほど 1 回だけ切れたが、すぐ戻った。
curl -sk -X PUT -H "X-API-KEY: $KEY" -H 'Content-Type: application/json' \
"$BASE/api/s/default/rest/networkconf/$WAN1_ID" \
-d '{"single_network_lan":"<Default の _id>"}'
API だけで設定するなら、ipv6_wan_delegation_type と single_network_lan を必ず一緒に入れる。
外から入れないことを確かめる
IPv6 で外に出られることは、すぐに確かめられた。curl -6 で Google に 204 が返り、Cloudflare から見た送信元も開発機の IPv6 アドレスだった。
外から入れないことの確認には手間がかかった。私が使えるサーバーは IPv6 を持っていない。外部の検査サービスは IPv6 アドレスの直接指定を受け付けなかった。そこで Cloudflare Workers の connect()(TCP ソケット)で、Cloudflare の外側から開発機につないでみることにした。wrangler dev --remote で一時的に動かすだけで、本番には何も足していない。
ここでも 2 回つまずいた。
- 対照として Cloudflare の DNS につなぐと、即座に失敗した。Workers は Cloudflare 自身のアドレスにはつなげない。対照を Google の DNS に替えた
- IPv6 アドレスを直接書くと、どこにつないでも 0 ミリ秒で失敗した。Workers は IPv6 アドレスの直書きを受け付けない。ホスト名ならつながる
2 つ目は、IPv6 アドレスをそのままホスト名に変える公開の DNS サービス、sslip.io で回避した(2001-4860-4860--8888.sslip.io のように、コロンをハイフンに替える)。
| つなぎ先(外から IPv6 で) | 結果 |
|---|---|
| Google の DNS(対照・開いている) | 接続できた(93 ミリ秒) |
| 開発機の ssh | 届かない(6 秒で時間切れ) |
| 開発機のもう 1 つの待ち受け | 届かない(6 秒で時間切れ) |
対照にはつながり、開発機には届かない。UCG Ultra のファイアウォールが、外から始まる通信を止めている。
効果: 山が逃げた
設定した直後の開発機では、外向きの IPv4 の接続が 0 本になり、確立済みの 60 本がすべて IPv6 に移っていた(Anthropic の API・Cloudflare・Google など)。
今日の午後には、ちょうど比べやすい材料があった。別の作業で開発機が Playwright の E2E テストを流していて、テストのたびに新しいブラウザで Google Fonts を読み込み直していた。その Google 宛ての接続が 700 本ほどあり、家全体の IPv4 の接続は 14:52〜15:44 に 1,000〜1,233 本まで上がっていた。面接中と同じ、断の一歩手前だ。
IPv6 を配ったのは 15:55。同じ E2E テストが動いたまま、家全体は 250〜470 本に下がった。
私は「E2E って Google Fonts だったよね。これは IPv4 なのかな」と聞いた。Claude が E2E と同じ Chromium で Google Fonts を読み込み、つながった先を記録した。
fonts.googleapis.com -> [2404:6800:400b:c00c::5f]
fonts.gstatic.com -> [2404:6800:400b:c00c::5e]
どちらも IPv6 だった。Chromium は IPv6 が使えるときは IPv6 を優先する。テストが何度 Google Fonts を取り直しても、もうトンネルの枠は使わない。
一方で、平常時の本数はあまり減っていない。夕方の家全体は 370 本で、昨日の同じ時刻(196 本)より多いくらいだ。平常時の大半は Raspberry Pi の監視と巡回ツールで、どれも Docker のコンテナから出ている。コンテナは IPv6 を持っていないので、相手が IPv6 を持っていてもトンネルを通る。回線を落とすのは 1,000 本を超える山なので、今回の効果は「山が逃げた」と表すのが正確だ。
IPv6 の通信は、どこを通っているのか
最後に私は、「IPv6 なら transix を経由せずに直接アクセスできるのか」と聞いた。経路を測ると、こうなっていた。
| 段 | 何か |
|---|---|
| 1 | 家の UCG Ultra |
| 2 | NTT 東の光回線網のルーター |
| 3〜4 | (応答なし) |
| 5 | 2404:8e00:feed:ff1c::2(transix の網) |
| 6〜 | (応答なし)。その先が Google |
2404:8e00:: は、transix を運営する Internet Multifeed のアドレス帯だ。IPv6 の通信も transix の網は通る。ただし、5 段目は普通の中継ルーターで、変換もポートの割り当てもしない。AFTR と同じアドレス帯でも、AFTR そのものではない。
transix には役目が 2 つある。IPv6 をそのまま中継する道(IPoE)と、IPv4 を包んで運ぶトンネルの出口(AFTR)だ。1024 本の枠があるのは後者だけで、IPv6 の通信は前者だけを通る。同じ高速道路を走るけれど、混んでいる料金所のレーンには入らない、という形になる。
まとめ
- DS-Lite(transix など)では、IPv4 の通信に使えるポートが 1 回線 1024 本。閉じた接続も TIME_WAIT の間は枠を持ったままなので、短い接続を大量に張るツールがあると、すぐに使い切る
- 使い切ると、新しい TCP 接続だけが拒否される。ping は通り、確立済みの接続も切れない
- ヘッドレスブラウザの巡回は、1 ページごとの広告・計測の接続が本数の大半を占める。止めるだけで、接続先のホストが 4 分の 1 になったページもあった
- Docker のコンテナは、ホストの
resolv.confが127.0.0.1だと 8.8.8.8 に直接問い合わせる。DNS の問い合わせもポートを使う - UniFi で、ひかり電話なし(/64 が 1 つ)の回線の LAN に IPv6 を配るなら、WAN の IPv6 を SLAAC にして「単一ネットワーク」を選ぶ。API で設定するときは
single_network_lanを必ず入れる(管理画面は空でも Default と表示する) - IPv6 を配ったら、外から入れないことを実際に試す。外に IPv6 の足場が無くても、Cloudflare Workers の
connect()と sslip.io で確かめられる - 平常時の本数を占める Docker のコンテナには、まだ IPv6 が無い。コンテナに IPv6 を持たせるのは、次の宿題になった