🐆

LinuxにおけるVRF基礎

2024/08/19に公開

概要

VRF(Virtual Routing and Forwarding)は、単一のルータやサーバにおいて複数のルーティングテーブルを使用できる技術になります。通常Linuxでデフォルトで使用するのはグローバルのルーティングテーブルになりますが、VRFはそれとは別に独立したルーティングテーブルを持ち、独立したVRFに基づいてパケットを分離、ルーティングすることができます。

VRFと類似の機能にNetwork Namespaceがありますが、目的と使い方に違いがあるため用途に合わせて使い分けることが必要です。Network Namespaceは、カーネルレベルでネットワークスタック全体を分離する手法であり、対してVRFは主にルーティングテーブルの分離のみに限定しており軽量に動作します。Network Namespaceは、テーブルの分離だけではなく、スタックを分離しているためインターフェース、ルーティングテーブル、iptables、ip rule、resolverなどを分離して動かすことができます。そのため、両者は包含関係にあり、例えば1つのNetwork Namespace内に複数のVRFのテーブルを持つことができます。

基本操作

LinuxでVRFを作成するには、ipコマンドを使用します。以下に基本的な手順を示します。

なお今回は、以下の環境を使用しています。基本的にKernel v4.4で動作しますが、l3mdev FIB ルールをサポートするためKernelは v4.8 以上を推奨します。

