什么是 VRF(virtual routing forwarding)#

VRF是一种实现三层网络隔离的关键技术。它通过创建多个路由表,为不同的网络流量提供独立的转发路径。 这意味着,任何三层网络结构,如接口的IP地址、静态路由的配置,甚至BGP(边界网关协议)会话,都可以被映射到特定的VRF中。这种映射机制就像是为每个VRF构建了一个独立的网络空间,彼此之间相互隔离,极大地增强了网络的安全性和管理的便利性。在MPLS VPN(多协议标签交换虚拟专用网络)等应用场景中,VRF为实现大规模的网络隔离和灵活的路由策略提供了基础框架。就像 VLAN 隔离了二层网络一样,VRF 隔离了三层网络。

5eb20c3e919fe3724b92c2ae7a66a7da_MD5

为什么需要 VRF#

在 VRF 出现之前,Linux 用户主要采用两种方式来尝试实现类似的功能:策略路由(policy routing)和网络命名空间(net namespace)。然而,这两种方法都存在明显的局限性。

策略路由虽然能够通过多个路由表和策略规则来模拟 VRF 的部分功能(事实上,在 Linux 中,也是基于策略路由来去对 VRF 做的实现),但它的缺点十分突出。这种方式在配置和管理上非常复杂,难以确保网络隔离的有效性,在面对严格的网络审计时,往往无法通过。其复杂性不仅增加了运维的难度,还可能导致网络故障的风险上升,因此不被推荐使用。

网络命名空间在容器技术兴起后得到了广泛应用,它能够为容器提供全面的网络隔离。但在模拟 VRF 功能时,却显得有些“大材小用”。网络命名空间会对所有网络相关的资源进行完全隔离,包括设备、接口、ARP 表和路由表等。这意味着,即使是一些不需要隔离的服务,也会被隔离在不同的命名空间中。以 LLDP(链路层发现协议)为例,在使用网络命名空间的情况下,若要在不同的网络隔离环境中使用 LLDP,就需要在每个命名空间中单独运行实例,并且由于默认套接字相同,还需要为每个实例创建独特的套接字。这不仅增加了系统的开销,还使得管理变得更加复杂。相比之下,VRF 在隔离三层网络结构的同时,允许全局配置的共享和非三层感知服务的统一运行,大大提高了资源的利用效率。

Policy Routing VRF Net Namespace
隔离路由表 隔离三层网络 整个协议栈从二层到 socket 隔离

a9f7c55f8f39572d339b138fb1e12429_MD5c9b6614c864c7d35a8ef0a4f12ecdbfa_MD59edc8b051f504bf72140d1238513d687_MD5

VRF 配置#

在 Linux 系统中配置 VRF,主要借助iproute2包来完成一系列操作。

  • 创建 VRF,并关联到 table 1
test1@test1:~$ ip link add vrf-1 type vrf table 1
test1@test1:~$ ip link set vrf-1 up
  • 添加接口到 VRF,可以看到 wg0 的 master 是 vrf-1,所有 wg0 的流量会使用关联的 vrf-1 路由表进行路由
test1@test1:~$ ip link set wg0 master vrf-1
test1@test1:~$ ip -d link show wg0 
9: wg0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1400 qdisc noqueue master vrf-1 state UNKNOWN mode DEFAULT group default qlen 1000
    link/none  promiscuity 0 minmtu 0 maxmtu 2147483552 
    wireguard 
    vrf_slave table 1 addrgenmode none numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
  • 添加和查看 VRF 静态路由
root@test1:~# ip route add default via 10.1.0.10 vrf vrf-1
root@test1:~# ip route show table 1 
default via 10.1.0.10 dev wg0 
local 10.1.0.10 dev wg0 proto kernel scope host src 10.1.0.10 
root@test1:~# ip route show vrf vrf-1 
default via 10.1.0.10 dev wg0 

VRF 之间路由#

有两种方法可以执行跨 VRF 路由。第一种方法涉及一个 VRF 的表中配置的路由,指向绑定到不同 VRF 的设备。

$ ip route add table 10 192.168.200.0/24 dev sw1p6

命中这些路由的数据包可以由设备转发:

$ ip route show table 10
...
192.168.200.0/24 dev sw1p6  scope link offload

执行跨 VRF 路由的第二种方法是将路由指向不同的 VRF 主设备。命中此类路由的数据包将在 VRF 的表中进行完整查找:

