RPF(Reverse Path Forwarding)の概要と動作
RPF(Reverse Path Forwarding)の概要と動作
概要
RPF (Reverse Path Forwarding) には、ユニキャストとマルチキャストの2種類があります。マルチキャストを対象とする mRPF (Multicast Reverse Path Forwarding) は、PIM-SSM (Protocol Independent Multicast - Source-Specific Multicast) や MSDP (Multicast Source Discovery Protocol) とともに、マルチキャスト配信のループを防止するために利用されます。一方で、今回はユニキャストの RPF について取り上げます。
uRPF (Unicast Reverse Path Forwarding) は、Linux やネットワーク機器で実装されており、RFC 3704(Cisco などによって提唱)で定義されています。uRPF は、特にマルチホーム環境において送信元アドレスの正当性を確認するための手法として使用されます。また、DoS 攻撃やネットワークスキャンを行う攻撃者が IP アドレススプーフィングを使って身元を隠すことが一般的ですが、uRPF を使用することで、送信元アドレスがルーティングテーブルに基づいた適切なインターフェースからのものでない場合にパケットを破棄することができ、スプーフィング攻撃から一定の保護を提供します。このため、uRPF は特にエンドユーザーの端末よりも、ISP 内のルータにおいてスプーフィング対策として有効です。
セキュリティ対策にはさまざまな方法があり、スプーフィング対策もその一部です。今回のスコープ外ですが、例えば Martian アドレス(0.0.0.0/8、10.0.0.0/8、127.0.0.0/8、172.16.0.0/12、192.168.0.0/16、224.0.0.0/4、240.0.0.0/4 など)や Bogon(TestNet、Private、Multicast、Link-local など)といった、有効なホストアドレスとはみなされない範囲のアドレスを ACL (Access Control List) で一律に破棄するという方法も有効です。
動作原理と動作モード
動作原理
uRPFは、自分ノード内部のRIB(Routing Information Base, RIB)もしくはFIB(Forwarding Information Base, FIB)を利用して送信元のIPアドレスをチェックします。その判断は下記のような考えに基づきます。
- ルートが存在しているならば、おそらく有効なホストから届いたものであろう。
- Inboudのインターフェースから到達できない、もしくはホストから到達できない送信元アドレスをセットされているものは、設定ミスか悪意のあるパケットである。
このようにパケットを破棄することにより、不当にネットワーク帯域のリソースを食い潰す無効なパケットを制限できます。一見すると大変有効な機能のように思われますが、適用する際にはルーチングの非対称性になる可能性や、適用する対象のルータやホストのパフォーマンスを考慮する必要があります。
動作モード
RPFの動作モードについてRFCには複数の記載がありますが、実装の非効率性や用途などからネットワーク機器などで主に使われているのは「Strictモード」と「Looseモード」の2つのモードがあります。
Mode | 動作 |
---|---|
Strict | 受信パケットが送信元IPアドレスに対して最適なインターフェースから到達しているかを確認します。(最適ルートと異なるインターフェースから受信した場合は破棄) |
Loose | 受信パケットの送信元IPアドレスがルーティングテーブル内で有効な経路に一致するかどうかを確認します。(経路が存在すればパケットは許可) |
対象の機器が複数のインターフェースを持ち、複数の経路からBGPルートを取得している場合、経路の非対称性が発生する可能性があります。通常は経路が対称であっても、障害や接続先の構成変更により、予期せず非対称な経路になることが考えられます。このような状況では、Strict RPF よりも Loose RPF を選択する方が適切です。
また、ルーティングテーブルにデフォルトルートが存在する場合、RPFが期待通りに動作しない可能性があります。この場合、RPFの有効性を再評価し、必要に応じてRPFの使用を検討することが重要です。
LINUXでの設定方法
LinuxでRPFを設定するには、カーネルパラメータの rp_filter
を使用するか、iptablesの rpfilter
モジュールを使用します。
カーネル設定 (net.ipv4.conf.all.rp_filter
)
-
net.ipv4.conf.<interface>.rp_filter
パラメータを設定することで、各インターフェースに対してRPFを有効にできます。インターフェース個別に設定するか、全体のインターフェースを設定できます。設定では記載がありませんが、0
に上書きすることで動作自体を無効にできます。# Strictモードを有効化 $ sysctl -w net.ipv4.conf.all.rp_filter=1 $ sysctl -w net.ipv4.conf.eth0.rp_filter=1 # Looseモードを有効化 $ sysctl -w net.ipv4.conf.all.rp_filter=2 $ sysctl -w net.ipv4.conf.eth0.rp_filter=2
Ubuntuではデフォルトの状態でLooseモードで設定されているようです。
$ sudo sysctl -a | grep -e '\.rp_filter' net.ipv4.conf.all.rp_filter = 2 net.ipv4.conf.default.rp_filter = 2 net.ipv4.conf.ens3.rp_filter = 2
Iptables設定 (rpfilter
モジュール)
-
iptables
の-t raw
テーブルにrpfilter
拡張モジュールを使用することで、RPFを実行できます。Kernelの場合と同様に、--loose オプションを使用してルーズモードに切り替えることもできます。$ iptables -t raw -A PREROUTING -m rpfilter --validmark --invert -j DROP
Tip:カーネル設定とIptablesの違い
- カーネル設定の
rp_filter
は、インターフェース単位で有効/無効を設定します。一方、iptables
のrpfilter
モジュールは、ルール単位でより細かい制御が可能です。例えば、一致した場合にログを取得して破棄する、不一致の場合に別の処理にジャンプさせるなど柔軟な制御が可能となります。詳しくはman iptables-extensions(8) などを参照ください。また、もう一つの違いとしてカーネルのrp_filter
はIPsecで保護されたパケットを特別扱いしますが、iptables
のrpfilter
モジュールは特別扱いしません。これは、IPsecのTunnelモードの場合などに発生することですがiptablesの場合には特別扱いをせず、到着したIPsecパケットはxfrmカーネルサブモジュールに入った後に、インナーのIPアドレスに基づいてもう一度IPスタックを通過するためこのような扱いの違いがあるものと考えられます。
ネットワーク機器での設定方法
一般的なネットワーク機器でも同様に設定できますが、ここではCiscoを取り上げます。
通常はNW機器ではRPFはデフォルトでは無効になります、一部のセキュリティ製品においては有効になっていることがあります。また、デフォルト無効である機器に対して有効にする場合には、RPF動作を有効にすることでFIBの使用制限やパフォーマンスへの影響(ハードウェア処理ではないなど)があるかは十分に調査検討する必要があります。一般的にネットワーク機器は大量のパケットをハードで処理していますので、機能の実装によってはCPUにPUNTしてしまい、転送自体が停止する可能性もありますので本番環境で有効にする場合には確認が必要です。
# Strictモードの設定
interface GigabitEthernet1
ip verify unicast source reachable-via rx
# Looseモードの設定
interface GigabitEthernet1
ip verify unicast source reachable-via any
RPF(Reverse Path Forwarding)の挙動を確認する
Linuxのネットワークネームスペース(Network Namespaces)を利用して、1つのホストの中でRPF(Reverse Path Forwarding)の挙動を確認します。特に、RPFが異なるモード(Strict、Loose、Off)でどのように動作するかを観察して理解を深めることを目的とします。
環境構築
+-----------------+ +-----------------+
| Namespace | | Namespace |
| NS1 | | NS2 |
| | | |
| Lo: 1.1.1.1/32 | | Lo: 2.2.2.2/32 |
| veth0 | Route1 | veth1 |
| 192.168.100.1 +------------------+ 192.168.100.2 |
| | | |
| veth2 | Route2 | veth3 |
| 192.168.200.1 +------------------+ 192.168.200.2 |
+-----------------+ +-----------------+
-
ネットワークネームスペースの作成とVETHペアの設定
-
NS1
とNS2
の2つのネットワークネームスペースを作成します。 -
NS1
にはveth0
とveth2
を、NS2
にはveth1
とveth3
をそれぞれ接続します。
# ネームスペースの作成 $ sudo ip netns add NS1 $ sudo ip netns add NS2 # VETHペアの作成 $ sudo ip link add veth0 type veth peer name veth1 $ sudo ip link add veth2 type veth peer name veth3 # VETHをそれぞれのネームスペースに割り当て $ sudo ip link set veth0 netns NS1 $ sudo ip link set veth1 netns NS2 $ sudo ip link set veth2 netns NS1 $ sudo ip link set veth3 netns NS2 $ sudo ip -all netns exec ip -br link show netns: NS2 lo DOWN 00:00:00:00:00:00 <LOOPBACK> veth1@if6 DOWN 92:8d:2d:c5:ed:00 <BROADCAST,MULTICAST> veth3@if8 DOWN 92:52:f9:bd:a1:46 <BROADCAST,MULTICAST> netns: NS1 lo DOWN 00:00:00:00:00:00 <LOOPBACK> veth0@if5 DOWN 1e:4b:7b:d8:84:10 <BROADCAST,MULTICAST> veth2@if7 DOWN 4e:74:36:77:3e:ce <BROADCAST,MULTICAST>
-
-
インターフェースの設定とIPアドレスの割り当て
次に、各インターフェースにIPアドレスを割り当てます。-
veth0
とveth1
は、192.168.100.0/24 のサブネットに属し、それぞれ192.168.100.1
と192.168.100.2
をアサインします(Route1) -
veth2
とveth3
は、192.168.200.0/24 のサブネットに属し、それぞれ192.168.200.1
と192.168.200.2
をアサインします(Route2) -
NS1
に1.1.1.1/32
のループバックアドレスを設定。 -
NS2
に2.2.2.2/32
のループバックアドレスを設定。
# NS1の設定 $ sudo ip netns exec NS1 ip addr add 192.168.100.1/24 dev veth0 $ sudo ip netns exec NS1 ip addr add 192.168.200.1/24 dev veth2 $ sudo ip netns exec NS1 ip link set veth0 up $ sudo ip netns exec NS1 ip link set veth2 up $ sudo ip netns exec NS1 ip link set lo up $ sudo ip netns exec NS1 ip addr add 1.1.1.1/32 dev lo # NS2の設定 $ sudo ip netns exec NS2 ip addr add 192.168.100.2/24 dev veth1 $ sudo ip netns exec NS2 ip addr add 192.168.200.2/24 dev veth3 $ sudo ip netns exec NS2 ip link set veth1 up $ sudo ip netns exec NS2 ip link set veth3 up $ sudo ip netns exec NS2 ip link set lo up $ sudo ip netns exec NS2 ip addr add 2.2.2.2/32 dev lo # NS1とNS2でIPフォワーディングを有効にする $ sudo ip netns exec NS1 sysctl -w net.ipv4.conf.all.forwarding=1 $ sudo ip netns exec NS2 sysctl -w net.ipv4.conf.all.forwarding=1 $ sudo ip -all netns exec ip -br link show netns: NS2 lo UNKNOWN 00:00:00:00:00:00 <LOOPBACK,UP,LOWER_UP> veth1@if6 UP 92:8d:2d:c5:ed:00 <BROADCAST,MULTICAST,UP,LOWER_UP> veth3@if8 UP 92:52:f9:bd:a1:46 <BROADCAST,MULTICAST,UP,LOWER_UP> netns: NS1 lo UNKNOWN 00:00:00:00:00:00 <LOOPBACK,UP,LOWER_UP> veth0@if5 UP 1e:4b:7b:d8:84:10 <BROADCAST,MULTICAST,UP,LOWER_UP> veth2@if7 UP 4e:74:36:77:3e:ce <BROADCAST,MULTICAST,UP,LOWER_UP> $ sudo ip -all netns exec ip -br -4 add show netns: NS2 lo UNKNOWN 127.0.0.1/8 2.2.2.2/32 veth1@if6 UP 192.168.100.2/24 veth3@if8 UP 192.168.200.2/24 netns: NS1 lo UNKNOWN 127.0.0.1/8 1.1.1.1/32 veth0@if5 UP 192.168.100.1/24 veth2@if7 UP 192.168.200.1/24 $ sudo ip -all netns exec sysctl -a | grep -e 'net.ipv4.conf.veth[0-4].forwarding' net.ipv4.conf.veth1.forwarding = 1 net.ipv4.conf.veth3.forwarding = 1 net.ipv4.conf.veth0.forwarding = 1 net.ipv4.conf.veth2.forwarding = 1
-
-
RPFの初期設定
-
LinuxのRPFはデフォルトで
2
(Loose)に設定されています。今回は開始の状態では、1
(Strict)に変更します。all
と個別のinterface
の設定がありますが、値の大きい方が優先されます。$ sudo ip netns exec NS1 sysctl -w net.ipv4.conf.default.rp_filter=1 $ sudo ip netns exec NS1 sysctl -w net.ipv4.conf.all.rp_filter=1 $ sudo ip netns exec NS1 sysctl -w net.ipv4.conf.veth0.rp_filter=1 $ sudo ip netns exec NS1 sysctl -w net.ipv4.conf.lo.rp_filter=1 $ sudo ip netns exec NS1 sysctl -a | grep -e 'net.ipv4.conf.[a-z,0-9]*.rp_filter' net.ipv4.conf.all.rp_filter = 1 net.ipv4.conf.default.rp_filter = 1 net.ipv4.conf.lo.rp_filter = 1 net.ipv4.conf.veth0.rp_filter = 1 net.ipv4.conf.veth2.rp_filter = 2
-
Martianログの有効化
-
log_martians
を有効にすると、カーネルは不正または異常と推定されるパケットについてログを出力するようになります。RPFに引っかかったパケットも「Martianパケット」としてログに記録されます。今回は、
NS1
のネームスペースでlog_martians
を有効にします。
$ sudo ip netns exec NS1 sysctl -w net.ipv4.conf.all.log_martians=1 net.ipv4.conf.all.log_martians = 1
-
動作確認
-
初期状態でのPING
-
NS2
において1.1.1.1/32
への往路ルートのみを追加します。 -
NS2
の2.2.2.2/32
からNS1
の1.1.1.1/32
へPINGを試みます。この時点では、NS1
には2.2.2.2/32
へのルートが設定されていないため、NS1
に到達したパケットはRPFによって破棄されるはずです。 - 破棄されることを確認するため、
NS1
側でtcpdump
を実行し、RPFのログを確認します。
# NS2でのルート設定(One way) $ sudo ip netns exec NS2 ip route add 1.1.1.1/32 via 192.168.100.1 dev veth1 $ sudo ip netns exec NS2 ping -c 3 1.1.1.1 -I 2.2.2.2 PING 1.1.1.1 (1.1.1.1) from 2.2.2.2 : 56(84) bytes of data. --- 1.1.1.1 ping statistics --- 3 packets transmitted, 0 received, 100% packet loss, time 2044ms # 復路のルートが存在しないため、到達前にRPFにより破棄、ログ出力されます $ sudo tail -f /var/log/kern.log | grep martian Aug 22 11:12:13 ubuntu kernel: [ 1907.056066] IPv4: martian source 1.1.1.1 from 2.2.2.2, on dev veth0 Aug 22 11:12:14 ubuntu kernel: [ 1908.062730] IPv4: martian source 1.1.1.1 from 2.2.2.2, on dev veth0 Aug 22 11:12:15 ubuntu kernel: [ 1909.086343] IPv4: martian source 1.1.1.1 from 2.2.2.2, on dev veth0 # ICMPが返信されてないことを確認 $ sudo ip netns exec NS1 tcpdump -i veth0 -v icmp tcpdump: listening on veth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes 00:50:31.260069 IP (tos 0x0, ttl 64, id 3564, offset 0, flags [DF], proto ICMP (1), length 84) 2.2.2.2 > 1.1.1.1: ICMP echo request, id 65497, seq 1, length 64 00:50:32.263021 IP (tos 0x0, ttl 64, id 3636, offset 0, flags [DF], proto ICMP (1), length 84) 2.2.2.2 > 1.1.1.1: ICMP echo request, id 65497, seq 2, length 64 00:50:33.287284 IP (tos 0x0, ttl 64, id 3706, offset 0, flags [DF], proto ICMP (1), length 84) 2.2.2.2 > 1.1.1.1: ICMP echo request, id 65497, seq 3, length 64
NS1 は 2.2.2.2/32 へのルートを持っていないため、RPFのStrictモードによって受信パケットが破棄され、PINGは失敗します。
-
-
ルートの設定とRPF=1(Strict)での再試行
-
NS1
とNS2
に相互のループバックアドレスへのルートを設定し、再びNS2
からNS1
へのPINGを実施します。 - この設定で、対象ルートが存在するのでPINGは成功するはずです。
# NS1でのルート設定(bi) $ sudo ip netns exec NS1 ip route add 2.2.2.2/32 via 192.168.100.2 dev veth0 $ sudo ip -n NS1 route get 2.2.2.2 2.2.2.2 via 192.168.100.2 dev veth0 src 192.168.100.1 uid 0 cache $ sudo ip netns exec NS2 ping -c 3 1.1.1.1 -I 2.2.2.2 PING 1.1.1.1 (1.1.1.1) from 2.2.2.2 : 56(84) bytes of data. 64 bytes from 1.1.1.1: icmp_seq=1 ttl=64 time=0.077 ms 64 bytes from 1.1.1.1: icmp_seq=2 ttl=64 time=0.139 ms 64 bytes from 1.1.1.1: icmp_seq=3 ttl=64 time=0.123 ms
-
-
Route2経由のルート変更とPING試行
- 次に、
NS1
の2.2.2.2/32
へのルートをRoute2
を経由するように変更し、再びNS2
からNS1
へのPINGを試みます。この場合、非対象ルートとなるためRPFの原因でPINGは失敗するはずです。 - 失敗する理由を確認するため、再度
tcpdump
やログを確認します。
# NS1のルートをRoute2経由に変更 $ sudo ip netns exec NS1 ip route change 2.2.2.2/32 via 192.168.200.2 dev veth2 $ sudo ip -n NS1 route get 2.2.2.2 2.2.2.2 via 192.168.200.2 dev veth2 src 192.168.200.1 uid 0 cache # 再度NS2からNS1へのPING $ sudo ip netns exec NS2 ping -c 3 1.1.1.1 -I 2.2.2.2 PING 1.1.1.1 (1.1.1.1) from 2.2.2.2 : 56(84) bytes of data. --- 1.1.1.1 ping statistics --- 3 packets transmitted, 0 received, 100% packet loss, time 2030ms # Kernel LogでRPFで破棄されていることを確認 $ sudo tail -f /var/log/kern.log | grep martian Aug 22 14:12:55 ubuntu kernel: [12747.687934] IPv4: martian source 1.1.1.1 from 2.2.2.2, on dev veth0 Aug 22 14:12:56 ubuntu kernel: [12748.689370] IPv4: martian source 1.1.1.1 from 2.2.2.2, on dev veth0 Aug 22 14:12:57 ubuntu kernel: [12749.715718] IPv4: martian source 1.1.1.1 from 2.2.2.2, on dev veth0 # ICMPが返信されてないことを確認 $ sudo ip netns exec NS1 tcpdump -i veth0 -v icmp tcpdump: listening on veth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes 14:16:15.769525 IP (tos 0x0, ttl 64, id 27416, offset 0, flags [DF], proto ICMP (1), length 84) 2.2.2.2 > 1.1.1.1: ICMP echo request, id 48840, seq 1, length 64 14:16:16.777444 IP (tos 0x0, ttl 64, id 27600, offset 0, flags [DF], proto ICMP (1), length 84) 2.2.2.2 > 1.1.1.1: ICMP echo request, id 48840, seq 2, length 64 14:16:17.799714 IP (tos 0x0, ttl 64, id 27709, offset 0, flags [DF], proto ICMP (1), length 84) 2.2.2.2 > 1.1.1.1: ICMP echo request, id 48840, seq 3, length 64 3 packets captured 3 packets received by filter 0 packets dropped by kernel
- 次に、
-
RPF=2(Loose)での試行
-
NS1
のRPF設定を2
(Loose)に変更し、再度NS2
からのPINGを実施します。この状態では、PINGが成功するはずです。
# NS1のRPFをLooseモードに変更 $ sudo ip netns exec NS1 sysctl -w net.ipv4.conf.all.rp_filter=2 net.ipv4.conf.all.rp_filter = 2 $ sudo ip netns exec NS1 sysctl -w net.ipv4.conf.veth0.rp_filter=2 net.ipv4.conf.veth0.rp_filter=2 # 再びNS2からNS1へのPING $ sudo ip netns exec NS2 ping -c 3 1.1.1.1 -I 2.2.2.2 PING 1.1.1.1 (1.1.1.1) from 2.2.2.2 : 56(84) bytes of data. 64 bytes from 1.1.1.1: icmp_seq=1 ttl=64 time=0.054 ms 64 bytes from 1.1.1.1: icmp_seq=2 ttl=64 time=2.62 ms 64 bytes from 1.1.1.1: icmp_seq=3 ttl=64 time=0.174 ms --- 1.1.1.1 ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 2005ms rtt min/avg/max/mdev = 0.054/0.949/2.620/1.182 ms
-
-
RPF=0(オフ)での試行
-
最後に、
NS1
のRPF設定を0
(オフ)に変更し、NS1
内の2.2.2.2/32
へのルートを削除します。その状態で再度PINGを実行します。 -
この動作を確認するため、ログを確認します。
# NS1のRPFをオフに設定 $ sudo ip netns exec NS1 sysctl -w net.ipv4.conf.all.rp_filter=0 net.ipv4.conf.all.rp_filter=0 $ sudo ip netns exec NS1 sysctl -w net.ipv4.conf.veth0.rp_filter=0 net.ipv4.conf.veth0.rp_filter=0 # 2.2.2.2/32 へのルートを削除 $ sudo ip netns exec NS1 ip route del 2.2.2.2/32 $ sudo ip -n NS1 route get 2.2.2.2 RTNETLINK answers: Network is unreachable # 再びNS2からNS1へのPING $ sudo ip netns exec NS2 ping -c 3 1.1.1.1 -I 2.2.2.2 PING 1.1.1.1 (1.1.1.1) from 2.2.2.2 : 56(84) bytes of data. --- 1.1.1.1 ping statistics --- 3 packets transmitted, 0 received, 100% packet loss, time 2042ms # RPFで破棄されてないので出力されない $ sudo tail -f /var/log/kern.log | grep martian (出力はないはずです)
-
参考
Discussion