$ cat /proc/version
Linux version 5.15.0-117-generic (buildd@lcy02-amd64-102) (gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0, GNU ld (GNU Binutils for Ubuntu) 2.38) 
  1. VRFデバイスの作成:

    $ sudo ip link add vrf-blue type vrf table 1001
    

    vrf-blueはVRFデバイスの名前、1001はルーティングテーブルIDです。このコマンドで、新しいVRFデバイスが作成されます。

  2. VRFデバイスの有効化:

    $ sudo ip link set dev vrf-blue up
    
  3. VRFの確認:

    $ ip -d link show vrf-blue
    4: vrf-blue: <NOARP,MASTER,UP,LOWER_UP> mtu 65575 qdisc noqueue state UP mode DEFAULT group default qlen 1000
        link/ether 3a:e8:ee:11:68:cd brd ff:ff:ff:ff:ff:ff promiscuity 0 minmtu 1280 maxmtu 65575
        vrf table 1001 addrgenmode eui64 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 6553
    
    $ ip -d link show type vrf
    4: vrf-blue: <NOARP,MASTER,UP,LOWER_UP> mtu 65575 qdisc noqueue state UP mode DEFAULT group default qlen 1000
        link/ether 3a:e8:ee:11:68:cd brd ff:ff:ff:ff:ff:ff promiscuity 0 minmtu 1280 maxmtu 65575
        vrf table 1001 addrgenmode eui64 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    
    $ ip -br link show type vrf
    vrf-blue         UP             3a:e8:ee:11:68:cd <NOARP,MASTER,UP,LOWER_UP>
    

    いくつか確認方法を示しましたが、vrf-blueが正しく設定されているかを確認できます。2番目の例ではtype vrfとすることで名称を知らなくても一覧を得ることができます。 例に-dオプションが含まれますが、これはルーティングテーブルIDを表示させるために必要だからです。

  4. ネットワークインターフェースをVRFに割り当て:

    $ sudo ip link set dev eth0 master vrf-blue
    

    eth0インターフェースがvrf-blue VRFに割り当てられます。

  5. VRFに割り当てられたインターフェースを確認:

    $ ip link show vrf vrf-blue
    3: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel master vrf-blue state UP mode DEFAULT group default qlen 1000
        link/ether 50:25:5d:00:01:01 brd ff:ff:ff:ff:ff:ff
    $ ip -br link show vrf vrf-blue
    eth0             UP             50:25:5d:00:01:01 <BROADCAST,MULTICAST,UP,LOWER_UP>
    
    

    eth0がvrf-blue に割り当てられていることが確認できます。インターフェースが多く存在する場合には-brオプションを付与することで簡易表示することもできます。

  6. IPアドレスの付与

    $ sudo ip addr add dev eth0 192.168.100.1/24
    $ ip -d address show eth0
    3: ens4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel master vrf-blue state UP group default qlen 1000
        link/ether 50:25:5d:00:01:01 brd ff:ff:ff:ff:ff:ff promiscuity 0 minmtu 68 maxmtu 65535
        vrf_slave table 1001 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535 parentbus virtio parentdev virtio2
        inet 192.168.100.1/24 scope global ens4
           valid_lft forever preferred_lft forever
        inet6 fe80::5225:5dff:fe00:101/64 scope link
           valid_lft forever preferred_lft forever
    
  7. VRF内の隣接エントリの確認:

    $ ip neighbor show vrf vrf-blue
    192.168.100.2 dev eth0 lladdr 00:50:79:66:68:02 DELAY
    

    L2解決できているアドレスを確認しています。この例では1つの隣接関係しか表示されていませんが、Broadcast型のLANネットワークの場合には通常複数の隣接関係が表示されるはずです。

  8. VRF内でのルートの挿入:

    $ sudo ip route add 192.168.200.0/24 via 192.168.100.2 vrf vrf-blue
    
    $ ip route show vrf vrf-blue
    192.168.100.0/24 dev eth0 proto kernel scope link src 192.168.100.1
    192.168.200.0/24 via 192.168.100.2 dev eth0
    
    $ ip route show table 1001
    192.168.100.0/24 dev eth0 proto kernel scope link src 192.168.100.1
    local 192.168.100.1 dev eth0 proto kernel scope host src 192.168.100.1
    broadcast 192.168.100.255 dev eth0 proto kernel scope link src 192.168.100.1
    192.168.200.0/24 via 192.168.100.2 dev eth0
    
    $ ip route get 192.168.200.1  vrf vrf-blue
    192.168.200.1 via 192.168.100.2 dev eth0 table 1001 src 192.168.100.1 uid 1000
        cache
    

    table 1001vrf vrf-blueの指定方法の違いで表示に差分が生じていますが、これはテーブル指定の場合にはVRFに直接関連しないルート(たとえば、ローカル、ホストルートやブロードキャストルート)も省略されることなく全て表示されるからです。

  9. VRF内でのコマンド実行:

    VRF内部からのコマンドを実行したい場合には、複数の方法があります。ipコマンドを利用したVRF内部からコマンドを実行する方法とアプリケーションからインターフェースを指定する方法になります。

    $ sudo ip vrf exec vrf-blue ping -c 1 192.168.100.2
    PING 192.168.100.2 (192.168.100.2) 56(84) bytes of data.
    64 bytes from 192.168.100.2: icmp_seq=1 ttl=64 time=3.45 ms
    
    --- 192.168.100.2 ping statistics ---
    1 packets transmitted, 1 received, 0% packet loss, time 0ms
    rtt min/avg/max/mdev = 3.449/3.449/3.449/0.000 ms
    
    $ ping -c 1 -I eth0 192.168.100.2
    PING 192.168.100.2 (192.168.100.2) from 192.168.100.1 ens4: 56(84) bytes of data.
    64 bytes from 192.168.100.2: icmp_seq=1 ttl=64 time=1.72 ms
    
    --- 192.168.100.2 ping statistics ---
    1 packets transmitted, 1 received, 0% packet loss, time 0ms
    rtt min/avg/max/mdev = 1.724/1.724/1.724/0.000 ms
    
    $ sudo ip vrf exec vrf-blue traceroute 192.168.100.2
    traceroute to 192.168.100.2 (192.168.100.2), 30 hops max, 60 byte packets
     1  192.168.100.2 (192.168.100.2)  5.834 ms  1.943 ms  2.065 ms
    
  10. 参考1:

    ディバイスeth0に対してip link set dev eth0 master vrf-blueコマンドでVRFの割り当てを行いました。master指定によりインターフェースを特定の仮装インターフェースに関連づけることができます。今回はVRFに割り当てましたが、他にもBONDING、BRIDGEなどの指定ができます。インターフェースを関連付けから切り離すには、nomaster キーワードを使用します。

    $ sudo ip link set dev eth0 nomaster
    
  11. 参考2:

    /etc/iproute2/rt_tables ファイルには、ルーティングテーブルのエントリが含まれています。ここには、デフォルトのテーブルIDとカスタムのテーブル名が一覧表示されています。

    $cat /etc/iproute2/rt_tables
    #
    # reserved values
    #
    255     local
    254     main
    253     default
    0       unspec
    #
    # local
    #
    #1      inr.ruhep
    1001    vrf1 #(カスタムの例)
    

    このファイルに、VRFに関連するテーブルIDが記載されている場合は対応するテーブルIDとVRF名が確認できます。カスタムで使用できるのは確保されていないテーブルIDを使用することになります。ipコマンドでVRFを追加し場合には、Kernel内部で保持されることからこのファイルへの書き込みはありません。こちらのファイルを手動で追加した場合には起動時に反映されてVRF名が利用できるようになります。テーブルIDを新たに確保する場合、いくつかのテーブルIDはOSで確保済みであり使用できません。

    テーブル番号 テーブル名 使用用途
    255 local ローカルに設定されたIPアドレスに関するルートを保持します。ループバックアドレスや、ローカルホスト上で設定されたIPアドレスへのルーティングに使用されます。(例:127.0.0.1などの自ホスト宛を処理)
    254 main 何もテーブルを指定しない時にデフォルトで選択されるテーブルです。通常はこのテーブルを利用します。
    253 default このテーブルは通常、特定の条件に基づいてデフォルトルートを保持します。特に指定がない限り使用されることはありませんが、共通のポリシールーティングに基づくデフォルト動作などの管理で使用されることがあります。
    0 unspec これは無指定(unspecified)を意味し、通常の操作では使用されません。特にポリシールーティングを設定する際などに明示的にルーティングテーブルを指定しない場合や、テーブルを削除する場合に使用されることがあります。
  12. 参考3:VRF に関連付けられた l3mdev からのパケット着信

    VRFは同一の物理インターフェースを持つデバイス上で複数のネットワークを隔離して管理することが可能になります。しかし、VRF に関連付けられた l3mdev から着信するパケットは、デフォルトではシステム上のプロセスにアクセスできない状態がデフォルトの設定になります。

    $ sudo sysctl -a | grep l3mdev
    net.ipv4.raw_l3mdev_accept = 1
    net.ipv4.tcp_l3mdev_accept = 0 #not accept
    net.ipv4.udp_l3mdev_accept = 0 #not accept
    

    これは通常アプリケーション側のソケットがデフォルトではVRFディバイスを意識した実装を行なっていないからです。

    setsockopt(sd, SOL_SOCKET, SO_BINDTODEVICE, dev, strlen(dev)+1);
    

    着信できるようにするためにはLinux システムのネットワーク設定を調整する必要があります。sysctl ツールを使用して、TCP および UDP パケットの受信条件を変更することができます。変更後は全てのVRFからパケットの着信が可能になるためアプリケーション側の変更が必要なくなりますが、セキュリティ面では注意が必要となります。

    $ sudo sysctl -w net.ipv4.tcp_l3mdev_accept=1
    net.ipv4.tcp_l3mdev_accept=1
    $ sudo sysctl -w net.ipv4.udp_l3mdev_accept=1
    net.ipv4.udp_l3mdev_accept=1
    

    これらの設定により、VRF に関連付けられた l3mdev デバイスからの TCP および UDP パケットの受信を許可することができます。または、永続的にするためには、/etc/sysctl.conf ファイルに設定を追加して、sysctl -pまたは再起動で反映が必要になります。

