Skip to content

Magma eBPF - Deployment Guide

This guide deploys a complete Magma 5G core with our eBPF GTP-U user-plane datapath:

  1. Magma Orchestrator (orc8r) - the cloud controller + NMS, deployed with the magma-orc8r Ansible deployer.
  2. Magma Access Gateway (AGW) - the edge node, deployed as Docker containers, then switched from the kernel gtp0 datapath to three eBPF/TC programs that do GTP-U encap/decap while OVS keeps flow management (QFI rides skb->mark → OVS pkt_mark).

Part 1 - Deploy Magma Orchestrator (orc8r)

Code: https://github.com/TOSSI-Foundation/magma-orc8r (the magma-deployer Ansible/RKE+Helm tooling) Upstream reference: https://magma.github.io/magma/docs/next/orc8r/deploy_using_ansible

1.0 Prerequisites

  • A clean Ubuntu host (VM or bare metal) with internet access, run as root.
  • Enough resources for a single-node Kubernetes + orc8r (≈ 4 vCPU / 8–16 GiB / 40 GiB).

1.1 Quick install

sudo bash -c "$(curl -sL https://github.com/TOSSI-Foundation/magma-orc8r/raw/main/deploy-orc8r.sh)"

The script will prompt for (press Enter to accept the defaults):

Prompt Default
Orchestrator domain name magma.local
NMS organization (subdomain) magma-test
NMS email ID admin
NMS password admin

It then: 1. installs yq + ansible, 2. creates a magma system user with passwordless sudo and an SSH key, 3. clones the deployer and runs ansible-playbook deploy-orc8r.yml, which brings up the full stack as roles: prerequisites → docker → kubernetes → openebs → metallb → postgresql → secrets → orc8r → prometheus → haproxy → elasticsearch → fluentd → dns.

1.2 Switch to the magma user (after the deploy finishes)

sudo su - magma

1.3 Set up the NMS login (once all pods are Ready)

cd ~/magma-deployer
ansible-playbook config-orc8r.yml

Check the pods first if you want to be sure:

kubectl get pods -A          # wait until orc8r-* and nms-* are Running/Ready

1.4 Grab the root CA (needed by the AGW in Part 2)

cat ~/magma-deployer/secrets/rootCA.pem

Save this file - the Access Gateway must trust it to register with the controller. You will copy it to the AGW at /var/opt/magma/certs/rootCA.pem (see Part 2.2).

The NMS is then reachable at https://<nms-org>.<orc8r-domain> (e.g. https://magma-test.magma.local) with the email/password you set.


Part 2 - Deploy the Magma AGW with the eBPF GTP-U datapath

Code: https://github.com/TOSSI-Foundation/magma/tree/ebpf - all of our work lives under lte/gateway/ebpf/.

2.1 Our work - what the eBPF datapath is (code detail)

We replace the kernel gtp0 vport (out-of-tree openvswitch.ko GTP encap/decap) with three TC/eBPF programs attached with clsact, sharing three pinned maps. OVS keeps doing flow management; QFI is lifted off the wire into skb->mark, which unmodified OVS reads back as pkt_mark. ~1,400 LoC, ~73% eBPF C.

Folder layout (lte/gateway/ebpf/)

bpf/        gtp_decap.c · gtp_encap.c · gtp_mark.c · gtp_maps.h · Makefile   the TC programs + map ABI
src/        tc_loader.c                                                       libbpf loader (attach + pin)
python/     ebpf_control.py · ebpf_session_adapter.py                        map primitives + reconciling adapter
deploy/     install-ebpf.sh · setup/teardown-ebpf-gtp.sh ·                   turnkey host install
            magma-ebpf-gtp.service · magma-ebpf-adapter.service              systemd units
