---
date: "2021-08-12T11:17:54Z"
title: VPP Linux CP - Part1
aliases:
- /s/articles/2021/08/12/vpp-1.html
---


{{< image width="200px" float="right" src="/assets/vpp/fdio-color.svg" alt="VPP" >}}

# About this series

Ever since I first saw VPP - the Vector Packet Processor - I have been deeply impressed with its
performance and versatility. For those of us who have used Cisco IOS/XR devices, like the classic
_ASR_ (aggregation services router), VPP will look and feel quite familiar as many of the approaches
are shared between the two. One thing notably missing, is the higher level control plane, that is
to say: there is no OSPF or ISIS, BGP, LDP and the like. This series of posts details my work on a
VPP _plugin_ which is called the **Linux Control Plane**, or LCP for short, which creates Linux network
devices that mirror their VPP dataplane counterpart. IPv4 and IPv6 traffic, and associated protocols
like ARP and IPv6 Neighbor Discovery can now be handled by Linux, while the heavy lifting of packet
forwarding is done by the VPP dataplane. Or, said another way: this plugin will allow Linux to use
VPP as a software ASIC for fast forwarding, filtering, NAT, and so on, while keeping control of the
interface state (links, addresses and routes) itself. When the plugin is completed, running software
like [FRR](https://frrouting.org/) or [Bird](https://bird.network.cz/) on top of VPP and achieving
&gt;100Mpps and &gt;100Gbps forwarding rates will be well in reach!

In this first post, let's take a look at tablestakes: making a copy of VPP's interfaces appear in
the Linux kernel.

## My test setup

I took two AMD64 machines, each with 32GB of memory and one Intel X710-DA4 network card (which offers
four SFP+ cages), and installed Ubuntu 20.04 on them. I connected each of the network ports back
to back with DAC cables. This gives me plenty of interfaces to play with. On the vanilla Ubuntu machine,
I created a bunch of different types of interfaces and configured IPv4 and IPv6 addresses on them.

The goal of this post is to show what code needed to be written and which changes needed to
be made to the plugin, in order to mirror each type of interface from VPP into a valid Linux interface.
As we'll see, marrying the Linux network interface approach with the VPP interface approach can be
tricky! Throughout this post, the vanilla Ubuntu machine will keep the following configuration, the
config file of which you can see in the Appendix:

| Name            | type | Addresses
|-----------------|------|----------
| enp66s0f0       | untagged   | 10.0.1.2/30 2001:db8:0:1::2/64
| enp66s0f0.q     | dot1q 1234 | 10.0.2.2/30 2001:db8:0:2::2/64
| enp66s0f0.qinq  | outer dot1q 1234, inner dot1q 1000 | 10.0.3.2/30 2001:db8:0:3::2/64
| enp66s0f0.ad    | dot1ad 2345 | 10.0.4.2/30 2001:db8:0:4::2/64
| enp66s0f0.qinad | outer dot1ad 2345, inner dot1q 1000 | 10.0.5.2/30 2001:db8:0:5::2/64

This configuration will allow me to ensure that all common types of sub-interface are supported
by the plugin.

### Startingpoint

The `linux-cp` plugin that ships with VPP 21.06, when initialized with the desired startup config
(see Appendix), will yield this (Hippo is the machine that runs my development branch of VPP, it's
called like that because it's always hungry for packets):

```

pim@hippo:~/src/lcpng$ ip ro
default via 194.1.163.65 dev enp6s0 proto static
10.0.1.0/30 dev e0 proto kernel scope link src 10.0.1.1
10.0.2.0/30 dev e0.1234 proto kernel scope link src 10.0.2.1
10.0.4.0/30 dev e0.1236 proto kernel scope link src 10.0.4.1
194.1.163.64/27 dev enp6s0 proto kernel scope link src 194.1.163.88

pim@hippo:~/src/lcpng$ fping 10.0.1.2 10.0.2.2 10.0.3.2 10.0.4.2 10.0.5.2
10.0.1.2 is alive
10.0.2.2 is alive
10.0.3.2 is unreachable
10.0.4.2 is unreachable
10.0.5.2 is unreachable

pim@hippo:~/src/lcpng$ fping6 2001:db8:0:1::2 2001:db8:0:2::2 \
  2001:db8:0:3::2 2001:db8:0:4::2 2001:db8:0:5::2
2001:db8:0:1::2 is alive
2001:db8:0:2::2 is alive
2001:db8:0:3::2 is unreachable
2001:db8:0:4::2 is unreachable
2001:db8:0:5::2 is unreachable
```

Yikes! So the plugin really only knows how to handle untagged interfaces, and sub-interfaces with
one dot1q VLAN tag. The other three scenarios (dot1ad VLAN tag; dot1q in dot1q; and dot1q in dot1ad)
are not ok. And, curiously, the `dot1ad 2345 exact-match` interface _was_ created (as linux interface
`e0.1236`, but it doesn't ping, and I'll show you why :-) But principally: let's fix this plugin!


### Anatomy of Linux Interface Pairs

In VPP, the plumbing to the Linux kernel is done via a TUN/TAP interface. For L3 interfaces, TAP is
used. This TAP appears in the Linux network namesapce as a device with which you can interact. From
the Linux point of view, on egress, all packets coming from the host into the TAP are cross-connected
directly to the logical VPP network interface. In VPP, on ingress, packets destined for an L3 address
on any VPP interface, as well as packets that are multicast, are punted into the TAP, which makes
them appear in the kernel.

In VPP, a linux interface pair (`LIP` for short) is therefore a tuple `{ vpp_phy_idx, vpp_tap_idx, netlink_idx }`.
Creating one of these, is the art of first creating a tap, and associating it with the `vpp_phy`,
copying traffic from it into the dataplane, and punting traffic from the dataplane into the TAP
so that Linux can see it. The plugin exposes an API endpoint that creates, deletes and lists
these linux interface pairs:
```
lcp create <sw_if_index>|<if-name> host-if <host-if-name> netns <namespace> [tun]
lcp delete <sw_if_index>|<if-name>
show lcp [phy <interface>]
```

If you're still with me, congratulations, because this is where it starts to get fun!

### Create interface: physical

The easiest interface type is a physical one. Here, the plugin will create a TAP, copy the MAC
address from the PHY, and set a bunch of attributes on the TAP, such as MTU and link state.
Here, I made my first set of changes (in [[patchset 3](https://gerrit.fd.io/r/c/vpp/+/33481/2..3)])
to the plugin:

*   Initialize the link state of the VPP interface, not unconditionally set it to 'down'.
*   Initialize the MTU of the VPP interface into the TAP, do not assume it is the VPP default
    of 9000; if the MTU is not known, assume the TAP has 9216, the largest possible on ethernet.

Taking a look:
```
DBGvpp# show int TenGigabitEthernet3/0/0
              Name               Idx    State  MTU (L3/IP4/IP6/MPLS)     Counter          Count
TenGigabitEthernet3/0/0           1     down         9000/0/0/0
DBGvpp# lcp create TenGigabitEthernet3/0/0 host-if e0
DBGvpp# show tap tap1
Interface: tap1 (ifindex 7)
  name "e0"
  host-ns "(nil)"
  host-mtu-size "9000"
  host-mac-addr: 68:05:ca:32:46:14
...

DBGvpp# set interface state TenGigabitEthernet3/0/1 up
DBGvpp# set interface mtu packet 1500 TenGigabitEthernet3/0/1
DBGvpp# lcp create TenGigabitEthernet3/0/1 host-if e1
```

And in Linux, unceremoniously, both interfaces appear:
```
pim@hippo:~/src/lcpng$ ip link show e0
291: e0: <BROADCAST,MULTICAST> mtu 9000 qdisc mq state DOWN mode DEFAULT group default qlen 1000
    link/ether 68:05:ca:32:46:14 brd ff:ff:ff:ff:ff:ff
pim@hippo:~/src/lcpng$ ip link show e1
307: e1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UNKNOWN mode DEFAULT group default qlen 1000
    link/ether 68:05:ca:32:46:15 brd ff:ff:ff:ff:ff:ff
```

The MAC address from the physical interface `show hardware-interface TenGigabitEthernet3/0/0`
corresponds to the one seen in the TAP, and the one seen in the Linux interface we just created.
The Linux interfaces respect the MTU and link state of their counterpart VPP interfaces (`e0` is
down at 9000b, `e1` is up at 1500b).

### Create interface: dot1q

Note that creating an ethernet sub-interface in VPP takes the following form:
```
create sub-interfaces <interface> {<subId> [default|untagged]} | {<subId>-<subId>}
  | {<subId> dot1q|dot1ad <vlanId>|any [inner-dot1q <vlanId>|any] [exact-match]}
```

Here, I'll start with the simplest form, canonically called a .1q VLAN or a _tagged_ interface.
The plugin handles it just fine, with a codepath that first creates a sub-interface on the parent's
TAP, forwards traffic to/from the VPP subinterface into the parent TAP, asks the Linux kernel to
create a new interface of type vlan with the `id` set to the dot1q tag, as a child of the `e0`
interface. Note however the `exact-match` keyword, which is very important. In VPP, without setting
exact-match, any ethernet frame that matches the sub-interface expression, will be handled by it.
This means the VLAN with tag 1234, but also a stacked (Q-in-Q or Q-in-AD) VLAN with the outer tag
set to 1234 will match. This is non-sensical for an IP interface, and as such the first two examples
will successfully create, but the third example will crash the plugin:

```
## Good, shorthand sets exact-match
DBGvpp# create sub TenGigabitEthernet3/0/0 1234
DBGvpp# lcp create TenGigabitEthernet3/0/0.1234 host-if e0.1234

## Good, explicitly set exact-match
DBGvpp# create sub TenGigabitEthernet3/0/0 1234 dot1q 1234 exact-match
DBGvpp# lcp create TenGigabitEthernet3/0/0.1234 host-if e0.1234

## Bad, will crash
DBGvpp# create sub TenGigabitEthernet3/0/0 1234 dot1q 1234
DBGvpp# lcp create TenGigabitEthernet3/0/0.1234 host-if e0.1234
```

The reason is that the first call is a shorthand: it creates sub-int 1234 as `dot1q 1234 exact-match`,
which is literally what the second example does, while the third example creates a non-exact-match sub-int
1234 with `dot1q 1234`. So I changed the behavior to explicitly reject sub-interfaces that are not exact-match
in [[patchset 4](https://gerrit.fd.io/r/c/vpp/+/33481/3..4)]. Actually, it turns out that VPP upstream also
crashes on setting an ip address on a sub-int that is not configured with `exact-match`, so I fixed that
upstream in this [[gerrit](https://gerrit.fd.io/r/c/vpp/+/33444)] too.

### Create interface: dot1ad

While by far 802.1q _VLAN_ interfaces are the most used, there's a lesser known sibling called
802.1ad -- the only difference is that VLAN ethernet frames with .1q use the well known 0x8100
ethernet type (called a a tag protocol identifier, or [TPID](https://en.wikipedia.org/wiki/IEEE_802.1Q)), while .1ad uses a
lesser known 0x88a8 type. In the first beginnings, Q-in-Q was suggested to use the 0x88a8 tag
for the outer type, and 0x8100 for the inner type, differentiating the two. But the industry
was conflicted, and many vendors chose to use 0x8100 for both inner- and outer-types, VPP supports
it and so does Linux, so let's implement it in [[patchset 5](https://gerrit.fd.io/r/c/vpp/+/33481/4..5)].
Without this change, the plugin would create the interface, but it would invariably create it as
.1q on the linux side, which explains why the `e0.1236` interface exists but doesn't ping in
my _startingpoint_ above. Now we have the expected behavior:

```
DBGvpp# create sub TenGigabitEthernet3/0/0 1236 dot1ad 2345 exact-match
DBGvpp# lcp create TenGigabitEthernet3/0/0.1236 host-if e0.1236

pim@hippo:~/src/lcpng$ ping 10.0.4.2
PING 10.0.4.2 (10.0.4.2) 56(84) bytes of data.
64 bytes from 10.0.4.2: icmp_seq=1 ttl=64 time=0.58 ms
64 bytes from 10.0.4.2: icmp_seq=2 ttl=64 time=0.57 ms
64 bytes from 10.0.4.2: icmp_seq=3 ttl=64 time=0.62 ms
64 bytes from 10.0.4.2: icmp_seq=4 ttl=64 time=0.67 ms
^C
--- 10.0.4.2 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3005ms
rtt min/avg/max/mdev = 0.566/0.608/0.672/0.041 ms
```

### Create interface: dot1q in dot1ad

This is the original Q-in-Q as it was intended. Frames here carry an outer ethernet TPID of 0x88a8
(dot1ad) which is followed by an inner ethernet TPID of 0x8100 (dot1q). Of course, untagged inner
frames are also possible - they show up as simply one ethernet TPID of dot1ad followed directly by
the L3 payload. Here, things get a bit more tricky. On the VPP side, we can simply create the
sub-interface directly; but on the Linux side, we cannot do that. This is because in VPP,
all sub-interfaces are directly parented by their physical interface, while in Linux, the
interfaces are stacked on one another. Compare:

```
### VPP idiomatic q-in-ad (1 interface)
DBGvpp# create sub TenGigabitEthernet3/0/0 1237 dot1ad 2345 inner-dot1q 1000 exact-match

### Linux idiomatic q-in-ad stack (2 interfaces)
ip link add link e0 name e0.2345 type vlan id 2345 proto 802.1ad
ip link add link e0.2345 name e0.2345.1000 type vlan id 1000 proto 802.1q
```

So in order to create Q-in-AD sub-interfaces, for Linux their intermediary parent must exist, while
in VPP this is not necessary. I have to make a compromise, so I'll be a bit more explicit and allow
this type of _LIP_ to be created only under these conditions:
*   A sub-int exists with the intermediary (in this case, `dot1ad 2345 exact-match`)
*   That sub-int itself has a _LIP_, with a Linux interface device that we can spawn the inner interface off of

If these conditions don't hold, I reject the request. If they do, I create an interface pair:
```
DBGvpp# create sub TenGigabitEthernet3/0/0 1236 dot1ad 2345 exact-match
DBGvpp# create sub TenGigabitEthernet3/0/0 1237 dot1ad 2345 inner-dot1q 1000 exact-match
DBGvpp# lcp create TenGigabitEthernet3/0/0.1236 host-if e0.1236
DBGvpp# lcp create TenGigabitEthernet3/0/0.1237 host-if e0.1237

pim@hippo:~/src/lcpng$ ip link show e0.1236
375: e0.1236@e0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9000 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 68:05:ca:32:46:14 brd ff:ff:ff:ff:ff:ff
pim@hippo:~/src/lcpng$ ip link show e0.1237
376: e0.1237@e0.1236: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9000 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 68:05:ca:32:46:14 brd ff:ff:ff:ff:ff:ff
```

Here, `e0.1237` was indeed created as a child of `e0.1236`, which in turn was created as a child of
`e0`.

The code for this is in [[patchset 6](https://gerrit.fd.io/r/c/vpp/+/33481/5..6)].

### Create interface: dot1q in dot1q

Given the change above, this is an entirely obvious capability that the plugin now handles, but I did
find a failure mode, when I tried to create a _LIP_ for a sub-interface when there are no _LIPs_ created.
It causes a NULL deref when trying to look up the _LIP_ of the parent (which doesn't yet have a _LIP_
defined). I fixed that in this [[patchset 7](https://gerrit.fd.io/r/c/vpp/+/33481/6..7)].

## Results

After applying the configuration to VPP (in Appendix), here's the results:

```
pim@hippo:~/src/lcpng$ ip ro
default via 194.1.163.65 dev enp6s0 proto static
10.0.1.0/30 dev e0 proto kernel scope link src 10.0.1.1
10.0.2.0/30 dev e0.1234 proto kernel scope link src 10.0.2.1
10.0.3.0/30 dev e0.1235 proto kernel scope link src 10.0.3.1
10.0.4.0/30 dev e0.1236 proto kernel scope link src 10.0.4.1
10.0.5.0/30 dev e0.1237 proto kernel scope link src 10.0.5.1
194.1.163.64/27 dev enp6s0 proto kernel scope link src 194.1.163.88

pim@hippo:~/src/lcpng$ fping 10.0.1.2 10.0.2.2 10.0.3.2 10.0.4.2 10.0.5.2
10.0.1.2 is alive
10.0.2.2 is alive
10.0.3.2 is alive
10.0.4.2 is alive
10.0.5.2 is alive

pim@hippo:~/src/lcpng$ fping6 2001:db8:0:1::2 2001:db8:0:2::2 \
  2001:db8:0:3::2 2001:db8:0:4::2 2001:db8:0:5::2
2001:db8:0:1::2 is alive
2001:db8:0:2::2 is alive
2001:db8:0:3::2 is alive
2001:db8:0:4::2 is alive
2001:db8:0:5::2 is alive

```

As can be seen, all interface types ping. Mirroring interfaces from VPP to Linux is now done!

We still have to manually copy the configuration (like link states, MTU changes, IP addresses and
routes) from VPP into Linux, and of course it would be great if we could mirror those states also
into Linux, and this is exactly the topic of my next post.


## Credits

I'd like to make clear that the Linux CP plugin is a great collaboration between several great folks
and that my work stands on their shoulders. I've had a little bit of help along the way from Neale
Ranns, Matthew Smith and Jon Loeliger, and I'd like to thank them for their work!

## Appendix

#### Ubuntu config
```
# Untagged interface
ip addr add 10.0.1.2/30 dev enp66s0f0
ip addr add 2001:db8:0:1::2/64 dev enp66s0f0
ip link set enp66s0f0 up mtu 9000

# Single 802.1q tag 1234
ip link add link enp66s0f0 name enp66s0f0.q type vlan id 1234
ip link set enp66s0f0.q up mtu 9000
ip addr add 10.0.2.2/30 dev enp66s0f0.q
ip addr add 2001:db8:0:2::2/64 dev enp66s0f0.q

# Double 802.1q tag 1234 inner-tag 1000
ip link add link enp66s0f0.q name enp66s0f0.qinq type vlan id 1000
ip link set enp66s0f0.qinq up mtu 9000
ip addr add 10.0.3.3/30 dev enp66s0f0.qinq
ip addr add 2001:db8:0:3::2/64 dev enp66s0f0.qinq

# Single 802.1ad tag 2345
ip link add link enp66s0f0 name enp66s0f0.ad type vlan id 2345 proto 802.1ad
ip link set enp66s0f0.ad up mtu 9000
ip addr add 10.0.4.2/30 dev enp66s0f0.ad
ip addr add 2001:db8:0:4::2/64 dev enp66s0f0.ad

# Double 802.1ad tag 2345 inner-tag 1000
ip link add link enp66s0f0.ad name enp66s0f0.qinad type vlan id 1000 proto 802.1q
ip link set enp66s0f0.qinad up mtu 9000
ip addr add 10.0.5.2/30 dev enp66s0f0.qinad
ip addr add 2001:db8:0:5::2/64 dev enp66s0f0.qinad
```

#### VPP config
```
vppctl set interface state TenGigabitEthernet3/0/0 up
vppctl set interface mtu packet 9000 TenGigabitEthernet3/0/0
vppctl set interface ip address TenGigabitEthernet3/0/0 10.0.1.1/30
vppctl set interface ip address TenGigabitEthernet3/0/0 2001:db8:0:1::1/64
vppctl lcp create TenGigabitEthernet3/0/0 host-if e0
ip link set e0 up mtu 9000
ip addr add 10.0.1.1/30 dev e0
ip addr add 2001:db8:0:1::1/64 dev e0

vppctl create sub TenGigabitEthernet3/0/0 1234
vppctl set interface state TenGigabitEthernet3/0/0.1234 up
vppctl set interface mtu packet 9000 TenGigabitEthernet3/0/0.1234
vppctl set interface ip address TenGigabitEthernet3/0/0.1234 10.0.2.1/30
vppctl set interface ip address TenGigabitEthernet3/0/0.1234 2001:db8:0:2::1/64
vppctl lcp create TenGigabitEthernet3/0/0.1234 host-if e0.1234
ip link set e0.1234 up mtu 9000
ip addr add 10.0.2.1/30 dev e0.1234
ip addr add 2001:db8:0:2::1/64 dev e0.1234

vppctl create sub TenGigabitEthernet3/0/0 1235 dot1q 1234 inner-dot1q 1000 exact-match
vppctl set interface state TenGigabitEthernet3/0/0.1235 up
vppctl set interface mtu packet 9000 TenGigabitEthernet3/0/0.1235
vppctl set interface ip address TenGigabitEthernet3/0/0.1235 10.0.3.1/30
vppctl set interface ip address TenGigabitEthernet3/0/0.1235 2001:db8:0:3::1/64
vppctl lcp create TenGigabitEthernet3/0/0.1235 host-if e0.1235
ip link set e0.1235 up mtu 9000
ip addr add 10.0.3.1/30 dev e0.1235
ip addr add 2001:db8:0:3::1/64 dev e0.1235

vppctl create sub TenGigabitEthernet3/0/0 1236 dot1ad 2345 exact-match
vppctl set interface state TenGigabitEthernet3/0/0.1236 up
vppctl set interface mtu packet 9000 TenGigabitEthernet3/0/0.1236
vppctl set interface ip address TenGigabitEthernet3/0/0.1236 10.0.4.1/30
vppctl set interface ip address TenGigabitEthernet3/0/0.1236 2001:db8:0:4::1/64
vppctl lcp create TenGigabitEthernet3/0/0.1236 host-if e0.1236
ip link set e0.1236 up mtu 9000
ip addr add 10.0.4.1/30 dev e0.1236
ip addr add 2001:db8:0:4::1/64 dev e0.1236

vppctl create sub TenGigabitEthernet3/0/0 1237 dot1ad 2345 inner-dot1q 1000 exact-match
vppctl set interface state TenGigabitEthernet3/0/0.1237 up
vppctl set interface mtu packet 9000 TenGigabitEthernet3/0/0.1237
vppctl set interface ip address TenGigabitEthernet3/0/0.1237 10.0.5.1/30
vppctl set interface ip address TenGigabitEthernet3/0/0.1237 2001:db8:0:5::1/64
vppctl lcp create TenGigabitEthernet3/0/0.1237 host-if e0.1237
ip link set e0.1237 up mtu 9000
ip addr add 10.0.5.1/30 dev e0.1237
ip addr add 2001:db8:0:5::1/64 dev e0.1237
```
