AF_XDP in a Proxmox LXC
AF_XDP in a Proxmox LXC
A small, hands-on note for making OVS inside a Proxmox LXC container use AF_XDP on the container’s uplink. This is the first slice of the larger userspace-datapath work in ADR 0004.
Read this first
In a Proxmox LXC the container’s eth0 is a veth; its peer lives on the
Proxmox host. AF_XDP on a veth can only run in copy / generic (SKB) mode,
never zero-copy, and some kernels/drivers refuse it entirely. So:
- this gets you a functional AF_XDP path, not a performance one;
- for zero-copy, run OVS on the Proxmox host with the physical NIC, or in a VM with a passed-through NIC — not in the LXC;
- OVS taking over
eth0removes the container’s normal uplink, so give the container a second interface (eth1) for management/SSH. - The veth’s VLAN offload must be disabled on both ends for tagged traffic — see step 4.
- Proxmox VE ships stock
lxc-pve; theveth.vlan_*keys from PR #4737 are not in it. On Proxmox, use the.link/udev persistence in step 4; the LXC keys only work with a patched/upstream LXC.
1. Check what the host and container can do
On the Proxmox host:
uname -r
grep -E 'CONFIG_XDP_SOCKETS|CONFIG_BPF_SYSCALL' /boot/config-$(uname -r)
lspci -nn | grep -i ethernet
ethtool -i <physical-nic> # driver; XDP support is driver-dependent
pct config <ctid> # unprivileged? caps? features?
AF_XDP needs CONFIG_XDP_SOCKETS=y and a driver that supports XDP. The LXC
needs CAP_BPF/CAP_NET_ADMIN and an unrestricted RLIMIT_MEMLOCK. An
unprivileged container usually cannot load XDP programs; use a privileged
one for the PoC. In /etc/pve/lxc/<ctid>.conf:
unprivileged: 0
features: nesting=1
lxc.apparmor.profile: unconfined
lxc.cap.drop:
lxc.prlimit.memlock: unlimited
Changing unprivileged is not always possible in place — create a fresh
privileged container if Proxmox refuses. Also disable the Proxmox firewall
on the test NIC while debugging.
2. Prepare the container
Give the container a second NIC for management (Proxmox: net1), then work
on net0 (eth0) as the future AF_XDP uplink.
apt-get update
apt-get install -y iproute2 ethtool libbpf1 libxdp1 xdp-tools
apt-get install -y openvswitch-switch
ulimit -l unlimited
Check that this OVS build knows afxdp without touching eth0 yet (a
distro build may not have it):
ldd "$(command -v ovs-vswitchd)" | grep -E 'libbpf|libxdp' \
|| echo "no libbpf/libxdp linked"
strings "$(command -v ovs-vswitchd)" | grep -m1 -i afxdp || true
The real proof is step 5; if adding the port says unknown interface type
afxdp, the build lacks --enable-afxdp — rebuild OVS or use a build that
has it.
3. Prove AF_XDP works before involving OVS
Do not debug OVS and the driver at the same time.
# XDP itself (generic/SKB mode is what a veth can do). Use a program
# that returns XDP_PASS, or you will cut the container's own uplink:
xdp-loader load -m skb eth0 <xdp-pass-prog.o>
xdp-loader status
xdp-loader unload eth0 -m skb
# AF_XDP specifically: xdpsock from the kernel's bpf selftests (or your
# distro's equivalent). -S = skb/copy, -N = native, -Z = zero-copy.
xdpsock -i eth0 -q 0 -S
If xdpsock -S fails on the veth, stop: OVS’s generic mode will fail the
same way. Fall back to the host or a VM.
4. Disable VLAN offload on both ends of the veth
For VLAN-tagged traffic this is required, and it is easy to miss. The veth
driver enables 802.1Q RX/TX offload by default. With RX offload on, the
kernel moves the VLAN tag into skb metadata; AF_XDP only sees the untagged
frame, so OVS’s trunks=/tag= never matches and the frames are dropped.
A VM’s tap has rx-vlan-offload: off [fixed]; a veth does not.
Each end of the pair has its own feature flags, so disable it on both:
# container side (the AF_XDP port)
ethtool -K eth0 rxvlan off txvlan off
# host side: the veth<vmid>i<index> peer Proxmox created for that NIC
ethtool -K veth301i2 rxvlan off txvlan off
Verify both:
ethtool -k eth0 | grep -E 'rx-vlan-offload|tx-vlan-offload'
ethtool -k veth301i2 | grep -E 'rx-vlan-offload|tx-vlan-offload'
With the patched LXC from PR #4737 the same setting is expressed in the container config and applied to both ends:
lxc.net.0.veth.vlan_rx_offload = 0
lxc.net.0.veth.vlan_tx_offload = 0
Without it, ovs-appctl dpctl/dump-flows shows only untagged flows ending in
actions:drop.
Persistence
ethtool -K is per netdev. A container restart recreates the veth, so the
flags reset. Make it declarative with systemd.link instead.
Inside the container, /etc/systemd/network/90-ovnxdp.link:
[Match]
Name=ovnxdp
[Link]
ReceiveVLANCTAGHardwareAcceleration=no
TransmitVLANCTAGHardwareAcceleration=no
On the Proxmox host, /etc/systemd/network/90-veth301i2.link:
[Match]
OriginalName=veth301i2
[Link]
ReceiveVLANCTAGHardwareAcceleration=no
TransmitVLANCTAGHardwareAcceleration=no
Apply to an existing interface (or restart the CT):
udevadm control --reload
udevadm trigger --action=add /sys/class/net/ovnxdp
udevadm trigger --action=add /sys/class/net/veth301i2
Proxmox VE ships stock lxc-pve, which does not have the
vlan_rx_offload/vlan_tx_offload keys; they come from PR #4737 and need a
patched/upstream LXC. On stock Proxmox the .link files above are the
working route. With a patched LXC the keys are applied on every start and the
.link files are not needed.
5. Configure OVS (inside the container)
ovs-vsctl add-br br-xdp -- set bridge br-xdp datapath_type=netdev
# eth0 becomes the AF_XDP uplink. generic = copy mode, the only mode a
# veth can be expected to support; best-effort would try zero-copy first.
ovs-vsctl add-port br-xdp eth0 -- \
set Interface eth0 type=afxdp \
options:xdp-mode=generic \
options:n_rxq=1
ovs-vsctl set Interface eth0 mtu_request=1500
ip link set br-xdp up
xdp-mode accepts best-effort (default), native-with-zerocopy,
native, and generic. Start with generic for a veth; use
best-effort on a real NIC and read the log to see which mode it chose.
6. Give a workload a port on that bridge
A plain container/workload still needs a kernel-visible device. On a netdev bridge that is a TAP:
ip tuntap add dev tap-c1 mode tap
ip link set tap-c1 up
ip link set tap-c1 address 02:00:00:00:c1:01
ovs-vsctl add-port br-xdp tap-c1
# then move it into the workload's netns (docker --network none):
pid=$(docker inspect -f '' c1)
mkdir -p /var/run/netns && ln -sfn "/proc/$pid/ns/net" /var/run/netns/c1
ip link set tap-c1 netns c1
ip netns exec c1 ip link set tap-c1 name eth0
ip netns exec c1 ip link set eth0 up
The TAP leg still crosses TUN/TAP; only the uplink (eth0) is AF_XDP.
7. Verify
ovs-vsctl show
ovs-appctl dpif/show # br-xdp: eth0 (afxdp), tap-c1 (tap)
ovs-appctl dpctl/dump-flows
ovs-appctl dpif-netdev/pmd-stats-show
grep -i 'XDP mode' /var/log/openvswitch/ovs-vswitchd.log
You want XDP mode will be in use: generic (or native/native-with-zerocopy
on a real NIC). Defaulted to ... / “Failed to detect suitable XDP mode”
means the driver refused every mode.
8. Persistence
ovn-fabric does not emit afxdp ports yet — its binding emitter only knows
vlan, veth and physical. Until that changes, keep the ovs-vsctl
above in a small unit or a script run after ovs-vswitchd, and do not put it
in the generated deployer yet.
Troubleshooting
| Symptom | Likely cause |
|---|---|
xsk_socket__create failed (Operation not permitted) |
unprivileged LXC, seccomp/AppArmor, or missing CAP_BPF/CAP_NET_ADMIN |
No XDP program is loaded at ifindex … |
libxdp/libbpf too old, or the driver/build lacks XDP |
Failed to detect suitable XDP mode |
veth/driver refused native and generic; use the host/VM |
setrlimit(RLIMIT_MEMLOCK) failed |
raise ulimit -l / lxc.prlimit.memlock |
unknown interface type afxdp |
OVS built without --enable-afxdp |
| container loses SSH after step 5 | eth0 is now OVS’s; use eth1 for management |
tagged VLAN traffic is dropped; dump-flows shows only untagged flows with actions:drop |
RX VLAN offload still on; disable rxvlan/txvlan on both veth ends (step 4) |
What this does not cover
- Zero-copy AF_XDP (needs a physical NIC on the host or a passed-through NIC in a VM).
- OVN with
br-inton the netdev datapath — that requires every bridge in the patch chain to be netdev, covered in ADR 0004. - DPDK; that is the separate path in ADR 0004.