ネットワーク構成例

下記の例では、2 つのネットワークネームスペース (NS1, NS2) と 3 つの VRF (VRF1, VRF2, VRF3) を作成し、それぞれを VETH インターフェースで接続します。NS1/VRF1内部のIFからNS2/VRF2を経由してNS2/VRF3までルーチングを確保し、ネットワーク的な到達性を確保します。

+------------------+                  +-----------------------------------------------------+
|       NS1        |                  |                       NS2                           |
|                  |                  |                                                     |
| +-------------+  |                  |  +----------------+                   +------------+|
| |    VRF1     |.1| 192.168.100.0/24 |.2|      VRF2      |  192.168.200.0/24 |     VRF3   ||
| |  veth2-vrf1 +--|------------------+--+ veth2-vrf2     |.1               .2|            ||
| |             |  |                  |  |      veth1-vrf2+-------------------+ veth1-vrf3 ||
| |             |  |                  |  |                |                   |            ||
| |             |  |                  |  |                |                   |            ||
| |             |  |                  |  |                |                   |            ||
| |             |  |                  |  |                |                   |            ||
| +-------------+  |                  |  +----------------+                   +------------+|
+------------------+                  +-----------------------------------------------------+

ネームスペース,VRFの作成と有効化

まず、2 つのネットワークネームスペース (NS1, NS2) を作成して、NS上でVRFを作成します。NSと紐づいたVRFを作成できたら、VRFディバイスを有効化します。