ansible/    roles/ebpf/* · playbooks/deploy_ebpf.yml                         production Ansible role
tests/      test_phase2_protocol.py · test_phase2_loader.py · e2e_validate.sh unit / component / system
tools/      ebpf-status.sh · ebpf-demo.sh                                    inspection + live demo

The three eBPF programs

Program Attach point Direction What it does
gtp_decap.c (gtp_decap) N3/S1-U eth1 ingress Uplink Parses the GTP-U header, validates TEID against the per-UE session, writes QFI into skb->mark, strips the outer IP/UDP/GTP, and bpf_redirects the inner packet to gtp_veth0 (into OVS).
gtp_mark.c (gtp_veth0_mark) gtp_veth0 ingress Uplink (seam) Restores/normalizes skb->mark so OVS classifies the flow on pkt_mark (= QFI). This is the QFI seam - no GTP match field, no OVS fork.
gtp_encap.c (gtp_encap) gtp_veth1 ingress Downlink Looks up the UE session, builds the GTP-U header (DL TEID + QFI + PDU-session container), bpf_adjust_head to push the outer headers, and sends it out N3 to the gNB. Includes the MTU/DF black-hole guard.

Pinned maps (/sys/fs/bpf, shared by all three programs)

  • ue_session_map - key = UE IP; value = {ul_teid, dl_teid, qfi, session_flags, ul_packets, ul_bytes, dl_packets, dl_bytes}. The live per-subscriber forwarding + accounting state.
  • config_map - datapath config by index: S1U_IFINDEX, SGI_IFINDEX, OVS_IFINDEX, DEBUG_LEVEL, SGI_IP, VETH_IFINDEX, LINK_MTU.
  • stats_map - global counters: UL/DL_PKTS, UL/DL_BYTES, GTP_DECAP_OK, GTP_ENCAP_OK, SESSION_MISS, TEID_MISMATCH, PKT_FORWARDED/DROPPED, etc.

Loader & control plane

  • src/tc_loader.c - a libbpf loader that compiles-in via CO-RE, attaches the three programs with clsact, and pins the maps. Runs as magma-ebpf-gtp.service.
  • python/ebpf_session_adapter.py - an event-driven adapter that mirrors pipelined's per-session gtp_br0 OVS flows into ue_session_map whenever they change. Magma's signaling stack and container images are never modified - sessions programmed by an external gNB/simulator through the stock AGW are auto-reconciled into eBPF. Runs as magma-ebpf-adapter.service.
  • python/ebpf_control.py - thin map read/write primitives over bpftool.

Data path summary

Uplink:   gNB ──GTP-U──▶ eth1 ─[gtp_decap: strip GTP, QFI→skb->mark]─▶ gtp_veth0
                         ─[gtp_mark]─▶ OVS gtp_br0 (match pkt_mark=QFI) ──▶ SGi ──▶ internet
Downlink: internet ──▶ SGi ──▶ OVS gtp_br0 ──▶ gtp_veth1 ─[gtp_encap: build GTP-U, DL TEID+QFI]─▶ eth1 ──▶ gNB

The SCTP/N2 control plane on eth1 is untouched; only the GTP-U user plane moves into eBPF.

2.2 Prerequisites on the AGW host

  • A clean Ubuntu host with two NICs: eth0 (mgmt/SGi) and eth1 (N3/S1-U to the gNB).
  • Copy the orc8r root CA from Part 1.4 onto the AGW:
sudo mkdir -p /var/opt/magma/certs
sudo nano /var/opt/magma/certs/rootCA.pem      # paste the rootCA.pem contents

2.3 Install the Docker AGW (with our ebpf-branch code)

Run the installer, pointing it at the TOSSI ebpf branch so /opt/magma contains our lte/gateway/ebpf/ folder:

sudo GIT_URL=https://github.com/TOSSI-Foundation/magma.git \
     MAGMA_VERSION=ebpf \
     EBPF_DATAPATH=true \
     bash -c "$(curl -sL https://raw.githubusercontent.com/TOSSI-Foundation/magma/refs/heads/ebpf/lte/gateway/deploy/agw_install_docker.sh)" agw

What this script does / what you'll see when it finishes: 1. Verifies it's root on Ubuntu and that rootCA.pem is present. 2. Configures DNS, holds the running kernel, disables unattended-upgrades. 3. Installs Docker CE + the compose plugin; creates the ubuntu user (sudo + docker). 4. Installs ansible==5.0.1, clones the repo to /opt/magma, checks out the branch. 5. Renames NICs to eth0/eth1 and applies netplan. 6. Runs ansible-playbook magma_docker.yml (tag agwc) which installs OVS + the host dependencies and stages the containers under /var/opt/magma/docker/. 7. Prints "Reboot this VM to apply kernel settings." → reboot.

2.4 Bring up the AGW containers

The Ansible run stages and starts the stack; to (re)bring it up manually:

cd /var/opt/magma/docker
sudo docker compose --compatibility up -d
sudo docker ps        # all components should be Up

The AGW components (containers): magmad, oai_mme, sctpd, pipelined, sessiond, mobilityd, subscriberdb, policydb, redis, directoryd, enodebd, state, control_proxy, eventd, … (22 services).

2.5 Deploy / confirm the eBPF datapath

If you did not use EBPF_DATAPATH=true, deploy the host-side datapath now (idempotent, safe to re-run):

sudo /opt/magma/lte/gateway/ebpf/deploy/install-ebpf.sh        # auto-detects N3 (eth1)

What it does: - builds the three eBPF objects + tc_loader (pulls libbpf 1.3 + bpftool if needed), - writes /etc/magma/ebpf.env, installs the two systemd units, and systemctl enable --now magma-ebpf-gtp.service magma-ebpf-adapter.service, - removes the kernel gtp0 vport (ovs-vsctl del-port gtp_br0 gtp0) - eBPF now owns GTP-U, - attaches gtp_decap@eth1, gtp_veth0_mark@gtp_veth0, gtp_encap@gtp_veth1 and pins the maps.

When it's up, any external gNB/simulator that attaches a UE through the stock AGW is auto-programmed into eBPF (via the adapter) - no per-UE manual steps.


Part 3 - Verify & inspect (manual check commands)

3.1 Containers & component logs

sudo docker ps                                  # every AGW component should be Up
sudo docker logs -f --tail 200 oai_mme          # MME signaling (S1AP/NGAP/NAS) - UE attach/register
sudo docker logs -f --tail 200 sessiond         # session/PDU + policy
sudo docker logs -f --tail 200 pipelined        # OVS / datapath programming
sudo docker logs --tail 200 magmad              # agent + orc8r check-in
# generic: sudo docker logs -f --tail 200 <component>

MME log (inside the container, if you prefer the file):

sudo docker exec oai_mme tail -f /var/log/mme.log 2>/dev/null \
  || sudo docker logs -f oai_mme

Confirm orc8r check-in (gateway shows "connected" in the NMS):

sudo docker logs magmad 2>&1 | grep -iE 'checkin|Successfully sent'

3.2 One-shot eBPF status (our tool)

sudo /usr/local/bin/ebpf-status.sh

It prints: the two services, the three TC programs (with bytecode tags), the pinned maps, the decoded config_map, nonzero stats_map counters, the live per-UE sessions (TEID/QFI + UL/DL counters), and the OVS QFI classifier flows (cookie=0xeb…).

3.3 The eBPF checks, by hand

# (a) the three TC programs are attached
sudo tc filter show dev eth1      ingress | grep gtp_decap
sudo tc filter show dev gtp_veth0 ingress | grep gtp_veth0_mark
sudo tc filter show dev gtp_veth1 ingress | grep gtp_encap
sudo bpftool prog show | grep -E 'gtp_(decap|encap|veth0_mark)'

# (b) maps pinned + live per-UE session (TEID/QFI/counters)
sudo bpftool map show pinned /sys/fs/bpf/ue_session_map
sudo bpftool map dump pinned /sys/fs/bpf/ue_session_map
sudo bpftool map dump pinned /sys/fs/bpf/stats_map

# (c) the QFI seam - stock OVS classifies UL on the eBPF-written mark (= QFI)
sudo ovs-ofctl dump-flows gtp_br0 | grep 'cookie=0xeb'
#     the legacy kernel-GTP flows now carry 0 packets (forwarding moved to eBPF):
sudo ovs-ofctl dump-flows gtp_br0 | grep -E 'NXM_NX_TUN_ID|in_port=32768'

# (d) live per-packet trace (decap parses UL, encap builds DL) - needs DEBUG_LEVEL>0
sudo timeout 30 cat /sys/kernel/debug/tracing/trace_pipe | grep -E 'GTP|ENCAP|QFI|Session'

# (e) full guided terminal demo (7 shots)
sudo /usr/local/bin/ebpf-demo.sh           # AUTO=1 to skip pauses; TRACE_SECS=30

Expected proof: the OVS UL classifier flow's n_packets equals the UE's ul_packets in ue_session_map → QFI is riding skb->mark/pkt_mark with no switch fork and no GTP match field; the legacy NXM_NX_TUN_ID flows stay at n_packets=0 because GTP forwarding now lives entirely in eBPF.


Appendix - directory & path reference

Path What
/opt/magma the cloned repo (ebpf branch) - source of lte/gateway/ebpf/
/var/opt/magma/docker/ AGW compose dir (.env, docker-compose.yaml) - docker compose up -d here
/var/opt/magma/certs/rootCA.pem orc8r root CA the AGW must trust
/var/opt/magma/ebpf/ built eBPF objects + tc_loader + adapter
/etc/magma/ebpf.env datapath config (S1U_IF, OVS_BR, OVS_VETH, …)
/sys/fs/bpf/{ue_session_map,config_map,stats_map} pinned maps
/usr/local/bin/ebpf-status.sh, ebpf-demo.sh inspection + demo tools