rp_filter が静的ルーティング下の traceroute に及ぼす影響

静的ルーティングを設定した WindowsLinux (RHEL6.4) で、それぞれ tracert と traceroute を実行したとき不思議な差が出た。解明までのプロセスが自分の好みにドンピシャだったので順を追って書く。

環境

環境を大まかに書くと下図のようになる。外部との通信用のデフォルトゲートウェイと、内部のシステムである targethost と通信するための内部ネットワークに接続するゲートウェイ A がある。Windows Server 2012 のサーバと RHEL 6.4 のサーバそれぞれに、targethost 向けの静的ルートを入れ、ゲートウェイにはゲートウェイ A を設定した。

設定が終わったので、「さて、正しいルートを通っているかな♪」と tracert と traceroute を叩く。すると違いが。

Windows Server 2012 での実行結果は希望通りのものだ。何の問題もない。

> tracert -d t.t.t.t
Tracing route to t.t.t.t over a maximum of 30 hops

  1    1 ms   <1 ms   <1 ms  a.a.a.a
  2   27 ms   23 ms   29 ms  x.x.x.x
  3   10 ms   17 ms    9 ms  y.y.y.y
  4   11 ms   11 ms   12 ms  z.z.z.z
  5   18 ms   12 ms   10 ms  t.t.t.t

Trace complete.

RHEL6.4 での実行結果は最初と最後以外が * (応答無しを示すアスタリスク) になってしまう。

# traceroute -n t.t.t.t
traceroute to t.t.t.t (t.t.t.t), 30 hops max, 60 byte packets
 1  a.a.a.a   0.658 ms   1.111 ms   1.558 ms
 2  * * *
 3  * * *
 4  * * *
 5  t.t.t.t  17.492 ms  18.005 ms  18.002 ms

同じネットワーク設定で、同じ静的ルーティングを設定しているのに、tracert と traceroute の結果には差がある。応答があるため、targethost と通信はできているが、経路が正しいかわからない(実際にはゲートウェイ指定までがサーバの責任ではあるのだが)。

最初の着眼点: tracert と traceroute の違い

まずこの差を見た時に思い出された古い経験があった。tracert と traceroute は機能は同じでも通信に使うプロトコルが違うのだ。

Windows の tracert は ICMP を使う。traceroute は TCP, UDP, ICMP を使えるが、デフォルトでは UDP のパケットを送出する。Windows Server 2012 と RHEL6.4 のサーバで違いが出た原因はこれだろうと推測。traceroute に ICMP を使わせればよいと考えて -I オプションをつけて実行。

状況変わらず。tracert と traceroute のプロトコルの違いが原因ではなかったことになる。

# traceroute -I -n t.t.t.t
traceroute to t.t.t.t (t.t.t.t), 30 hops max, 60 byte packets
 1  a.a.a.a   0.824 ms   1.276 ms   1.739 ms
 2  * * *
 3  * * *
 4  * * *
 5  t.t.t.t  23.943 ms  25.183 ms  25.400 ms

次の着眼点: タイムアウト

traceroute の man を眺めて、traceroute の原理を思い返して、次に試したのがタイムアウトのオプション指定だった。ルーターからの Time Exceeded の通知が遅いのかと思い -w 60 オプションをつけて実行。

60秒待たされたが状況変わらず。冷静に考えれば Windows Server 2012 側のレスポンス速度はミリ秒オーダーであり、デフォルトの5秒を超えるとは考え難かった。

# traceroute -I -n -w 60 t.t.t.t
traceroute to t.t.t.t (t.t.t.t), 30 hops max, 60 byte packets
 1  a.a.a.a   9.642 ms   9.684 ms   9.576 ms
 2  * * *
 3  * * *
 4  * * *
 5  t.t.t.t  20.978 ms  21.206 ms  20.643 ms

急展開の発見: ルーターからの Time Exceeded はサーバーまで返っている!

ルーターからの応答が本当に無いのか、それとも Time Exceeded ではない何かなど、traceroute の想定していない応答になってしまっているのか、それを確認したくて tcpdump を実行してみた。するとなんと、全てのルーターからちゃんと ICMP Time Exceeded が返って来ているではないか。

tcpdump の見やすさのため、以下のオプションを指定して実行した。

  • -q 1: 各TTLごとに1パケットずつ(通常は3パケット)
  • -z 1: 1秒に1パケットずつ(通常は16パケット同時)
