😽

RPF(Reverse Path Forwarding)の概要と動作

2024/08/23に公開

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 は、インターフェース単位で有効/無効を設定します。一方、iptablesrpfilter モジュールは、ルール単位でより細かい制御が可能です。例えば、一致した場合にログを取得して破棄する、不一致の場合に別の処理にジャンプさせるなど柔軟な制御が可能となります。詳しくはman iptables-extensions(8) などを参照ください。また、もう一つの違いとしてカーネルの rp_filter はIPsecで保護されたパケットを特別扱いしますが、iptablesrpfilter モジュールは特別扱いしません。これは、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  |  
            +-----------------+                  +-----------------+

  1. ネットワークネームスペースの作成とVETHペアの設定

    • NS1NS2 の2つのネットワークネームスペースを作成します。

    • NS1 には veth0veth2 を、NS2 には veth1veth3 をそれぞれ接続します。

    # ネームスペースの作成
    $ 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>
    
  2. インターフェースの設定とIPアドレスの割り当て
    次に、各インターフェースにIPアドレスを割り当てます。

    • veth0veth1 は、192.168.100.0/24 のサブネットに属し、それぞれ 192.168.100.1192.168.100.2 をアサインします(Route1)
    • veth2veth3 は、192.168.200.0/24 のサブネットに属し、それぞれ 192.168.200.1192.168.200.2 をアサインします(Route2)
    • NS11.1.1.1/32 のループバックアドレスを設定。
    • NS22.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
    
  3. RPFの初期設定

  4. 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
    
  5. 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
    

動作確認

  1. 初期状態でのPING

    • NS2 において1.1.1.1/32への往路ルートのみを追加します。
    • NS22.2.2.2/32 から NS11.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は失敗します。

  2. ルートの設定とRPF=1(Strict)での再試行

    • NS1NS2 に相互のループバックアドレスへのルートを設定し、再び 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
    
  3. Route2経由のルート変更とPING試行

    • 次に、NS12.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
    
    
    
    
    
  4. 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
    
  5. 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
     (出力はないはずです)
    
    

参考

https://www.kernel.org/doc/html/latest/networking/ip-sysctl.html

Discussion