アップリンクが二つあり、ip -6 route show はどちらでも同じネクストホップを表示します: default via fe80::1 dev uplink0 と default via fe80::1 dev uplink1 です。これは重複アドレスの競合ではなく、裸のネクストホップ fe80::1 をポリシーテーブルへコピーしても、どちらのリンクを意図したのかカーネルには伝わりません。本当の問いは症状より狭いものです: 二番目のテーブルに必要なのは片方のリンクか両方か、そして再起動後もそれらのルートを生かしておくのは何か。順に確認する三つの条件で、大半は決まります。

同じ fe80::1 は二つの別のネイバー

リンクローカルアドレスは一つのリンクに限定されます。したがって fe80::1 は同じアドレスが二回印字されたものではなく、たまたま同じ接尾辞を共有する二つのネイバーです。ゾーンインデックスはホストがどちらを指すかを示す方法なので、fe80::1%uplink0 と fe80::1%uplink1 はカーネルにとって別の宛先であり、その接尾辞が遠隔ルーターまで運ばれることはありません。RFC 4007 は背後のゾーンモデルを説明しており、ポリシーテーブルがすべてのリンクローカルネクストホップにデバイスを伴わせなければならない理由でもあります。

あるホストの数値ゾーンIDをテンプレートへコピーするVPN設定ジェネレーターは、ローカルな文脈をアドレスの一部であるかのように扱っており、それが書き出すファイルはインターフェース名やインデックスが異なるあらゆるマシンで誤りです。ポリシーテーブルも二つのリンクを一つに統合しません: ルーティングされたVPNはネットワーク間でパケットを受け渡しますが、遠隔のEthernetネイバーのリンクローカルな同一性をトンネル越しに運ぶことはないため、リンクローカル通信は自分のリンクにとどまります。

デバイスを外すとルートは書けなくなります。ip -6 route add default via fe80::1 table 200 はゲートウェイを解決できず、iproute2 は Error: Nexthop has invalid gateway. で拒否しますが、デバイスがネクストホップを二つのネイバーの一方に固定するため、dev uplink0 を加えるとコマンドは成功します。デバイスが教えてくれないのは、uplink0 がこのテーブルに正しいリンクかどうかであり、別のホストからコピーしたルートはそのホストのインターフェース名を連れてくるので、本当の間違いはそこから生まれます。

条件一: テーブルにリンクを一つ入れるか二つ入れるか

二番目のアップリンクが何のためかを起点にします。両方のリンクが同じ通信を運び、一方が待機系なら、代替テーブルには両方のゲートウェイを異なるメトリックで入れるべきです: 同じ宛先、優先する経路が一つ、明らかな第二候補が一つ。アップリンクが、異なる通信を異なるプロバイダーから出すため、あるいは一部の通信がVPNを迂回し残りが内側にとどまるスプリットトンネルのために存在するなら、二つのリンクはそもそも一つのテーブルに入るべきではありません。それらはルールで選ばれる二つのテーブルに入るべきで、ルールが選ぶのはテーブルでありトンネルではありません: 迂回する通信はルーティングの前にマークされ、そのマークがそれを二番目のテーブルへ送ります。

その二番目の場合、テーブルをいつ参照するかを決めるのはルールであり、参照後にパケットがどこへ行くかを決めるのはルートです:

ip -6 rule add fwmark 0x2 table 201
ip -6 route add default via fe80::1 dev uplink1 metric 100 table 201
ip -6 route add 2001:db8:100::/48 dev uplink1 metric 100 table 201
ip -6 route show table 201

どちらのオブジェクトも他方を代替せず、デフォルトだけを持つテーブルでもゲートウェイの解決に失敗することがあります。追加が拒否されたり、ルートはあるが使えない場合は、そのインターフェースの接続済みプレフィックスを同じテーブルに入れるか、そのプレフィックスが別の場所でルーティングされているときはホップを onlink として指定します。上のブロックでは、2001:db8:100::/48 は uplink1 上のその接続済みプレフィックスを表し、任意の宛先を表すものではありません。fwmark ルールは、ほかの何かが設定しなければならないマークだけを消費します。転送経路上の nftables や iptables のルールか、ソケット上の SO_MARK から設定され、マークの発生源なしでコピーしたブロックは何にも一致しません。

条件二: メトリックは死んだプロバイダーを検出しない

メトリックは存在するルート間の優先順位であり、ヘルスチェックではありません。キャリアが落ちると、カーネルはそのデバイスのルートを削除し、二番目のデフォルトが引き継ぎます。ケーブルが上がったままで背後のプロバイダーが死ぬと、テーブルでは何も変わりません: 優先ルートはインストールされ、ネイバーはレイヤー2で到達可能で、パケットはブラックホールへ出ていきます。その区別があなたの構成で重要なら、フェイルオーバーは経路を検査してルートを削除する何かから来なければなりません。それがルーティングデーモンでも、エントリを削除して再インストールする小さなスクリプトでも同じです。

テーブルを構築した後に選択を検証し、カーネルが見るのと同じ方法で検証します。ip -6 route get 2001:db8:100::1 は使用されるルートを出力し、デバイスと選択された送信元アドレスを含み、ポリシールールを尊重します。誤ったアップリンクの送信元アドレスは、ルールが予想より早く一致した通常の兆候です。

条件三: 永続性の所有者を決める

ip -6 route add で追加したルートは、再起動まで、または何かがフラッシュするまで残ります。ルーター広告から学習したデフォルトは有効期間を持ち、ルーターが取り下げることができますが、手動で追加したデフォルトは自分では期限切れになりません。二つを一つのテーブルで混ぜると、古い永続ルートが新しく広告されたルートより上に残り続け、それが示していたアップリンクが存在しなくなって久しくてもその状態が続きます。

テーブルごとに所有者を一つ選びます。ネットワークマネージャーやルーティングデーモンがすでにインターフェースを管理しているなら、ポリシールートをそこで記述し、カーネルには触れないでください。スクリプトで行うなら、スクリプトを冪等にし、同時に撤去手順も書いてください。最初の失敗した変更のときに必ず必要になります:

ip -6 route flush table 201
ip -6 rule del fwmark 0x2 table 201

他のホストのために転送するルーターでは、テーブルを責める前に net.ipv6.conf.all.forwarding を確認してください。転送がオフなら、別のネットワーク宛てのパケットは、テーブル 201 がどれほど正しくても破棄されます。

次の人が監査できるように設計を記録する

fe80::1 だけを保存する棚卸しは、重要なフィールドを捨ててしまいます。宛先、ネクストホップ、デバイス、テーブル、メトリックを一緒に記録し、さらに各ルートの出所が広告か、マネージャー生成か、静的かを記録します。後で誰かが二番目のアップリンクが実際に通信を運んでいるかと尋ねたとき、その記録は推測なしに答えます。

テーブルを完成と宣言する前に、ip -6 neigh show dev uplink0 でネイバーを確認し、順序の事故がないか ip -6 rule show を読み直し、デフォルトルートを取る ping ではなくルールに一致する通信でテストします。それから意図的に片方のアップリンクを落とし、カーネルがどのルートにフォールバックするかを見てください。答えは書き留めたものと一致するはずです。

関連記事