# traceroute -I -n -q 1 -z 1 t.t.t.t
traceroute to t.t.t.t (t.t.t.t), 30 hops max, 60 byte packets
 1  a.a.a.a   1.963 ms
 2  *
 3  *
 4  *
 5  t.t.t.t  10.490 ms

別のターミナルで実行しておいた tcpdump の結果はこうなる。見事に ICMP time exceeded が確認できる。

# tcpdump -n -i eth1 icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), capture size 65535 bytes

06:14:41.565256 IP rhel6.4 > t.t.t.t: ICMP echo request, id 15728, seq 1, length 40
06:14:41.566080 IP a.a.a.a > rhel6.4: ICMP time exceeded in-transit, length 68

06:14:42.564260 IP rhel6.4 > t.t.t.t: ICMP echo request, id 15728, seq 2, length 40
06:14:42.598026 IP x.x.x.x > rhel6.4: ICMP time exceeded in-transit, length 36

06:14:43.565176 IP rhel6.4 > t.t.t.t: ICMP echo request, id 15728, seq 3, length 40
06:14:43.573244 IP y.y.y.y > rhel6.4: ICMP time exceeded in-transit, length 36

06:14:44.565501 IP rhel6.4 > t.t.t.t: ICMP echo request, id 15728, seq 4, length 40
06:14:44.579159 IP z.z.z.z > rhel6.4: ICMP time exceeded in-transit, length 76

06:14:45.566333 IP rhel6.4 > t.t.t.t: ICMP echo request, id 15728, seq 5, length 40
06:14:45.576810 IP t.t.t.t > rhel6.4: ICMP echo reply, id 15728, seq 5, length 40

サーバまで ICMP Time Exceeded が返っているとなると、それが traceroute には伝わっていないことになる。そんな芸当ができるのはカーネルファイアウォールぐらいしかない。しかし、iptables は利用していないため、ファイアウォール要因ではない・・・

記憶のリンク: rp_filter カーネルパラメータ

そこまで推測が進んだ時にあるカーネルパラメータの存在が(大袈裟に言えば電撃的に)思い出された。「rp_filter」だ。この rp_filter は Oracle RAC を組む為の Oracle Grid Infrastructure のインストレーション・ガイドに登場するカーネルパラメータで、「よくわからんけど経路云々でパケットをドロップするかもしれない危険なものだから(RACの相互接続はプライベードで閉じているネットワークなので)無効化すべき」程度には認識していた。

2.7.9 複数のプライベート・インターコネクトとEnterprise Linux

カーネル2.6.31以上(Oracle Unbreakable Enterprise Kernel 2.6.32を含む)では、戻り経路フィルタのバグが修正されています。この修正の結果、プライベート・インターコネクトに複数のNICを使用するOracle RACシステムでは、現在、rp_filterパラメータに固有の設定が必要です。この要件は、Linuxカーネル2.6.32以上を実行しているすべてのExadataシステムにも適用されます。rp_filterパラメータにこれらの設定を行わないと、インターコネクト・パケットが遮断または破棄される可能性があります。

rp_filterの値で、戻り経路フィルタがフィルタなし(0)、厳密なフィルタ(1)または緩いフィルタ(2)に設定されます。プライベート・インターコネクトの場合は、rp_filterの値を0または2に設定します。プライベート・インターコネクトNICを1に設定すると、プライベート・インターコネクトで接続の問題が発生する可能性があります。プライベート・インターコネクトは、分離されたプライベートのネットワーク上にあるはずなので、このフィルタを無効または解放することは危険だとは考えられていません。

たとえば、eth1およびeth2がプライベート・インターコネクトNICで、eth0がパブリック・ネットワークNICの場合、/etc/sysctl.confで次のエントリを使用して、プライベート・アドレスのrp_filterを2(緩いフィルタ)に設定し、パブリック・アドレスを1(厳密なフィルタ)に設定します。

net.ipv4.conf.eth2.rp_filter = 2
net.ipv4.conf.eth1.rp_filter = 2
net.ipv4.conf.eth0.rp_filter = 1
Enterprise Linux 5.6(Enterprise Linux 5 Update 6)にはinitscripts-8.45.33-1.0.4.el5.i386.rpmを使用した修正が含まれ、これによって、カーネル・パラメータnet.ipv4.conf.default.rp_filterが2(解放モード)に設定されます。そのため、Unbreakable Linux KernelをEnterprise Linux 5.6の最上位に適用した後、すべてのNICのrp_filter値が2に設定されているため、手動での変更が不要になる場合があります。パブリック・ネットワークでより厳密な戻り経路フィルタが必要な場合は、パブリックNICのrp_filterを1に設定します。

