折腾 VPP 上的 SRv6 路由

经验

简介

之前在做的东西又不打算用 openflow 了,准备换到 SRv6 上面做。没有用现成的路由器产品,用服务器跑 VPP + FRR 当软路由。 把过程中的经验总结一下。

SRv6

Segment Routing (SR) 是最近几年比较热门的一种源路由架构。把路径分成不同的段,每一段有自己的标识(SID), 在入口节点处把路径对应的 SID 按顺序全放到报头,后续只需要根据报头中的路径转发。数据平面上可以选择 MPLS(SR-MPLS),也可以选择 IPv6 (SRv6)。前者相对比较普及一些;后者比较新一些,给 IPv6 扩展了一个报头 SRH,SID 可以用 IPv6 地址表示,报头里用一个指针指向下一个 SID。好处是普通设备可以无视 SRH 进行转发,与现有 IPv6 网兼容。

VPP

Vector Packet Processing (VPP)是思科开源的一套框架,提供了交换机或路由器的功能,也就是可以作为一个软件的数据平面来处理数据包。

据我所知 SRv6 的开源实现主要是 linux 内核还有 VPP。而 VPP 在底层用到了 DPDK,性能上肯定更有优势,这篇论文 的跑分表示在 SRv6 上的转发能力大致是内核的 8 倍。所以选择用 VPP 作为数据平面,但是只是当作一个工具用,没涉及到底层的代码实现。 后续测试用多台vpp软路由器搭好的试验拓扑在万兆网卡上用 SRv6 转发,iperf3 打满,端到端速率能到 6Gbps。

过程

启用 VPP

Ubuntu 安装 VPP 二进制包还是很简单的,按照 官网指示 就可以。 安装好之后就可以像大多数软件一样用 systemctl 启动 vpp 服务。

这里后续想增加动态路由的时候还是用到了源码编译,方法见 下文

接管网卡

首先用 lshw -class network -businfo 可以查看设备上所有的网卡以及后续需要的 businfo,编辑 vpp 默认的配置文件 /etc/vpp/startup.confdpdk 相关的配置里按照注释里那样将要接管的网卡对应的名字和那个编号写上。重启 VPP 发现再运行那个命令设备名不见了,而在 vpp 的命令行 vppctl 出现了配置的网卡,这样这张卡就可以被 vpp 用了。

后续 会介绍被接管的卡怎么与内核通信来实现更多的功能。

常用命令

vpp 开启后,通过 sudo vppctl 可以进到命令行(这里只开了一个实例,可以开启多个使用不同的配置文件,用 -c 配置文件 来进入不同的实例)。

常用的命令就是开关网卡,配 IP 等等,可以把需要的命令写到一个文件里,在配置文件的 startup-config里指明文件路径,每次启动都会先去执行这些命令。 注意如果里面有一条命令出错了,vpp能启动但后面的命令不会被执行。

show interface                       显示所有网卡还有各种数据
show ip/ip6 fib                      显示路由表
show class table verbose             显示ACL表详细信息
set interface state eth0 up          开启 eth0
set interface ip address eth0 ip/len 配置 eth0 的 ip 和掩码
?                                    帮助信息,可以输一半命令加一个问号显示特定命令的帮助

vpp 官方文档,选版本进入,不同版本的命令还是有一定差别。

SR 相关命令

与 SR 关系比较大的命令主要是这几条:

sr localsid address {bsid} behavior end                  配置为 end 节点,普通路过节点
sr localsid address {bsid} behavior end.dx4 {intf} {ip4} 配置为 end.dx4,去 ip6 头转发给下一跳 ip4
sr policy add bsid {bsid} next AAA next BBB encap        配置一个 policy,路过的段 [AAA, BBB]
set sr encaps source addr {ip6}                          给被封装的包一个源地址
sr steer l3 {ip_prefix} via bsid {bsid}                  将特定ip前缀的包引到这个 policy

vpp 官方文档介绍 SRv6, 我这里用的是 20.01 版本。

一些经验

我这里的经验是指用 VPP 的 SRv6 功能封装普通 IPv4 包,控制用不同 SR 策略转发。只是一个用服务器当软路由器搭建的试验环境,并不是用在真实的 SD-WAN 环境中。

结合 ACL 进行 SR 引流