# ネームスペース NS1, NS2 の作成
$ sudo ip netns add NS1
$ sudo ip netns add NS2
$ ip netns list
NS1
NS2

# VRF の作成
$ sudo ip netns exec NS1 ip link add VRF1 type vrf table 1001
$ sudo ip netns exec NS2 ip link add VRF2 type vrf table 1002
$ sudo ip netns exec NS2 ip link add VRF3 type vrf table 1003

# VRF をネームスペース内で有効化
$ sudo ip netns exec NS1 ip link set dev VRF1 up
$ sudo ip netns exec NS2 ip link set dev VRF2 up
$ sudo ip netns exec NS2 ip link set dev VRF3 up
$ sudo ip -all netns exec ip -br link show type vrf
netns: NS1
VRF1             UP             c6:1c:5f:68:01:d2 <NOARP,MASTER,UP,LOWER_UP>
netns: NS2
VRF2             UP             0a:af:fc:b3:e0:cc <NOARP,MASTER,UP,LOWER_UP>
VRF3             UP             62:d7:7a:c5:33:98 <NOARP,MASTER,UP,LOWER_UP>

VETHの接続とアドレス付与

VRF内にVETHのペアを2セット作成して、VETHをまずNSと紐づけた上でVRFに割り当てます。VETHの有効化を行い、L3情報の付与を行います。この時点でNS/VRF越しのIP疎通が可能になりますので、PINGでVETHピア同士の疎通を確認します。

# VETH ペアの作成と VRF2, VRF3 の接続
$ sudo ip link add veth1-vrf2 type veth peer name veth1-vrf3
$ sudo ip link add veth2-vrf1 type veth peer name veth2-vrf2

# netns と veth 紐づけ
$ sudo ip link set veth1-vrf2 netns NS2
$ sudo ip link set veth1-vrf3 netns NS2
$ sudo ip link set veth2-vrf1 netns NS1
$ sudo ip link set veth2-vrf2 netns NS2

# インターフェースをVRFに割り当て
$ sudo ip netns exec NS1 ip link set dev veth2-vrf1 master VRF1
$ sudo ip netns exec NS2 ip link set dev veth2-vrf2 master VRF2
$ sudo ip netns exec NS2 ip link set dev veth1-vrf2 master VRF2
$ sudo ip netns exec NS2 ip link set dev veth1-vrf3 master VRF3

# VETH ペアの有効化
$ sudo ip netns exec NS1 ip link set dev veth2-vrf1 up
$ sudo ip netns exec NS2 ip link set dev veth2-vrf2 up
$ sudo ip netns exec NS2 ip link set dev veth1-vrf2 up
$ sudo ip netns exec NS2 ip link set dev veth1-vrf3 up

#NS1 veth2-vrf1  .1---- 192.168.100.0/24 ---.2  veth2-vrf2 NS2
#NS2 veth1-vrf2  .1---- 192.168.200.0/24 ---.2  veth1-vrf3 NS2
# L3アドレスの付与
$ sudo ip netns exec NS1 ip addr add dev veth2-vrf1 192.168.100.1/24
$ sudo ip netns exec NS2 ip addr add dev veth2-vrf2 192.168.100.2/24
$ sudo ip netns exec NS2 ip addr add dev veth1-vrf2 192.168.200.1/24
$ sudo ip netns exec NS2 ip addr add dev veth1-vrf3 192.168.200.2/24


$ sudo ip -all netns exec ip -br -4 addr show
netns: NS1
lo               UNKNOWN        127.0.0.1/8
veth2-vrf1@if6   UP             192.168.100.1/24
netns: NS2
lo               UNKNOWN        127.0.0.1/8
veth1-vrf2@veth1-vrf3 UP             192.168.200.1/24
veth1-vrf3@veth1-vrf2 UP             192.168.200.2/24
veth2-vrf2@if5   UP             192.168.100.2/24

$ sudo ip -all netns exec ip -d -4 addr show
netns: NS1
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 promiscuity 0 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
5: veth2-vrf1@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master VRF1 state UP group default qlen 1000
    link/ether 52:4e:47:47:05:91 brd ff:ff:ff:ff:ff:ff link-netns NS2 promiscuity 0
    veth
    vrf_slave table 1001 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    inet 192.168.100.1/24 scope global veth2-vrf1
       valid_lft forever preferred_lft forever
       
