実際の課題:マルチホーム構成におけるLinuxのジレンマ
Linuxサーバーに2つ目のネットワークインターフェースを接続すると、直ちにルーティングの破綻を引き起こすことがよくあります。Ubuntu 22.04を実行するデュアルホーム構成のデータベースノードを例に考えてみましょう。eth0は192.168.1.0/24の内部管理用VLANに接続され、eth1は10.0.0.0/24の専用1 Gbpsアップリンク経由で一般ユーザー向けのトラフィックを処理しています。
両方のリンクランプは正常に点灯し、各インターフェースは有効なIPアドレスを取得しています。外向きのcurlリクエストも問題なく動作します。しかし、外部からのトラフィックがeth1に到達した途端、クライアントの接続は初期ハンドシェイクで無限にハングしてしまいます。パブリックサブネットからのPingリクエストはパケットが100%ドロップし、Request timeoutエラーが無言で返されます。システムのデフォルトゲートウェイをeth1に切り替えると、今度は問題が逆転します。eth1は即座に応答するようになるものの、eth0経由のSSHアクセスが完全に失われてしまうのです。
これこそが、典型的な「非対称ルーティング(Asymmetric Routing)」の挙動です。
原因分析:標準ルーティングが破綻する理由
Linuxはデフォルトで、mainという名前の宛先ベースの単一ルーティングテーブルを使用します。カーネルが送信パケットを準備する際、評価する属性はただ1つ、宛先IPアドレスのみです。元のフレームがどのインターフェースで受信されたかは完全に無視されます。
本番環境でこの処理がどのように破綻するか、着信TCPハンドシェイクの流れを追ってみましょう。
- クライアント(
203.0.113.50)がeth1のパブリックIP(10.0.0.50:443)宛てにTCP SYNパケットを送信します。 - ローカルアプリケーションがSYNを受信し、送信元
10.0.0.50、宛先203.0.113.50のSYN-ACK応答をトリガーします。 - カーネルは
203.0.113.50についてルーティングテーブルmainを照会します。ホスト固有のルートが見つからないため、eth0に紐づけられたデフォルトゲートウェイ(0.0.0.0/0)を選択します。その結果、SYN-ACKはeth1ではなくeth0から送出されます。 - 上流のファイアウォールがこのフレームを即座に破棄します。パケットは
eth1サブネットに属する送信元IPを持っているにもかかわらず、eth0の上流経路から出てくるため、リバースパスフィルタリング(RPF: Reverse Path Filtering)およびステートフル接続追跡(conntrack)の検証に失敗するためです。
宛先のみに基づくルーティングでは、カーネルは受信時のコンテキストを認識できません。送信パケットには、元のトラフィックがどこから入ってきたかを記憶する仕組みが必要なのです。
対症療法の検証:簡易的な解決策が不十分な理由
本格的なポリシーベースルーティングを導入する前に、エンジニアは通常、次の2つの対症療法を試しがちです。
1. 送信元NAT(SNAT)またはマスカレード
iptables -t nat -A PREROUTINGを用いて着信パケットを書き換えることで、応答パケットがローカルインターフェースに戻るように強制します。
- メリット:上流ルーターの設定を一切変更することなく動作します。
- デメリット:NginxやEnvoyのアクセスログでクライアントの実際のIPアドレスが隠蔽されます。また、高トラフィック時(50,000以上の同時フローなど)にconntrackテーブルのオーバーヘッドが増大します。
2. リバースパスフィルタリング(RPF)の無効化
net.ipv4.conf.all.rp_filter=0を設定することで、カーネル自身が非対称な応答を破棄するのを防ぎます。
- メリット:
sysctlの1行の変更だけで済みます。 - デメリット:ルーティングの欠陥自体は解消されません。応答パケットは依然として誤った物理回線から送出されるため、上流のISPやクラウドのエッジゲートウェイ側で結局破棄されてしまいます。
3. ポリシーベースルーティング(PBR)
ip ruleを介してルーティングポリシーデータベース(RPDB)を使用し、送信元IPやfwmarkに基づいて送信ルートをマッチングさせます。
- メリット:クライアントIPの可視性を維持し、接続追跡テーブルの飽和を防ぎ、物理NIC間における送受信経路の完全な対称性を保証します。
- デメリット:標準のDHCP設定の範囲を超えて、永続的なルーティングテーブルやサブネットルートを明示的に定義する必要があります。
PBRの設定手順
ポリシーベースルーティングは、カーネルレベルでこの問題を根本から解決します。2つの独立したルーティングテーブルを作成し、各テーブルをそれぞれの送信元IPにバインドします。
検証用ネットワーク構成
- プライマリ アップリンク(eth0):IP
192.168.1.50/24、ゲートウェイ192.168.1.1 - セカンダリ アップリンク(eth1):IP
10.0.0.50/24、ゲートウェイ10.0.0.1
ステップ 1: rt_tablesへのテーブル登録
Linuxでは、1から252までのインデックスでカスタムルーティングテーブルを登録できます。/etc/iproute2/rt_tables内で、数値の識別子をわかりやすいテーブル名に対応付けます。sudo権限を正しく適用させるため、単純なリダイレクトではなくtee -aを使用します。
echo "100 isp_eth0" | sudo tee -a /etc/iproute2/rt_tables
echo "200 isp_eth1" | sudo tee -a /etc/iproute2/rt_tables
ステップ 2: 各テーブルへのルート割り当て
各ルーティングテーブルには、直接接続されているサブネットと、上流のデフォルトゲートウェイという2つの経路が必要です。
# ルーティングテーブル isp_eth0(テーブル100)の設定
sudo ip route add 192.168.1.0/24 dev eth0 src 192.168.1.50 table isp_eth0
sudo ip route add default via 192.168.1.1 dev eth0 table isp_eth0
# ルーティングテーブル isp_eth1(テーブル200)の設定
sudo ip route add 10.0.0.0/24 dev eth1 src 10.0.0.50 table isp_eth1
sudo ip route add default via 10.0.0.1 dev eth1 table isp_eth1
ステップ 3: RPDBにおけるポリシールールの定義
送信パケットの送信元アドレスが10.0.0.50である場合、カーネルにテーブルisp_eth1を参照するよう指示します。評価順序を確定的なものにするため、明示的な優先度(priority)を設定します。
sudo ip rule add from 192.168.1.50/32 table isp_eth0 priority 1000
sudo ip rule add from 10.0.0.50/32 table isp_eth1 priority 1010
デーモンの再起動なしに変更を即時反映させるため、FIBルートキャッシュをフラッシュします。
sudo ip route flush cache
ステップ 4: パケット転送経路の検証
ip rule showを実行して、有効なルールの優先度を確認します。
ip rule show
出力結果には、優先度インデックス順にソートされた検索条件が一覧表示されます。
0: from all lookup local
1000: from 192.168.1.50 lookup isp_eth0
1010: from 10.0.0.50 lookup isp_eth1
32766: from all lookup main
32767: from all lookup default
ip route getを使用して、カーネルが外部宛先をどのように解決するかを確認します。
ip route get 8.8.8.8 from 10.0.0.50
パケットがテーブルisp_eth1を経由してルーティングされ、eth1から送出されていることが確認できます。
8.8.8.8 from 10.0.0.50 via 10.0.0.1 dev eth1 table isp_eth1 uid 0
cache
別のターミナルでtcpdump -nn -i eth1 icmpを実行した状態でping -I eth1 8.8.8.8を実行します。Echo Request(エコー要求)とEcho Reply(エコー応答)が、まったく同じ物理インターフェースを通過するようになったことが確認できます。
ステップ 5: Netplanによる設定の永続化
ip ruleコマンドで実行した設定は、システム再起動後に消失します。systemd-networkdを使用するUbuntuおよびDebianシステムでは、/etc/netplan/01-netcfg.yamlに設定を保存して永続化します。
network:
version: 2
renderer: networkd
ethernets:
eth0:
addresses:
- 192.168.1.50/24
routes:
- to: 0.0.0.0/0
via: 192.168.1.1
table: 100
routing-policy:
- from: 192.168.1.50/32
table: 100
priority: 1000
eth1:
addresses:
- 10.0.0.50/24
routes:
- to: 0.0.0.0/0
via: 10.0.0.1
table: 200
routing-policy:
- from: 10.0.0.50/32
table: 200
priority: 1010
構文を検証し、ルールを適用します。
sudo netplan try
sudo netplan apply
これで、サーバーは両方のインターフェースで同時に着信するトラフィックを正常に処理できるようになりました。パケットは侵入元とまったく同じインターフェースおよびゲートウェイを経由して返送されるため、NATやステートフルなパケット改変に依存することなく、パケットドロップを解消できます。