$ ip route add table 10 192.168.200.0/24 dev vrf-red

然而,这些路由目前并未卸载。命中它们的数据包由内核转发:

$ ip route show table 10
...
192.168.200.0/24 dev vrf-red scope link

请注意,卸载标志未设置(offloading 是 mellanox 网卡 offloading 的设置)。

local 路由#

Linux 中,默认的路由查找顺序是先找 local 路由表,再找 main 表。而且 local 表的优先级是最高的:

test2@test2:~$ ip rule show 
0:      from all lookup local
32766:  from all lookup main
32767:  from all lookup default

这就意味着,如果有本地地址和 VRF 路由地址冲突,势必会影响到 VRF 路由转发。例如,本机的应用层有两个属于不同 VRF 域确监听同一个 IP 地址的服务。

  • ens33 和 wg0 都配置了 10.1.0.10/32 地址,wg0 属于 vrf-1
root@test1:~# ip -4 addr show 
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    altname enp2s1
    inet 192.168.50.16/24 brd 192.168.50.255 scope global dynamic noprefixroute ens33
       valid_lft 53664sec preferred_lft 53664sec
    inet 10.1.0.10/32 scope global ens33
       valid_lft forever preferred_lft forever
9: wg0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1400 qdisc noqueue master vrf-1 state UNKNOWN group default qlen 1000
    inet 10.1.0.10/32 scope global wg0
       valid_lft forever preferred_lft forever
  • 10.1.0.10 的路由同时存在在 local 表和 vrf-1 表中:
root@test1:~# ip route show table local 
local 10.1.0.10 dev ens33 proto kernel scope host src 10.1.0.10 
root@test1:~# ip route show table 1 
default via 10.1.0.10 dev wg0 
local 10.1.0.10 dev wg0 proto kernel scope host src 10.1.0.10 

root@test1:~# ip rule show 
0:      from all lookup local
1000:   from all lookup [l3mdev-table]
32766:  from all lookup main
32767:  from all lookup default
  • 在 vrf-1 域中 ping 10.1.0.10,无法生效
root@test1:~# ip vrf exec vrf-1 ping 10.1.0.10 -c 1
PING 10.1.0.10 (10.1.0.10) 56(84) bytes of data.
^C
--- 10.1.0.10 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms

这个问题的本质在于,VRF 的路由隔离是通过策略路由实现的,而 Linux 默认行为策略路由的优先级低于 local 路由。这样 local 路由就打破了 VRF 的路由隔离。规避这个问题的方法就是把 local 路由的优先级调低:

root@test1:~# ip rule add pref 32765 table local 
root@test1:~# ip rule del pref 0
root@test1:~# ip rule show 
1000:   from all lookup [l3mdev-table]
32765:  from all lookup local
32766:  from all lookup main
32767:  from all lookup default
  • 再次在 vrf-1 域中 ping 10.1.0.10,可以工作:
root@test1:~# ip vrf exec vrf-1 ping 10.1.0.10 -c 1
PING 10.1.0.10 (10.1.0.10) 56(84) bytes of data.
64 bytes from 10.1.0.10: icmp_seq=1 ttl=64 time=0.288 ms

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

socket listener#

默认情况下,不同 VRF 域中的 socket 绑定的端口是隔离的,也就是说,不同的 VRF 域中的 socket 可以监听相同的端口。

使用 setsockopt 可以将 socket 绑定到指定的 VRF 域:

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

或者使用 cmsg 和 IP_PKTINFO 指定 output device。

如果想让在默认 VRF 域(即没有绑定到任何 VRF 设备的 socket)中运行 TCP/UDP 服务为所有 VRF 域提供服务,需要开启 tcp_l3mdev_acceptudp_l3mdev_accept

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

对于 RAW socket,出于后向兼容性的考虑默认是启用的。

  • 默认情况下,默认 VRF 可以和其它 VRF 可以监听同一个端口
root@test1:~# python3 server.py & 
[1] 137516
root@test1:~# Server is listening on port 8888
Waiting for a connection...

root@test1:~# python3 server.py wg0
Server is listening on port 8888
Waiting for a connection...
  • 但是包从不同的 VRF 域访问时是不会处理的