SR 相关命令 介绍了通过 ip 前缀将流量引向一个 SR Policy,而用过 openflow 的会记得流表里用很多可以匹配的字段的组合来执行不同的策略。 想实现类似的功能要用到 vpp 的 ACL 表功能。

  1. 首先,需要跑一个脚本设置 ACL 的下一个节点是 SR,这个没法用命令行配置,只能用 API
#!/usr/bin/env python
import os
import time
import fnmatch
from vpp_papi import VPP

CLIENT_ID = "Vppclient"
VPP_JSON_DIR = "/usr/share/vpp/api/core"
VPP_JSON_PLUGIN_DIR = "/usr/share/vpp/api/plugins"
API_FILE_SUFFIX = "*.api.json"
TIME_ITV = 20

def load_json_api_files(json_dir=VPP_JSON_DIR, suffix=API_FILE_SUFFIX):
    jsonfiles = []
    for root, dirnames, filenames in os.walk(json_dir):
        for filename in fnmatch.filter(filenames, suffix):
            jsonfiles.append(os.path.join(json_dir, filename))
    if not jsonfiles:
        print("Error: no json api files found")
        exit(-1)
    return jsonfiles


def connect_vpp(jsonfiles):
    vpp = VPP(jsonfiles)
    r = vpp.connect(CLIENT_ID)
    print("VPP api opened with code: %s" % r)
    return vpp

jsonfiles = load_json_api_files()

vpp = connect_vpp(jsonfiles)
vpp.api.add_node_next(node_name='ip4-inacl', next_name='sr-pl-rewrite-encaps-v4')
  1. 配置 ACL 表,分两步:根据 mask 创建一个表,通过匹配值应用到特定流量

配置示例:

# 对源端口 1935 的 TCP 流量 使用索引为 1 的 SR 策略
sr policy add bsid fc00:1::999:bb next fc00:2::1a encap
classify table mask hex 0000000000000000000000000000f00000000000000000ff00000000000000000000ffff000000000000000000000000
classify session acl-hit-next 1 table-index 0 match hex 00000000000000000000000000004000000000000000000600000000000000000000078f000000000000000000000000 action set-sr-policy-index 1
set interface input acl intfc TenGigabitEthernet85/0/0 ip4-table 0

这里的一个坑是虽然 vpp 提供了比较用户友好的 ACL 表编写方式,只写出特定字段自己生成 mask,但在涉及到第四层的端口这个字段时命令解析会出问题,一旦加上端口命令就执行不了。

所以这里采用原始的 mask 匹配的方式,上面的一长串数字表示从以太网头开始的 IPv4 包头,需要自己标记出特定字段和对应的值,就可以以一种很麻烦的方式实现类似 openflow 里流表匹配的功能。

写了两个相对简化这个过程的函数,匹配一些常用的字段:

import socket
import struct
from ipaddress import IPv4Network


def gen_mask(src_mac=0, dst_mac=0, ip_ver=0, ip_proto=0, src_ip:IPv4Network=None, dst_ip:IPv4Network=None, src_port=0, dst_port=0):
    if src_mac: src_mac = 0xffffffffffff
    if dst_mac: dst_mac = 0xffffffffffff
    if ip_ver: ip_ver = 0xf
    if ip_proto: ip_proto = 0xff
    if src_ip: src_ip = int(src_ip.netmask)
    if dst_ip: dst_ip = int(dst_ip.netmask)
    if src_port: src_port = 0xffff
    if dst_port: dst_port = 0xffff
    mask = f"{src_mac:012x}{dst_mac:012x}0000{ip_ver:01x}00000000000000000{ip_proto:02x}0000{src_ip:08x}{dst_ip:08x}{src_port:04x}{dst_port:04x}00000000000000000000"
    return mask


def gen_match(src_mac=0, dst_mac=0, ip_ver=0, ip_proto=0, src_ip:IPv4Network=None, dst_ip:IPv4Network=None, src_port=0, dst_port=0):
    src_ip = int(src_ip.network_address) if src_ip else 0
    dst_ip = int(dst_ip.network_address) if dst_ip else 0
    match = f"{src_mac:012x}{dst_mac:012x}0000{ip_ver:01x}00000000000000000{ip_proto:02x}0000{src_ip:08x}{dst_ip:08x}{src_port:04x}{dst_port:04x}00000000000000000000"
    return match

端口镜像到内核