netns: NS2
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 promiscuity 0 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
4: veth1-vrf2@veth1-vrf3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master VRF2 state UP group default qlen 1000
    link/ether 7a:64:27:34:a0:27 brd ff:ff:ff:ff:ff:ff promiscuity 0
    veth
    vrf_slave table 1002 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    inet 192.168.200.1/24 scope global veth1-vrf2
       valid_lft forever preferred_lft forever
5: veth1-vrf3@veth1-vrf2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master VRF3 state UP group default qlen 1000
    link/ether 3a:49:a7:e9:67:61 brd ff:ff:ff:ff:ff:ff promiscuity 0
    veth
    vrf_slave table 1003 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    inet 192.168.200.2/24 scope global veth1-vrf3
       valid_lft forever preferred_lft forever
6: veth2-vrf2@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master VRF2 state UP group default qlen 1000
    link/ether a6:7a:47:8f:40:fb brd ff:ff:ff:ff:ff:ff link-netns NS1 promiscuity 0
    veth
    vrf_slave table 1002 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    inet 192.168.100.2/24 scope global veth2-vrf2
       valid_lft forever preferred_lft forever

$ sudo ip -n NS1 vrf exec VRF1 ping -c 1 -I veth2-vrf1 192.168.100.2
PING 192.168.100.2 (192.168.100.2) from 192.168.100.1 veth2-vrf1: 56(84) bytes of data.
64 bytes from 192.168.100.2: icmp_seq=1 ttl=64 time=0.039 ms

--- 192.168.100.2 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.039/0.039/0.039/0.000 ms

$ sudo ip -n NS2 vrf exec VRF2 ping -c 1 -I veth1-vrf2 192.168.200.2
PING 192.168.200.2 (192.168.200.2) from 192.168.200.1 veth1-vrf2: 56(84) bytes of data.
64 bytes from 192.168.200.2: icmp_seq=1 ttl=64 time=0.077 ms

--- 192.168.200.2 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.077/0.077/0.077/0.000 ms

VRFへのルート追加とエンド疎通

前節までの状態では、VRF内にはConnectedルートのみが存在しているため、VRF1からVRF3のエンド同士の疎通はできません。VRF1とVRF3にお互いのルートを追加することで、エンド同士での疎通ができるようにします。

# 現在のルート確認
$ sudo ip -n NS1 route show vrf VRF1
192.168.100.0/24 dev veth2-vrf1 proto kernel scope link src 192.168.100.1
$ sudo ip -n NS2 route show vrf VRF2
192.168.100.0/24 dev veth2-vrf2 proto kernel scope link src 192.168.100.2
192.168.200.0/24 dev veth1-vrf2 proto kernel scope link src 192.168.200.1
$ sudo ip -n NS2 route show vrf VRF3
192.168.200.0/24 dev veth1-vrf3 proto kernel scope link src 192.168.200.2

# 必要なルートの追加
$ sudo ip -n NS1 route add 192.168.200.0/24 via 192.168.100.2 vrf VRF1
$ sudo ip -n NS2 route add 192.168.100.0/24 via 192.168.200.1 vrf VRF3

# 追加ルートの確認
$ sudo ip -n NS1 route show vrf VRF1
192.168.100.0/24 dev veth2-vrf1 proto kernel scope link src 192.168.100.1
192.168.200.0/24 via 192.168.100.2 dev veth2-vrf1
$ sudo ip -n NS2 route show vrf VRF3
192.168.100.0/24 via 192.168.200.1 dev veth1-vrf3
192.168.200.0/24 dev veth1-vrf3 proto kernel scope link src 192.168.200.2

# VRF1からVRF3への疎通確認
$ sudo ip netns exec NS1 ping 192.168.200.2 -I veth2-vrf1 -c 1
PING 192.168.200.2 (192.168.200.2) from 192.168.100.1 veth2-vrf1: 56(84) bytes of data.
64 bytes from 192.168.200.2: icmp_seq=1 ttl=63 time=0.085 ms

--- 192.168.200.2 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.085/0.085/0.085/0.000 ms

まとめ

VRFは、ネットワークの仮想化とセキュリティ強化するためのツールであり、適切に活用することで、ネットワーク運用の効率化と信頼性向上が期待できます。

参考:Virtual Routing and Forwarding (VRF)

Discussion