犯人はお前だ!: rp_filter カーネルパラメータ

パケットをドロップする可能性があるということで真面目に rp_filter を調べてみる。以下の説明が一番わかりやすい。

Linux Advanced Routing & Traffic Control HOWTO 13.1. 戻り経路フィルタ (Reverse Path Filtering)

デフォルトでは、ルータはすべてをルーティングします。 パケットが「明らかに」自分のネットワークには属していなくてもです。 よくある例は、プライベートの IP 空間がインターネットに漏れてしまう問題です。 195.96.96.0/24 に向かう経路があるインターフェースに対しては、 212.64.94.1 から発したパケットは、本来到着しないはずです。

ほとんどの人はこの機能を無効にしたいと思うはずですから、 カーネルハッカー達はこれを簡単できるようにしてくれました。 /proc 以下にあるファイルを使うと、カーネルに対してこの指示ができます。 この方法は戻り経路フィルタ (Reverse Path Filtering) と呼ばれています。基本的には、あるパケットに対する返信が、 そのパケットの入ってきたインターフェースに向かわない場合、 このパケットはインチキだとみなされて無視されることになります。

tcpdump の結果を見ているから、ルータ X, Y, Z からの ICMP Time Exceeded パケットのソースアドレスがそれぞれ x.x.x.x, y.y.y.y, z.z.z.z であることは明らかである。そして、targethost (t.t.t.t) への静的ルーティングは定義しているが、x.x.x.x, y.y.y.y, z.z.z.z にもそれらが所属するネットワーク向けにも静的ルーティングは定義していない。つまり、仮に x.x.x.x, y.y.y.y, z.z.z.z に返信しようとしても、それらが入って来たインターフェースは使われず、デフォルトゲートウェイに向いているインターフェースを使用する。まさに rp_filter が「インチキ」と判定するパケットだ。

targethost (t.t.t.t) との通信に使うインターフェース(eth1)の rp_filter を無効(0) にして traceroute を実行する。

# cat /proc/sys/net/ipv4/conf/eth1/rp_filter 
1
# echo 0 > /proc/sys/net/ipv4/conf/eth1/rp_filter 
# cat /proc/sys/net/ipv4/conf/eth1/rp_filter 
0

# traceroute -I -n t.t.t.t
traceroute to t.t.t.t (t.t.t.t), 30 hops max, 60 byte packets
 1  a.a.a.a   0.658 ms   1.111 ms   1.558 ms
 2  x.x.x.x  26.490 ms  27.348 ms  28.381 ms
 3  y.y.y.y  19.065 ms  19.799 ms  20.006 ms
 4  z.z.z.z  21.561 ms  21.791 ms  21.974 ms
 5  t.t.t.t  17.492 ms  18.005 ms  18.002 ms

キタ━━━━(゚∀゚)━━━━!!


これでこれまでの事象の説明がつくことになる。

Windows Server 2012

Windows には rp_filter のような動作はないのだろう。

RHEL 6.4

RHEL 6 ではデフォルトで rp_filter = 1 であり、ルーティング情報上、送信に使わないインターフェースから入って来た「インチキ」パケットを破棄する。

rp_filter = 0 にすることでその動作は無効になる。

まとめ

覚えておくべきことは以下の通りと思う。

  • Windows の tracert と Linux の traceroute はデフォルトで使用するプロトコルが異なる。
    • tracert は ICMP を使う。
    • traceroute は UDP を使う。
    • traceroute で ICMP を使うには -I オプションを使用する。
  • RHEL 6 (7も) では静的ルーティングしているホスト・ネットワークに対する traceroute では途中のホップが無応答(*)表示になる。
    • 原因は rp_filter カーネルパラメータにある。
    • 対応方法は以下の案がある。
      • 一時的に使用するインターフェース(ethN)の rp_filter カーネルパラメータを 0 にする。
      • tcpdump -i ethN icmp」 + 「traceroute -I -n -q 1 -z 1」で生パケットから途中のホップの返信を確認する。
      • 途中経路のネットワークも静的ルーティングに加える(途中経路をサーバが知る必要はないので設計上おかしいが1つの技術観点上の方法として)。

今ある知識を動員してあがきまくっているうちに、単なる情報として記憶されていたもの(今回だと「rp_filter」)が呼び覚まされて急激に理解、利用されていくというプロセスは個人的にとても心地よい。水溶液に解けていた物質がなんらかのショックにより急激に結晶化するような。