Software Based Router (FRR) + Data Plane Development Kit (DPDK)
Overview
This guide extends the Software Based Router (FRR) guide by adding VPP (Vector Packet Processor) as a high-performance DPDK-based data plane. The combination of FRR (control plane) and VPP (data plane) creates a software router capable of line-rate packet forwarding, suitable for 10/40/100 Gbps science network links.
What is VPP? VPP (fd.io Vector Packet Processor) is an open-source framework based on DPDK (Data Plane Development Kit) that bypasses the Linux kernel for packet forwarding, achieving near wire-speed performance on commodity hardware.
What is DPDK? DPDK (Data Plane Development Kit) is a set of libraries that allow user-space applications to directly control NICs, bypassing the Linux kernel network stack. This eliminates interrupt overhead and enables sustained multi-gigabit forwarding.
Architecture with FRR + VPP:
┌─────────────────────────────────────────┐
│ FRR (Control Plane) │
│ BGP, Zebra, route-map management │
│ Managed by: SENSE Ansible (sense.frr) │
└─────────────────┬───────────────────────┘
│ Routes/FIB
┌─────────────────▼───────────────────────┐
│ VPP (Data Plane - DPDK) │
│ High-speed packet forwarding │
│ VLAN sub-interfaces │
│ Direct NIC access (bypasses kernel) │
└─────────────────────────────────────────┘
When to Use This Setup
Use FRR + VPP when:
- Your site requires 10 Gbps or higher routing throughput
- You are using testbed resources (e.g., FABRIC nodes) with dedicated NICs
- Standard Linux kernel forwarding is a bottleneck
- You want jumbo frame support (MTU 9000) at line rate
Use plain FRR (without VPP) when:
- Throughput requirements are below ~5 Gbps
- The Linux kernel routing stack is sufficient
- Simpler deployment is preferred
Prerequisites
- A Linux host with a DPDK-compatible NIC (Intel X710, X520, ConnectX-5, etc.)
- The NIC must have IOMMU enabled in the host BIOS (
intel_iommu=onkernel parameter) - The host kernel must support hugepages (required by DPDK)
- Docker or Docker Compose installed
- FRR already installed (see the FRR installation guide)
- SSH access from the SiteRM Frontend
Step 1 — Prepare the Host
Enable Hugepages
VPP requires hugepages for its memory model. Configure them permanently:
# Add to /etc/default/grub GRUB_CMDLINE_LINUX:
# intel_iommu=on iommu=pt default_hugepagesz=1G hugepagesz=1G hugepages=8
# After editing grub:
grub2-mkconfig -o /boot/grub2/grub.cfg
reboot
Verify after reboot:
grep HugePages /proc/meminfo
# Should show HugePages_Total: 8 (or however many you configured)
Enable IOMMU
Verify IOMMU is active:
dmesg | grep -i iommu
# Should see: "Intel-IOMMU: enabled"
Bind NIC to DPDK-compatible Driver
Identify your NIC’s PCI address:
lspci | grep -i ethernet
# Example: 01:00.0 Ethernet controller: Intel Corporation X710 10GbE Controller
Load the vfio-pci driver and bind the NIC:
modprobe vfio-pci
# Get the vendor:device IDs
lspci -n -s 01:00.0
# Example output: 01:00.0 0200: 8086:1572
# Bind the NIC to vfio-pci
echo "8086 1572" > /sys/bus/pci/drivers/vfio-pci/new_id
Important: Once bound to DPDK, the NIC is no longer visible to the Linux kernel — it will disappear from ip link show. VPP will own it directly.
Step 2 — Install VPP
Install VPP on the host system:
# Add the FD.io VPP repository (example for EL9/RHEL9):
curl -s https://packagecloud.io/install/repositories/fdio/release/script.rpm.sh | bash
dnf install -y vpp vpp-plugins vpp-api-python
# For Ubuntu 22:
curl -s https://packagecloud.io/install/repositories/fdio/release/script.deb.sh | bash
apt-get install -y vpp vpp-plugin-core
Step 3 — Configure VPP
VPP configuration is split into startup configuration and runtime interface setup.
Startup Configuration (/etc/vpp/startup.conf)
unix {
nodaemon
log /var/log/vpp/vpp.log
full-coredump
cli-listen /run/vpp/cli.sock
}
api-trace {
on
}
cpu {
## Assign cores to VPP main and worker threads
## main-core 1
## corelist-workers 2-3
}
dpdk {
## List of DPDK-managed NICs by PCI address
dev 0000:01:00.0 {
name eth0
}
## Optionally add more NICs:
# dev 0000:01:00.1 {
# name eth1
# }
num-rx-queues 4
num-tx-queues 4
}
buffers {
buffers-per-numa 128000
}
Start VPP
systemctl enable --now vpp
Configure VPP Interfaces at Runtime
Use the VPP CLI (vppctl or vpp_api_test) to set up interfaces:
# Enter VPP CLI
vppctl
# Show available interfaces
show interface
# Bring up the DPDK interface
set interface state eth0 up
# Configure VLAN sub-interfaces (SENSE will manage these automatically)
# Example: Create VLAN 3600 on eth0
create sub eth0 3600
set interface state eth0.3600 up
set interface ip address eth0.3600 2001:db8::/64
# Show current IP config
show interface address
Step 4 — Configure FRR alongside VPP
FRR handles the control plane (BGP route exchange). Follow the FRR installation guide for the Docker Compose setup, but make one important change: since VPP owns the NIC, FRR uses a separate interface (such as a loopback or a Linux TAP/MEMIF interface created by VPP) for BGP peering.
Important: Do NOT bind the interface used for BGP management traffic to DPDK — only the high-speed data interfaces should be DPDK-managed.
Example FRR BGP configuration for a VPP-backed router:
!
router bgp 65001
bgp router-id 10.0.0.1
neighbor 10.0.0.2 remote-as 65002
!
address-family ipv6 unicast
network 2001:db8::/32
neighbor 10.0.0.2 activate
neighbor 10.0.0.2 soft-reconfiguration inbound
exit-address-family
exit
!
Step 5 — Configure SiteRM Frontend
The Ansible inventory for an FRR+VPP node uses the same sense.frr.frr plugin as a plain FRR deployment. SENSE manages the FRR control plane; VPP data-plane changes (VLAN sub-interfaces, IP addresses) are handled via additional Ansible tasks.
# fe/conf/etc/ansible-conf.yaml
inventory:
vpp_frr_router:
network_os: sense.frr.frr
host: 10.0.1.100 # Management IP of the VPP+FRR host
user: ubuntu
sshkey: /opt/siterm/config/ssh-keys/id-rsa-sense
become: true # May need sudo to run vppctl
ssh_common_args: "-o StrictHostKeyChecking=no"
In main.yaml, register the node as a switch with VPP-managed ports:
MAIN:
general:
sitename: T2_US_YOURSITE
...
site:
...
switch:
vpp_frr_router:
allports: false
ports:
"eth0":
capacity: 100000 # 100 Gbps NIC
vlan_range: [3600-3699]
Step 6 — Tune Interfaces for High Throughput
Run the interface tuning script on each data interface before binding to DPDK. This sets optimal NIC parameters for jumbo frames and high queue depth:
#!/bin/bash
INTF=$1
echo "Tuning interface $INTF for high-throughput..."
# Set jumbo frame MTU
ip link set dev $INTF mtu 9000
# Increase TX queue length
ip link set dev $INTF txqueuelen 10000
# Disable adaptive interrupt coalescing
ethtool -C $INTF adaptive-rx off
# Set fixed interrupt coalescing (1ms)
ethtool -C $INTF rx-usecs 1000
# Set maximum ring buffer sizes
MAX_RX=$(ethtool -g $INTF | grep 'RX:' | awk '{print $2}' | head -1)
MAX_TX=$(ethtool -g $INTF | grep 'TX:' | awk '{print $2}' | head -1)
ethtool -G $INTF rx $MAX_RX tx $MAX_TX
echo "Done tuning $INTF"
Usage:
chmod +x tune-interface.sh
./tune-interface.sh enp1s0
Kernel TCP Tuning
Apply the following sysctl settings for maximum throughput across all flows:
# TCP buffer sizes (2 GiB max)
sysctl -w net.core.rmem_max=2147483647
sysctl -w net.core.wmem_max=2147483647
sysctl -w net.ipv4.tcp_rmem="4096 87380 268435456"
sysctl -w net.ipv4.tcp_wmem="4096 87380 268435456"
# Network backlog and TCP tuning
sysctl -w net.core.netdev_max_backlog=250000
sysctl -w net.ipv4.tcp_no_metrics_save=1
sysctl -w net.ipv4.tcp_adv_win_scale=1
sysctl -w net.ipv4.tcp_low_latency=1
sysctl -w net.ipv4.tcp_timestamps=0
sysctl -w net.ipv4.tcp_sack=1
sysctl -w net.ipv4.tcp_moderate_rcvbuf=1
# Use BBR congestion control + fq queueing disc
sysctl -w net.ipv4.tcp_congestion_control=bbr
sysctl -w net.ipv4.tcp_mtu_probing=1
sysctl -w net.core.default_qdisc=fq
Persist these in /etc/sysctl.conf.
Verification
After the setup is complete:
- Verify VPP is forwarding:
vppctl show interface vppctl show interface address vppctl show ip6 fib - Verify FRR BGP peering:
docker exec -it frr vtysh -c "show bgp summary" docker exec -it frr vtysh -c "show bgp ipv6 unicast" - Test connectivity:
ping6 <peer_IPv6_address> tracepath6 <remote_host> - Verify SENSE manages the node:
From SiteRM Frontend, run:
siterm-ansible-runner --hostname vpp_frr_router --action getfacts
Kubernetes Deployment with Multus
VPP+FRR can also be deployed as a Kubernetes StatefulSet using Multus CNI to pass physical NICs directly into the pod. This is the recommended approach for sites already managing SENSE infrastructure on Kubernetes.
How it works:
NetworkAttachmentDefinitionobjects define each physical NIC attachment using thehost-deviceCNI plugin- The DPDK NIC is passed directly into the pod (PCI passthrough via
vfio-pci) - VPP inside the pod binds the NIC and creates Linux mirror interfaces via LCP (
lcp create) — these are the interfaces FRR uses for BGP and routing - SENSE connects to FRR via SSH on a dedicated port (22334) using the pod’s IPv6 address
Two variants are provided in the vpp-frr repository:
| File | NICs | Hugepage type | Use case |
|---|---|---|---|
vpp-frr-router.yaml |
1 NIC | 1Gi + 2Mi | Standard single-uplink router |
new-vpp-frr.yaml |
2 NICs, bonded | 2Mi only | Dual-uplink with XOR/L3-4 bond |
Prerequisites for Kubernetes Deployment
- Kubernetes cluster with Multus CNI installed
- Node with a DPDK-compatible NIC bound to
vfio-pci(see Step 1 of bare-metal guide above) - Hugepages configured on the target node:
# For 2Mi hugepages (both variants):
echo 4096 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
# For 1Gi hugepages (vpp-frr-router.yaml only):
echo 1 > /sys/kernel/mm/hugepages/hugepages-1048576kB/nr_hugepages
Persist in /etc/default/grub: hugepagesz=2M hugepages=4096 hugepagesz=1G hugepages=1
- The
sensenamespace must exist:kubectl create namespace sense
Variant 1: Single NIC (vpp-frr-router.yaml)
Uses one physical NIC passed into the pod. The NIC is attached via a NetworkAttachmentDefinition with static IPAM to give the pod a management IP before VPP starts.
Resources: 32 CPU, 50Gi RAM, 1Gi hugepages-1Gi + 8Gi hugepages-2Mi
Step 1 — Create the NetworkAttachmentDefinition:
apiVersion: k8s.cni.cncf.io/v1
kind: NetworkAttachmentDefinition
metadata:
name: pci-pt-interface-1
namespace: sense
spec:
config: '{
"cniVersion": "0.4.0",
"name": "net1",
"plugins": [
{
"type": "host-device",
"pciBusID": "0000:17:00.1",
"ipam": {
"type": "static",
"addresses": [{"address": "10.77.50.10/24", "gateway": "10.77.50.1"}],
"routes": [{"dst": "10.77.50.0/24"}]
}
},
{
"type": "tuning",
"sysctl": {
"net.ipv6.conf.net1.autoconf": "0",
"net.ipv6.conf.net1.accept_ra": "0"
}
}
]
}'
Step 2 — Create the VPP startup ConfigMap:
The startup.gate file configures VPP interfaces at startup. It runs after VPP initializes the DPDK NIC and sets up VLAN sub-interfaces:
comment { LAN interface }
set interface state Ethernet1/0/0 up
set interface mtu packet 9216 Ethernet1/0/0
set interface feature gso Ethernet1/0/0 enable
lcp create Ethernet1/0/0 host-if e1
comment { WAN Facing interface (VLAN 3999) }
create sub-interfaces Ethernet1/0/0 3999
set interface state Ethernet1/0/0.3999 up
set interface mtu packet 1500 Ethernet1/0/0.3999
set interface feature gso Ethernet1/0/0.3999 enable
set interface ip address Ethernet1/0/0.3999 2605:9a00:10:7::1/127
comment { LAN-WAN interface 2540 }
create sub-interfaces Ethernet1/0/0 2540
set interface state Ethernet1/0/0.2540 up
set interface mtu packet 9212 Ethernet1/0/0.2540
set interface feature gso Ethernet1/0/0.2540 enable
set interface ip address Ethernet1/0/0.2540 2605:9a00:10:2010::/64
lcp create Ethernet1/0/0 host-if e1— creates Linux interfacee1mirroring the VPP interface. FRR usese1for control-plane traffic.- Sub-interfaces (
Ethernet1/0/0.2540, etc.) are the SENSE-managed VLAN data paths. - MTU 9212 on LAN-WAN interfaces accounts for VLAN header overhead on a 9216 outer MTU.
Step 3 — Create the SSH key Secret:
apiVersion: v1
kind: Secret
metadata:
name: vppfrr-ssh-key
namespace: sense
type: Opaque
stringData:
authorized_keys: |
ssh-ed25519 AAAA... your-sense-ssh-public-key
The SiteRM Frontend connects to the VPP+FRR pod via SSH (port 22334) to run Ansible. Add the Frontend’s public key here.
Step 4 — Deploy the StatefulSet:
Key fields in the StatefulSet spec:
annotations:
k8s.v1.cni.cncf.io/networks: pci-pt-interface-1 # Attach the NIC via Multus
securityContext:
privileged: true
capabilities:
add: [NET_ADMIN, SYS_RAWIO, IPC_LOCK] # Required for DPDK/VPP
env:
- name: ENV_MAIN_CORE
value: "0" # VPP main thread core
- name: ENV_CORELIST_WORKERS
value: "1-8" # VPP forwarding worker cores
- name: ENV_BUFFERS_PER_NUMA
value: "24576" # Packet buffer pool size
- name: ENV_PUBLIC_INTF
value: "net1" # Multus interface name in pod
- name: ENV_PUBLIC_INTF_PCI
value: "0000:17:00.1" # PCI address of the NIC
- name: SSH_PORT
value: "22334" # SSH port for SENSE management
- name: SSH_LISTEN6_ADDRESS
value: "[2605:9a00:10:7::1]" # IPv6 addr VPP assigns to WAN iface
Deploy:
kubectl apply -f vpp-frr-router.yaml
kubectl -n sense get pods -l app=vpp-frr
Variant 2: Dual NIC with Bonding (new-vpp-frr.yaml)
Uses two physical NICs that VPP bonds together in XOR/L3-4 mode for higher throughput and link redundancy. Neither NIC has IPAM — VPP handles all addressing.
Resources: 24 CPU, 24Gi RAM, 8Gi hugepages-2Mi (no 1Gi hugepages)
Two NetworkAttachmentDefinitions (one per NIC, no IPAM):
# Interface 1
apiVersion: k8s.cni.cncf.io/v1
kind: NetworkAttachmentDefinition
metadata:
name: pci-pt-interface-1
namespace: sense
spec:
config: '{"cniVersion":"0.4.0","name":"net1","plugins":[
{"type":"host-device","pciBusID":"0000:17:00.1"},
{"type":"tuning","sysctl":{"net.ipv6.conf.net1.autoconf":"0","net.ipv6.conf.net1.accept_ra":"0"}}
]}'
---
# Interface 2
apiVersion: k8s.cni.cncf.io/v1
kind: NetworkAttachmentDefinition
metadata:
name: pci-pt-interface-2
namespace: sense
spec:
config: '{"cniVersion":"0.4.0","name":"net2","plugins":[
{"type":"host-device","pciBusID":"0000:17:00.0"},
{"type":"tuning","sysctl":{"net.ipv6.conf.net2.autoconf":"0","net.ipv6.conf.net2.accept_ra":"0"}}
]}'
VPP startup.gate for bonded NICs:
comment { === Interface 1 === }
set interface state Ethernet1/0/0 up
set interface mtu packet 9216 Ethernet1/0/0
set interface feature gso Ethernet1/0/0 enable
lcp create Ethernet1/0/0 host-if e1
comment { === Interface 2 === }
set interface state Ethernet2/0/0 up
set interface mtu packet 9216 Ethernet2/0/0
set interface feature gso Ethernet2/0/0 enable
lcp create Ethernet2/0/0 host-if e2
comment { === Create bond (XOR L3+L4 hash) === }
create bond mode xor load-balance l34
bond add BondEthernet0 Ethernet1/0/0
bond add BondEthernet0 Ethernet2/0/0
set interface state BondEthernet0 up
set interface mtu packet 9216 BondEthernet0
set interface feature gso BondEthernet0 enable
lcp create BondEthernet0 host-if bond0
comment { === WAN Facing interface (VLAN 3999) === }
create sub-interfaces BondEthernet0 3999
set interface state BondEthernet0.3999 up
set interface mtu packet 1500 BondEthernet0.3999
set interface ip address BondEthernet0.3999 2605:9a00:10:7::1/127
comment { === LAN-WAN interface 2540 === }
create sub-interfaces BondEthernet0 2540
set interface state BondEthernet0.2540 up
set interface mtu packet 9212 BondEthernet0.2540
set interface ip address BondEthernet0.2540 2605:9a00:10:2010::/64
- Bond mode
xor load-balance l34— hashes on Layer 3 (IP) + Layer 4 (TCP/UDP) for per-flow load balancing across both NICs. - Both physical interfaces are mirrored to Linux (
e1,e2), and the bond aggregate is mirrored tobond0for FRR.
StatefulSet — references both Multus networks:
annotations:
k8s.v1.cni.cncf.io/networks: pci-pt-interface-1,pci-pt-interface-2
env:
- name: ENV_PUBLIC_INTF
value: "net1"
- name: ENV_PUBLIC_INTF_PCI
value: "0000:17:00.1"
- name: ENV_PRIVATE_INTF
value: "net2"
- name: ENV_PRIVATE_INTF_PCI
value: "0000:17:00.0"
Deploy:
kubectl apply -f new-vpp-frr.yaml
kubectl -n sense get pods -l app=vpp-frr
Connecting SENSE to the Kubernetes VPP+FRR Pod
The pod exposes SSH on port 22334 at the IPv6 address assigned to the WAN sub-interface (e.g., 2605:9a00:10:7::1). Configure the SiteRM Ansible inventory to reach it:
inventory:
vpp_frr_k8s:
network_os: sense.frr.frr
host: "2605:9a00:10:7::1" # IPv6 addr of pod's WAN interface
user: root
sshkey: /opt/siterm/config/ssh-keys/id-rsa-sense
become: false
ssh_common_args: "-o StrictHostKeyChecking=no -p 22334"
The SiteRM Debugger should also be deployed in the same Kubernetes network namespace (or configured with the pod’s IP) to enable ping/traceroute probes from within the VLAN.
Node Pinning
Both manifests use nodeSelector and nodeAffinity to pin the pod to the specific node that has the DPDK NIC bound:
nodeSelector:
kubernetes.io/hostname: c020.af.uchicago.edu
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- c020.af.uchicago.edu
Update these values to match the hostname of your DPDK node.
Troubleshooting
| Symptom | Cause | Resolution |
|---|---|---|
| VPP fails to start | Hugepages not configured | Check /proc/meminfo for HugePages_Total > 0; check node hugepage allocation |
| NIC not visible in VPP | PCI binding not complete | Re-run vfio-pci binding steps; verify NetworkAttachmentDefinition PCI address |
Pod stuck in Pending |
Hugepages not available on node or NIC in use | Check kubectl describe pod, verify hugepage resource on node |
| BGP sessions not establishing | FRR and VPP using same NIC | Separate management and data NICs; use LCP mirror interface for FRR |
| SENSE Ansible fails | SSH not accessible on pod IP/port | Verify pod is running, SSH port 22334 reachable, authorized_keys Secret is correct |
| Packet drops at high rate | Insufficient hugepages or worker cores | Increase hugepages on node and assign more ENV_CORELIST_WORKERS |
| Bond not forming | Second NIC not passed to pod | Verify both NetworkAttachmentDefinitions exist and both PCI addresses are correct |
Notes
- VPP is the data plane only. All BGP, route-maps, and prefix lists are managed by FRR and by extension by SENSE via Ansible.
- SENSE’s
sense.frr.frrAnsible plugin targets FRR, not VPP directly. VPP interface configuration is expected to be handled separately (or via custom Ansible tasks). - For testbed environments (e.g., FABRIC), SmartNICs or FPGA-based NICs may have different DPDK driver requirements.
- Reference implementation and demo playbooks are available in the vpp-frr GitHub repository.