Railway 2026年7月2日障害から学ぶ「デフォルトルート依存」の罠と再現ラボ

2026年7月2日、PaaSのRailwayで米国東部(US East)を中心とする障害が発生した。同社の公開インシデントレポートによると、上流ISPのバックボーン劣化に端を発した一連の対応の途中で、対象拠点が「一時的にインターネットへの全経路を失う」という二次障害が約20分間発生している。本記事ではこの障害を、ネットワークエンジニアの視点で「なぜ経路が消えたのか」に絞って解説し、その中核であるデフォルトルート依存を手元で再現する。

何が起きたか(事実)

Railwayの公式レポートに基づく事実は以下のとおり(時刻はUTC、2026年7月2日)。

  • 07:44 — 米国リージョン間トラフィックでパケットロスを観測。障害を宣言。原因は上流キャリアの米国バックボーン劣化で、US WestとUS East間が飽和した。
  • 07:44–08:32 — 劣化したキャリアを米国境界で切り離す作業。
  • 08:39 — 対象ゾーンでセカンダリキャリアも切り離したところ、その拠点からインターネットへの全経路が消失
  • 08:59 — セカンダリキャリアを再接続し、ルーティングが安定化。
  • 11:49 — メッシュネットワーキングのエージェントを再起動し、拠点内の私設トンネル(プライベートネットワーキング)が全復旧。
  • 12:01 — インシデント解決。

Railwayは各データセンターに「最低3社」のTier 1キャリアを接続して冗長性を確保している。それでも全経路が消えたのはなぜか。レポートの一文が核心を突いている。

The zone is one of our first-generation sites, and unlike our newer datacenters, it gets its default route (the catch-all path to the internet) from its carriers rather than generating one itself.

訳すと「このゾーンは第一世代の拠点で、新しいデータセンターと違い、デフォルトルート(インターネットへの包括的な経路)を自分で生成せず、キャリアから受け取っていた」。つまり、劣化キャリアとセカンダリキャリアの両方を切ったことで、0.0.0.0/0 を教えてくれる相手がいなくなり、拠点は外に出る経路を失った。なお同レポートにBGPという語は明記されていないが、Tier 1キャリアとの経路交換という文脈から、以下では一般的なBGPでのデフォルトルート伝搬として仕組みを解説する(この対応付け自体は筆者による技術的な補足で、公式の断定ではない)。

技術的な仕組み:デフォルトルートは「誰が生成するか」で挙動が変わる

ルータは宛先が具体的なルートに一致しないパケットを、デフォルトルート 0.0.0.0/0(IPv6なら ::/0)へ流す。このデフォルトルートの出どころには大きく2通りある。

  1. 学習型(learned): 上流のキャリアからデフォルトルートを広告してもらう。BGPでは上流側が neighbor <PEER> default-originate を設定すると、自分の経路表に 0.0.0.0/0 があるかに関わらずピアへ 0.0.0.0/0 を送れる。受け手はそれを経路表に入れて外に出る。
  2. 自己生成型(self-originated): 拠点自身が静的に 0.0.0.0/0 を持ち、それを内部へ配る(例: ip route 0.0.0.0/0 を静的に置き、内部プロトコルへ再配布する)。上流はあくまで具体的な宛先の到達性を提供する役割に徹する。

学習型は設定が単純だが、致命的な弱点がある。デフォルトルートの存在が「上流セッションが生きているか」に完全に従属する点だ。上流ピアを全部落とせば、学習していた 0.0.0.0/0 は経路表から一斉に消える。今回の「セカンダリも切ったら全経路が消えた」は、まさにこの学習型の性質そのものだ。

自己生成型なら、上流を全部落としても拠点内の 0.0.0.0/0 は残る(出口が塞がるので実際の到達性は落ちるが、経路表から default が消えて“インターネットの存在ごと見失う”ことはない)。運用上の差は大きく、Railwayも再発防止として第一世代拠点を自己生成型へ移行すると述べている。