# vrf-1 域的访问,可以连通 vrf-1 的监听
root@test1:~# python3 server.py wg0
Server is listening on port 8888
Waiting for a connection...
Connection from ('10.2.0.10', 45948)
root@test1:~# ss -nltp src :8888
State                       Recv-Q                      Send-Q                                           Local Address:Port                                            Peer Address:Port                      Process                      
LISTEN                      0                           1                                                  0.0.0.0%wg0:8888                                                 0.0.0.0:*                          users:(("python3",pid=137539,fd=3))

test2@test2:~$ telnet 10.1.0.10 8888
Trying 10.1.0.10...
Connected to 10.1.0.10.
Escape character is '^]'.
hello
hello

# vrf-1 域的访问,无法访问默认 VRF 的监听,连接直接被拒绝
root@test1:~# python3 server.py 
Server is listening on port 8888
Waiting for a connection...
root@test1:~# ss -nltp src :8888
State                       Recv-Q                      Send-Q                                           Local Address:Port                                            Peer Address:Port                      Process                      
LISTEN                      0                           1                                                      0.0.0.0:8888                                                 0.0.0.0:*                          users:(("python3",pid=137528,fd=3))

test2@test2:~$ telnet 10.1.0.10 8888
Trying 10.1.0.10...
telnet: Unable to connect to remote host: Connection refused

16:52:01.999705 IP 10.2.0.10.42296 > 10.1.0.10.8888: Flags [S], seq 4099934920, win 65280, options [mss 1360,sackOK,TS val 3732241640 ecr 0,nop,wscale 7], length 0
16:52:02.000938 IP 10.1.0.10.8888 > 10.2.0.10.42296: Flags [R.], seq 0, ack 4099934921, win 0, length 0
  • 开启tcp_l3mdev_accept后,默认 VRF 不能和其它 VRF 域监听同一个端口。(这里内核文档的描述感觉有问题?可以同时监听一个端口?)

https://docs.kernel.org/networking/vrf.html

Using VRF-aware applications (applications which simultaneously create sockets outside and inside VRFs) in conjunction with <font style="color:rgb(34, 34, 34);background-color:rgb(236, 240, 243);">net.ipv4.tcp_l3mdev_accept=1</font> is possible but may lead to problems in some situations. With that sysctl value, it is unspecified which listening socket will be selected to handle connections for VRF traffic; ie. either a socket bound to the VRF or an unbound socket may be used to accept new connections from a VRF.

root@test1:~# sysctl -w net.ipv4.tcp_l3mdev_accept=1
root@test1:~# python3 server.py wg0
Server is listening on port 8888
Waiting for a connection...
root@test1:~# sudo python3 server.py 
Traceback (most recent call last):
  File "/root/server.py", line 31, in <module>
    server_socket.bind(server_address)
OSError: [Errno 98] Address already in use
  • 包可以跨域访问到默认 VRF 中的 socket:
root@test1:~# python3 server.py 
Server is listening on port 8888
Waiting for a connection...
Connection from ('10.2.0.10', 39596)
root@test1:~# ss -nltp src :8888
State                       Recv-Q                      Send-Q                                           Local Address:Port                                            Peer Address:Port                      Process                      
LISTEN                      0                           1                                                      0.0.0.0:8888                                                 0.0.0.0:*                          users:(("python3",pid=137564,fd=3))

# ESTA socket 可以看到是属于 vrf-1 域的
ESTAB                   0                       0                                        10.1.0.10%vrf-1:8888                                       10.2.0.10:37948                   users:(("python3",pid=137564,fd=4))

root@test2:~# telnet 10.1.0.10 8888
Trying 10.1.0.10...
Connected to 10.1.0.10.
Escape character is '^]'.
hello
hello
  • 但是不能跨域访问其它的非默认 VRF:
root@test1:~# sudo python3 server.py wg0
Server is listening on port 8888
Waiting for a connection...
root@test1:~# ss -nltp src :8888
State                       Recv-Q                      Send-Q                                           Local Address:Port                                            Peer Address:Port                      Process                      
LISTEN                      0                           1                                                  0.0.0.0%wg0:8888                                                 0.0.0.0:*                          users:(("python3",pid=137583,fd=3))

root@test1:~# telnet localhost 8888
Trying 127.0.0.1...
telnet: Unable to connect to remote host: Connection refused

不过无论如何设置tcp_l3mdev_accept,从 accept创建的 establish socket 都会基于包入向接口所在的 VRF 创建。

附测试程序:

import socket
import sys
import struct

# 解析命令行参数
if len(sys.argv) > 1:
    device = sys.argv[1]
else:
    device = None