这里用的是结合内核的 veth 功能和 vpp 的 host-interface 功能实现的端口镜像

# linux 网络配置,执行完这些再去 vpp 执行后面的
# 创建了一个叫 ns0 的 netns,建立了一对 veth
ip netns add ns0
ip link add span0 type veth peer name vethns0
ip link set vethns0 netns ns0
ip netns exec ns0 ip link set lo up
ip netns exec ns0 ip link set vethns0 up

# 以下是 vpp 命令
# vpp 配置 host-interface
set int l2 bridge spanport 1
set int state spanport up
create host-interface name span0
set interface state host-span0 up
set interface l2 bridge host-span0 1

# vpp 配置端口镜像
set interface span eth0 destination host-span0
set interface span eth0 l2 destination host-span0

# 之后可以用类似 tcpdump 这样的东西直接从内核的 span0 接口抓到 vpp 里 eth0 的包

模拟链路时延等参数

linux 内核可以用 netem 来模拟链路的时延,限制带宽什么的。看 VPP 的文档里也有一个叫做 netsim 的东西, 然而发现文档说只支持两个自己的口交叉连接来配置。

后来看命令的帮助发现 vpp 确实实现了只配置自己一个网口的类似 netem 的功能,但好像还没写到文档里。

set nsim delay {delay} ms bandwidth {bw} gbit drop-fraction {loss-rate}

nsim output-feature enable-disable {网卡名} 这是开
nsim output-feature enable-disable {网卡名} disable 这是关

不过也有问题,发现一旦 vpp 配置开了多线程,用这项功能时 vpp 会直接挂掉。

连接 ODL 控制器

曾经尝试过用 vpp 连接 ODL 控制器,需要借助同个组织开发的叫 honeycomb 的软件作为 netconf server, 用 ODL 作为 netconf client 连接路由器。连接后就可以用它的 RESTCONF 接口,跟 ODL 本身的阴阳那一套一样。 我之前都是用 ONOS 对 ODL 并不熟悉,所以直接放弃学了。

后来发现 honeycomb 很久没有更新,跟不上现在的版本了,而且提供的 RESTCONF 接口非常复杂,json 一层套一层, 没找到现成的封装好的好用工具,就直接放弃了 ODL 控制器,自己写程序手动 SSH 到路由器执行命令。也能实现想要的功能,但很难看。 期待以后能像用 openflow 一样在控制器上面很方便地写 SR Policy 相关的程序,或许是调研不够这样的软件我还没找到。

结合 FRRouting 进行动态路由

这个过程相对比较困难,直到最后还是有一些问题没解决,用很难看的方式掩盖了一下。而且相应软件失去了维护,之后也很难解决。

这里用的是之前的一个叫 vppsb 的项目来实现与内核的动态路由协议交互。vppsb 在 19.xx 之后就不能用了,还好有人给出了修改的方式。 按照这个地方给出的脚本来编译源码,就可以得到一份支持 vppsb 的 20.01 版本的 vpp。 生成 deb 包后安装即可。

执行 enable tap-inject 命令,会将 vpp 内的网卡在内核里全部创建一个对应的网卡,在外面就像对待普通网卡一样配置 ip 地址,安装和配置 内核里的 frr 即可,然后 vpp 就啪的一下支持动态路由了。

本该是这样的。然而经过测试,在纯 IPv4 转发的环境下一切运行良好,如果是在 IPv6 转发时,开启 tap-inject 以及 frr 的 IS-IS 协议后, VPP 会变得无法进行正常的邻居发现,从而把包全部丢弃了。只有手动加上邻居的信息后才能一切正常。

也想尝试网上说的用 netns 的方法与 frr 交互,但是没成功。

更新 2022-08

新版本的 VPP 好像加入了一个新的与 linux 内核进行路由表交互的插件 linux-cp (linux control plane), 项目结束后我没有再关注这方面内容,详细信息可参考插件作者的系列介绍文章.

感想

感觉体验很差,文档和资料非常少且更新不够及时,不看代码的情况下很多功能需要进行多次尝试才理解怎么用。 这是没有涉及代码只是作为软件来使用的感受,如果要让我自己看代码二次开发恐怕更头疼,不过我目前还涉及不到。 感觉大厂里面应该会有这方面经验比较丰富的大佬,如果有人带着会好一些。

See Also