要点は「冗長キャリアが3社あっても、デフォルトルートの生成を全部上流に委ねていれば、それは3社同時断(あるいは運用者が意図せず全断してしまう操作)に対して単一障害点になり得る」ということだ。

実際に試す

キャリア断でデフォルトルートが消える様子を、(A) 依存関係のロジックをPythonで、(B) 実ルーティングをLinuxの network namespace で、それぞれ手元に再現する。

前提

  • (A) Python 3.8 以上(標準ライブラリのみ、追加インストール不要)。macOS / Linux / Windows いずれも可。
  • (B) Linux(iproute2ip コマンドが必要)。root権限(sudo)が必要。Ubuntu 22.04 / 24.04 で確認できる。macOSには ip netns が無いため (B) はLinux専用。

(A) デフォルトルート依存のロジックを再現する

「学習型(self_originates=False)」と「自己生成型(self_originates=True)」で、キャリアを全断したときの挙動差を最小モデルで示す。次を route_sim.py として保存する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#!/usr/bin/env python3
"""default-route dependency simulator:
学習型 vs 自己生成型 で、キャリア全断時の挙動差を示す最小モデル。"""
from dataclasses import dataclass, field

@dataclass
class Site:
name: str
carriers: list # 接続中のキャリア名
self_originates: bool # 自分で 0.0.0.0/0 を生成するか
_learned_from: set = field(default_factory=set)

def on_carrier_up(self, carrier):
self.carriers.append(carrier)
if not self.self_originates:
self._learned_from.add(carrier)

def on_carrier_down(self, carrier):
if carrier in self.carriers:
self.carriers.remove(carrier)
self._learned_from.discard(carrier)

@property
def has_default_route(self):
# 自己生成なら常に有効。学習依存なら学習元が1つ以上必要。
if self.self_originates:
return True
return len(self._learned_from) > 0

def status(self):
return "REACHABLE" if self.has_default_route else "*** ISOLATED (no 0.0.0.0/0) ***"


def drain_scenario(site):
print(f"[{site.name}] self_originates={site.self_originates}")
print(f" init carriers={site.carriers} -> {site.status()}")
site.on_carrier_down("carrier-A(degraded)") # 劣化した primary を切る
print(f" drop primary -> carriers={site.carriers} -> {site.status()}")
site.on_carrier_down("carrier-B") # 誤って secondary も切る(08:39)
print(f" drop secondary -> carriers={site.carriers} -> {site.status()}")
site.on_carrier_up("carrier-B") # secondary を戻す(08:59)
print(f" restore B -> carriers={site.carriers} -> {site.status()}")
print()


for so in (False, True):
s = Site("us-east-zone", carriers=[], self_originates=so)
s.on_carrier_up("carrier-A(degraded)")
s.on_carrier_up("carrier-B")
drain_scenario(s)

実行と期待出力:

1
python3 route_sim.py
1
2
3
4
5
6
7
8
9
10
11
[us-east-zone] self_originates=False
init carriers=['carrier-A(degraded)', 'carrier-B'] -> REACHABLE
drop primary -> carriers=['carrier-B'] -> REACHABLE
drop secondary -> carriers=[] -> *** ISOLATED (no 0.0.0.0/0) ***
restore B -> carriers=['carrier-B'] -> REACHABLE

[us-east-zone] self_originates=True
init carriers=['carrier-A(degraded)', 'carrier-B'] -> REACHABLE
drop primary -> carriers=['carrier-B'] -> REACHABLE
drop secondary -> carriers=[] -> REACHABLE
restore B -> carriers=['carrier-B'] -> REACHABLE

学習型は secondary を切った瞬間に ISOLATED になり、自己生成型はキャリアがゼロでもデフォルトルートを保持する。これがRailwayの二次障害の本質だ。

(B) 実ルーティングで再現する(Linux / network namespace)

site 名前空間の外向き経路を carrier 名前空間経由の学習型デフォルトルートとして設定し、キャリア断で 0.0.0.0/0 が消えて疎通が失われる様子を実際のルーティングで見る。setup スクリプトを netns_lab.sh として保存する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env bash
set -euo pipefail
# 2つの名前空間 site / carrier を veth で接続する
ip netns add site
ip netns add carrier