# 定义服务器地址和端口
server_address = ('', 8080)

# 创建 TCP 套接字
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 如果指定了网络接口,则进行绑定
if device:
    try:
        # 获取 SO_BINDTODEVICE 选项的值
        SO_BINDTODEVICE = 25
        # 将设备名转换为字节类型
        device_bytes = device.encode() + b'\x00'
        # 设置 SO_BINDTODEVICE 选项
        server_socket.setsockopt(socket.SOL_SOCKET, SO_BINDTODEVICE, device_bytes)
    except OSError as e:
        print(f"Failed to bind to device {device}: {e}")
        sys.exit(1)

# 绑定地址和端口
server_socket.bind(server_address)

# 开始监听
server_socket.listen(1)

print(f"Server is listening on port {server_address[1]}")

while True:
    # 接受客户端连接
    print("Waiting for a connection...")
    connection, client_address = server_socket.accept()
    try:
        print(f"Connection from {client_address}")

        # 接收客户端数据并原样返回
        while True:
            data = connection.recv(1024)
            if data:
                connection.sendall(data)
            else:
                break

    finally:
        # 关闭连接
        connection.close()

VRF 在 Linux 中的实现 (From dog250)#

Linux 4.4 - 4.8#

f453e429c95b2818ebc715ffff42ddb1_MD5

VRF 是基于虚拟网卡实现的,每一个 VRF 域都表现为一个虚拟网卡,然后将具体的物理网卡(或者是别的虚拟网卡)添加到指定的 VRF 网卡,从而实现隔离。

数据包在被物理网卡 ethX 接收后,在 netif_receive_skb 中,会有 rx_handler 回调来截获数据包。VRF 会注册一个 rx_handler 回调,该回调中将 skb 的 dev 字段替换为 VRF 虚拟网卡 Device 对象,这个处理是和 Bridge 处理一致的。接下来,系统依赖以下的事实来实现 VRF 逻辑:

  • 数据包 skb 的 dev 字段已经是 VRF 设备,表明数据包看起来是通过 VRFX 来接收的。
  • 用户显式配置的Policy Routing 的 rule 要求来自 VRFX 接收的数据包要查询X号路由表,如:
root@vm1:~# ip rule add oif vrf-1 table 1
root@vm1:~# ip rule add iif vrf-1 table 1

这就实现了VRF逻辑。

可以看到,用户需要自己来手工完成策略路由表的定向操作,这个配置是重点,没有它的话,便不会让 VRF 网卡接收的数据包查询策略路由表。

这明显是一种很初级但可用(我的做法确实Low,但是就是可用!这是Linux的一种典型的文化)的做法。很自然的,Linux 4.8 内核开 始,VRF 有了第二阶段的实现方法,省略了用户自己手工配置策略路由这个步骤。

Linux 4.8 以后#

在 Linux 4.8 内核以及以后,系统提供了一种更加优雅的做法,即引入了一个叫做 L3mdev(Layer3 master device)的机制,有了这个 L3mdev 机制,便省去了显式配置策略路由的必要。

在创建一个 VRF 虚拟网卡的时候,系统便将其与一个特定的策略路由表自动关联,L3mdev 机制基于这种关联来完成策略路由表的定向操作。这种 L3mdev 机制事实上相当于又一个3层的 Hook,该 Hook 将感兴趣流量伪装成从一个虚拟网卡接收,用该虚拟网卡来实现一些特定于3层的处理逻辑。

b6f99011cbfae45790720393c7c89ed0_MD5

对比一下 4.8 内核之前的处理方法,就知道如今的 L3mdev 有多么高尚了,老的注册 rxhandler 回调的方法如下图:

b77eae239cc961eceb4b96dcd79602ee_MD5

下图是采用新的 L3mdev 机制的 VRF 实现框图:

96f31a6dd78b05b563e168102c8ebb12_MD5

数据路径实现逻辑#

以下(即Linux 4.8 及以后的 VRF 内核实现)来用两个情景分析的方式阐述 VRF 的实现,通过这两个情景分析,基本上 VRF 的实现也就了然于胸了。

收包场景#

从独立的物理网卡,比如 eth0 收到数据包,依次经过网卡驱动程序,netif_receive_skb,ip_rcv 等调用,直到 ip_rcv_finish 被调用之前,VRF 的逻辑和非 VRF 逻辑并没有任何不同支持,在 ip_rcv_finish 中,事情起了变化:

