Linux で転送を有効にしたら IPv6 のデフォルトルートが消えた
IPv6 転送を有効にするとカーネルはアップリンクのルーター広告を無視し、学習したデフォルトルートが期限切れになる。accept_ra で直す。
VPN クライアントの通信を通すために Debian ゲートウェイで net.ipv6.conf.all.forwarding=1 を設定したところ、およそ半時間後にそのマシンには IPv6 のデフォルトルートが残っていなかった。上流のルーターは生きており、link-local の隣接ホストもまだ応答し、アップリンクもグローバルアドレスを保持していた。止まっていたのは、そのインターフェースでルーター広告を受け取り続けるというカーネルの姿勢だった。sysctl を打った瞬間にルートが消えたのではなく、RA から学習したデフォルトルートの寿命が切れたときに消え、誰も更新しなかったのだ。
一見したところ壊れているようには見えなかった。転送は機能し、古いルートがキャッシュに残っている間はクライアントも通っていたし、障害は原因となった変更より遅れて現れた。その遅れのせいで、無関係な上流や関与していないファイアウォール規則のせいにされる。
どの sysctl に触る前にもルート表を読む
まず ip -6 route show default を実行し、ゲートウェイだけでなく属性を読む。RA から学習したデフォルトは proto ra と表示され、優先度を持ち、expires の残り時間を示す。手で追加したルートにはどれもなく、この違いがどちらの仕組みがルートを所有しているかを教えてくれる。
$ ip -6 route show default
default via fe80::1 dev eth0 proto ra metric 1024 expires 1432sec hoplimit 64 pref medium次にアドレスを見る。ip -6 addr show dev eth0 だ。時間単位で測られる優先寿命と有効寿命を持つグローバルアドレスは、分単位で測られるルーター寿命のデフォルトルートより長く生き残る。アドレスがあってデフォルトルートがないのが、広告の問題の典型だ。アドレスもなければ、まずリンク、プレフィックス情報、重複アドレス検出を追うことになる。
なぜ転送で RA の処理が止まるのか
RA はタイプ 134 の ICMPv6 パケットで、ホップ限界 255 の link-local 送信元から送られる。カーネルが使う前に、インターフェースの accept_ra を参照する。既定値は 1 で、1 は「はい」ではない。このホストが転送していない間だけ受け取る、という意味だ。forwarding=1 にするとその条件が崩れ、広告に含まれるプレフィックスもデフォルトルーターも検討される前に破棄される。
値は三つあり、転送を生き延びるのは三つ目だけだ:
accept_ra | forwarding=0 | forwarding=1 |
|---|---|---|
| 0 | ignored | ignored |
| 1 (default) | accepted | ignored |
| 2 | accepted | accepted |
さらに二つのフラグが実際に何を入れるかを決める。accept_ra_defrtr は広告内のデフォルトルーターをルートにするか、accept_ra_pinfo はプレフィックス情報をアドレス設定に使うかを制御する。accept_ra_defrtr を切るのが、転送ホストでデフォルトを学習せずに SLAAC アドレスを保つ通常の方法だ。これは accept_ra とは別のレバーで、アドレスは欲しいがルートはいらないときに使う。カーネルの IP sysctl リファレンス に三つとも載っている。
三つのコマンドで確認する
まず、読み込まれていると思うファイルではなく、実際の値を読む。
sysctl net.ipv6.conf.eth0.forwarding net.ipv6.conf.all.forwarding \
net.ipv6.conf.eth0.accept_ra net.ipv6.conf.eth0.accept_ra_defrtr次に、広告が今も届いていることを確かめる。ICMPv6 のタイプバイトで絞れば十分で、近隣探索の通信の壁を読まずに済む。
tcpdump -ni eth0 'icmp6 and ip6[40] == 134'数分間なにも出なければ accept_ra は原因ではない。上流が広告をやめたか、経路上の何かが ICMPv6 を絞っているか、そのインターフェースが本当は想定のセグメントにいない。三つ目として、自分で広告を要求してフィールドを読む。
rdisc6 eth0rdisc6 は Debian と Ubuntu では ndisc6 パッケージに入っている。出力では、他の何かを変える前にルーター寿命と優先度を見る。ルーター寿命 0 の広告はデフォルトルーターを外せという指示で、どの accept_ra の値でも使えるルートにはならない。ブリッジモードの上流 CPE がプレフィックスだけ配って自分をルーターとして出さない場合も、これで見つかる。
アップリンクだけを許容インターフェースにする
上流がデフォルトルーターであるべきなら、転送中でもそこだけで広告を受け取り、他のどこでも受け取らない。
# /etc/sysctl.d/60-ipv6-router.conf
net.ipv6.conf.all.forwarding = 1
net.ipv6.conf.eth0.accept_ra = 2
net.ipv6.conf.eth0.accept_ra_defrtr = 1範囲を絞ることが重要なのは、Linux の IPv6 転送がリンク単位ではなく 一つのグローバルスイッチだからだ。net.ipv6.conf.wg0.forwarding に 1 を書いてから net.ipv6.conf.all.forwarding を読むと、グローバル値が反転し、すべてのインターフェースが転送を始める。トンネルではルーター、LAN では普通のホスト、という共存はできない。絞れるのは受け入れだけだ。accept_ra=2 をアップリンクに置き、トンネルと LAN のインターフェースは 1 か 0 のままにしておけば、トンネル越しに来た迷い込んだ広告が、そちらを指すデフォルトルートを入れることはできない。
一時的な sysctl -w は次の再起動で消え、systemd-networkd も NetworkManager も自前の設定からこの値を書き込む。networkd ではアップリンクの .network ファイルに IPv6Forwarding=yes と IPv6AcceptRA=yes を設定する。networkd が IPv6Forwarding をグローバルと記しているのも同じカーネル側の理由だ。NetworkManager ではアップリンクの ipv6.method=auto が accept_ra の値を左右するので、最初の確認を信じず nmcli connection up の後に sysctl を読み直す。
静的ルートという代替、ip -6 route add default via fe80::1 dev eth0 は確かに機能し、上流ルーターが安定した link-local アドレスを持つなら妥当な答えだ。何を手放すのかは明確にしておきたい。静的なデフォルトは更新も撤回もされないので、上流を入れ替えた後も死んだ隣接ホストを指し続ける。RA のルートは安全側に倒れ、静的ルートは静かに壊れる。
ゲートウェイ自身ではなく転送されたパケットを試す
ゲートウェイ自身の ping は、どのクライアントも使えないルートで成功しうるし、ゲートウェイが送信元だから成功することもある。接続済みのクライアントから試し、そのうえでゲートウェイにそのクライアントのアドレス宛てに何をするのか尋ねる。
# on the VPN client
ping -6 -c 3 2001:4860:4860::8888
# on the gateway, using a client address
ip -6 route get 2001:4860:4860::8888 from fd42:20::2そのあとルーター寿命一回分だけ待って、もう一度見る。望ましい定常状態は、proto ra と表示され expires の値がリセットされ続けるデフォルトと、all.forwarding=1 の組み合わせだ。expires が減る一方なら、広告はまだどこかで捨てられており、たいていの犯人は起動時の sysctl が走った後に accept_ra を書き換えるネットワークマネージャーだ。
締める前に四点を確認する。デフォルトルートが proto ra で更新され続けていること、accept_ra=2 がアップリンクにだけ効いていること、再起動後とマネージャー再起動後も転送が 1 であること、そして IPv6 の ping をゲートウェイではなくクライアントが完了できること。ルートが健全なのにクライアントが失敗するなら、次の容疑者は戻り経路か上流のファイアウォールであって、広告ではない。