ip link add veth-s type veth peer name veth-c
ip link set veth-s netns site
ip link set veth-c netns carrier

ip netns exec site ip addr add 10.0.0.2/24 dev veth-s
ip netns exec carrier ip addr add 10.0.0.1/24 dev veth-c
ip netns exec site ip link set veth-s up
ip netns exec carrier ip link set veth-c up
ip netns exec site ip link set lo up
ip netns exec carrier ip link set lo up

# site は「学習型」= デフォルトルートを carrier(10.0.0.1) 経由で受け取る想定
ip netns exec site ip route add default via 10.0.0.1 dev veth-s
echo "== site の経路表(キャリア接続中)=="
ip netns exec site ip route

実行:

1
sudo bash netns_lab.sh

期待出力(学習したデフォルトルートが入っている):

1
2
3
== site の経路表(キャリア接続中)==
default via 10.0.0.1 dev veth-s
10.0.0.0/24 dev veth-s proto kernel scope link src 10.0.0.2

次に「キャリアを全断」する操作を再現する。学習型では上流断はデフォルトルートの消失に相当するので、ip route del default で表現できる。

1
2
3
4
5
6
# キャリア全断 = 学習していた default が消える
sudo ip netns exec site ip route del default
echo "== site の経路表(全断後)=="
sudo ip netns exec site ip route
# 外向きの宛先へのルート解決を試す(default が無いので解決不能)
sudo ip netns exec site ip route get 8.8.8.8 || true

期待出力:

1
2
3
== site の経路表(全断後)==
10.0.0.0/24 dev veth-s proto kernel scope link src 10.0.0.2
RTNETLINK answers: Network is unreachable

default が消え、直結の 10.0.0.0/24 以外は Network is unreachable。これが「拠点がインターネットの経路を失う」状態そのものだ。

対して「自己生成型」なら、上流に関係なく拠点自身が 0.0.0.0/0 を持つ。同じ site 名前空間で、上流断の状況でもデフォルトルートを自前で保持できることを確認する(blackhole を使うと出口が無くても経路表にはdefaultが残る様子を安全に再現できる)。

1
2
3
4
5
# 自己生成型: 上流に依存せず default を自分で持つ(ここでは出口の有無と分離して確認)
sudo ip netns exec site ip route add blackhole default
echo "== site の経路表(自己生成 default)=="
sudo ip netns exec site ip route
sudo ip netns exec site ip route get 8.8.8.8

期待出力(default が経路表に存在し、ルート解決が成功する=“インターネットを見失わない”):

1
2
3
4
5
== site の経路表(自己生成 default)==
blackhole default
10.0.0.0/24 dev veth-s proto kernel scope link src 10.0.0.2
8.8.8.8 dev lo src 10.0.0.2 uid 0
cache

blackhole なので実パケットは破棄されるが、狙いは「上流ゼロでも default が経路表に残るか」の対比。自己生成型は残り、学習型は残らない。)

後片付け:

1
2
sudo ip netns del site
sudo ip netns del carrier

まとめ

Railwayの2026年7月2日障害は、上流ISP劣化そのものよりも、対応中に「デフォルトルートをキャリア任せにしていた拠点で、上流を全断したら全経路を失った」という二次障害が実務的な教訓になる。冗長キャリアが何社あっても、デフォルトルートの生成を全部上流に委ねれば単一障害点化しうる。自拠点でデフォルトルートを自己生成しておけば、上流全断でも“インターネットの経路そのものを見失う”事態は避けられる。上の(A)/(B)は、その挙動差をコードとルーティングの両面で手元に再現できる。

出典: Railway 公式インシデントレポート(July 2, 2026)FRRouting BGP ドキュメント(default-originate)


Railway 2026年7月2日障害から学ぶ「デフォルトルート依存」の罠と再現ラボ
https://blog.hashito.biz/2026/07/10/try-railway-2026-07-02-default-route-outage/
著者
hashito
作成日
2026年7月10日
著作権