static int ip_rcv_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
    const struct iphdr *iph = ip_hdr(skb);
    struct rtable *rt;
    struct net_device *dev = skb->dev;

    /* if ingress device is enslaved to an L3 master device pass the
     * skb to its handler for processing
     */
    // 这里增加了这么一个L3mdev调用,正是该L3mdev逻辑的处理,实现了VRF的核心:路由表使用与VRF域关联的策略路由表
    skb = l3mdev_ip_rcv(skb);
    // 后面的逻辑直到定位路由表,VRF逻辑和常规逻辑没有任何不同
    ...
    }

具体来讲,I3mdev_ip_rcv 的逻辑非常简单,对于 VRF 而言,实现 I3mdev_I3_rcv 回调函数完成的功能仅仅是:

  • 将 skb 的 dev 字段重新修改为该 dev 的 master 设备,即该物理网卡附着的 VRF 虚拟网卡设备。

我们接下来看下上述的 l3mdev_ip_rcv 里做的关于 skb 的 dev 字段的修改在什么时候会用到。可以想象的是,当然是在定位策略路由表的时候用的咯,问题是,在代码层面这是怎么实现的。

我们跳过中间步骤,直达策略路由表的定位,在定位路由表的时候,实际上是在定位一个 rule,系统会遍历所有的 rules 链表,比较典型的 rules 链表可以通过下面的命令查看:

ip rule ls

一般情况下,我们会看到以下结果:

0:      from all lookup local 
32766:  from all lookup main 
32767:  from all lookup default 

当我们配置了 VRF 域时,看到的是如下结果:

0:      from all lookup local 
1000:   from all lookup [l3mdev-table] 
32766:  from all lookup main 
32767:  from all lookup default  

嗯,多了一个 l3mdev-table,系统在遍历 rules 链表的时候,遇到 [3mdev-table] 表的时候,采用的是一种隐式的处理方式。意思是说,即便你创建了多个 VRF 域,关联了多张策略路由表,系统中依然只能看到一张 [I3mdev-table] 表。

既然这样,系统又是如何定位到与特定的 VRF 域关联的那张路由表呢?比如说,vrf-blue 关联了路由表 10,vrf-red 关联了路由表 20,此时正在处理的包属于 vrf-blue,系统如何定位到要查找路由表 10 而不是路由表 20 呢?

为了了解 [l3mdev-table] 的工作方式,就需要看下 fib_rule_match 的逻辑:

static int fib_rule_match(struct fib_rule *rule, struct fib_rules_ops *ops,
                          struct flowi *fl, int flags,
                          struct fib_lookup_arg *arg)
{
    int ret = 0;

    ...
    // 前面的逻辑是常规的iif,oif,mark等匹配,以下的这个l3mdev不一般!
    // rule->l3mdev即ip rule ls看到的那个[l3mdev-table]
    if (rule->l3mdev && !l3mdev_fib_rule_match(rule->fr_net, fl, arg))
        goto out;

    ret = ops->match(rule, fl, flags);
    out:
    return (rule->flags & FIB_RULE_INVERT) ? !ret : ret;
}

我们重点看下 l3mdev_fib_rule_match:

int l3mdev_fib_rule_match(struct net *net, struct flowi *fl,
              struct fib_lookup_arg *arg)
{
    struct net_device *dev;
    int rc = 0;
 
    rcu_read_lock();
    ... // 我们仅仅分析iif情景
 
    dev = dev_get_by_index_rcu(net, fl->flowi_iif);
    // 如果这个dev是一个VRF master虚拟网卡设备
    if (dev && netif_is_l3_master(dev) &&
        dev->l3mdev_ops->l3mdev_fib_table) {
        // 那么便通过其自身的回调函数取出和该VRF关联的策略路由表!
        arg->table = dev->l3mdev_ops->l3mdev_fib_table(dev);
        rc = 1;
        goto out;
    }
 
out:
    rcu_read_unlock();
 
    return rc;
}

到此为止呢,我们的一幅图景就闭合了:

  • 首先在 ip_rcv_finish 的 l3mdev_ip_rcv 调用中定位到与收包物理网卡关联的 VRF master 虚拟网卡;
  • 然后在 fib_lookup 内层的 l3mdev_fib_rule_match 调用中取出与 VRFmaster 虚拟网卡关联的策略路由表;
  • 最终在该特定的路由表中进行路由查找。

本地始发包场景#

由于本地的数据包全部来自于某个 socket 而不是网卡,socket 是不和网卡关联的,所以 IP 层在查路由之前无法知道一个数据包和哪个网卡关联,就更别提去选择使用哪张路由表了…

因此,如若一个 socket 程序想使用 VRF 机制,就必须为一个 socket 去绑定一个特定的网卡:

setsockopt(sd, SOL_SOCKET, SO_BINDToDEVICE, vrf_dev, strlen(vrf_dev)+1);

有了这个调用,在 ip_queue_xmit 中查找路由的时候,自然就会在 fib_rule_match 中定位到与参数 vrf_dev 关联的策略路由表了。然而,如果 socket 程序并不知情,它只是 bind 了一个隶属于该 vrf_dev 的 slave 物理网卡,比如 eth0,那要怎么处理呢?

显然 SO_BINDTODEVICE 参数会为数据包在 __ip_route_output_key_hash 调用中的路由查找失败而负责,为其暂时绑定一个 dummy dst_entry,从而逻辑可以到达 __ip_local_out:

int __ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb)
{
    struct iphdr *iph = ip_hdr(skb);
 
    iph->tot_len = htons(skb->len);
    ip_send_check(iph);
 
    /* if egress device is enslaved to an L3 master device pass the
     * skb to its handler for processing
     */
    skb = l3mdev_ip_out(sk, skb);
    ...
}

和网卡收包过程的 l3mdev 调用一样,我们看下这个与 l3mdev_ip_rcv 相对的 l3mdev_ip_out 调用里做了什么文章,实现对应回调函数的是 vrf_ip_out:

static struct sk_buff *vrf_ip_out(struct net_device *vrf_dev,
                  struct sock *sk,
                  struct sk_buff *skb)
{
    struct net_vrf *vrf = netdev_priv(vrf_dev);
    ...
    // 取出VRF关联的那个唯一的dst_entry,以便将数据包定向到vrf_xmit
    rth = rcu_dereference(vrf->rth);
    if (likely(rth)) {
        dst = &rth->dst;
        dst_hold(dst);
    }
    ...
    skb_dst_set(skb, dst);
 
    return skb;
}

很简单,在这里仅仅是将 skb 的那个 dummy dst_entry 换成了和 VRF 设备绑定的那个 dst_entry。

事实上,与 VRF 设备绑定的 dst_entry 只有一个且只做一件事,那就是,调用 VRF 设备的 dev_hard_xmit 回调函数,VRF 机制正是在该 dev_hard_xmit 回调函数中实现了真正的路由查询,在 dev_hard_xmit 回调中真正做事的函数是 vrf_process_v4_outbound

static netdev_tx_t vrf_process_v4_outbound(struct sk_buff *skb,
                       struct net_device *vrf_dev)
{
    struct iphdr *ip4h = ip_hdr(skb);
    int ret = NET_XMIT_DROP;
    struct flowi4 fl4 = {
        /* needed to match OIF rule */
        // 这个会让l3mdev_fib_rule_match定位到正确的路由表
        .flowi4_oif = vrf_dev->ifindex,
        .flowi4_iif = LOOPBACK_IFINDEX,
        .flowi4_tos = RT_TOS(ip4h->tos),
        .flowi4_flags = FLOWI_FLAG_ANYSRC | FLOWI_FLAG_SKIP_NH_OIF,
        .daddr = ip4h->daddr,
    };
    struct net *net = dev_net(vrf_dev);
    struct rtable *rt;
    // 真实查路由
    rt = ip_route_output_flow(net, &fl4, NULL);
    ...
    // 真实发送skb
    ret = vrf_ip_local_out(dev_net(skb_dst(skb)->dev), skb->sk, skb);
    ...
}

整个流程就结束了。

总结#

73bdcf83f5f9df9658b5e59dc119aa76_MD5

问题#

  1. VRF 设备的 mtu 为什么是 65575?
  2. 在添加完静态路由后,为什么路由表中多了一条 169.254.0.0/16 的路由?
169.254.0.0/16 dev vrf-1 scope link metric 1000 

Reference#

  1. https://docs.kernel.org/networking/vrf.html
  2. https://www.dasblinkenlichten.com/working-with-linux-vrfs/
  3. https://blog.csdn.net/weixin_38387929/article/details/111594909
  4. https://github.com/Mellanox/mlxsw/wiki/Virtual-Routing-and-Forwarding-%28VRF%29