Skip to content

UPF Certification

This is the complete, deployable guide to UPF Certification — Stage 1 of CNTC. It certifies a 5G User Plane Function as a black box over its N3 (GTP-U) and N4 (PFCP) interfaces: you point the framework at any open-source UPF, run it, and get a graded verdict and — on a clean pass — a certificate.

A UPF Certification PASS asserts that the UPF, on the stated rig:

  • conforms to the PFCP / N4 control interface (3GPP TS 29.244) — association, and session establish / modify / delete;
  • is robust on N3 — it does not crash on malformed GTP-U, unknown TEIDs, or PSC (0x85) extension headers;
  • forwards user traffic at the measured throughput and latency reported in the scorecard; and
  • holds the reported number of concurrent PFCP sessions (UEs).

CNTC has two decoupled layers: an engine (upfbench) that measures, and an umbrella (cntc) that judges the serialized results.json against a requirement catalog. Because they are decoupled, any past run can be re-graded without re-running it.

The engine drives four test suites over the UPF’s interfaces (N3 GTP-U, N4 PFCP, N6 upstream):

  • performance — throughput (NDR/PDR), latency/jitter, burst, and multi-flow on a single tunnel.
  • load — many UEs at once: session capacity plus aggregate and per-UE throughput under load.
  • pfcp — N4 conformance to TS 29.244 (association, establish / modify / delete, error handling).
  • n3neg — N3 robustness: malformed GTP-U, unknown TEID, PSC ext-header — crash detection and recovery.

Requirements are grouped into profiles. The conformance profile is hardware-independent and binary pass/fail (PFCP + N3 robustness) — certifiable on any rig. The performance profile is hardware-dependent and graded relative to a baseline on the same rig class, never as an absolute number.

  • An open-source UPF under test — e.g. SD-Core BESS-UPF, OAI-UPF, or Open5GS-UPF — reachable on its N4 (PFCP) endpoint and N3 (GTP-U) access interface.
  • A Linux traffic node (the “rig”) with Python 3.10+, tcpreplay, scapy, and passwordless sudo (raw send needs it).
  • L2/L3 reachability from the rig to the UPF’s access interface and N4 endpoint.
  • Optional, for line-rate throughput: DPDK + TRex with SR-IOV NICs.

Clone the repository and install the dependencies. make prereqs installs system packages, builds the bundled pfcpsim, and runs a readiness check.

git clone https://github.com/TOSSI-Foundation/CNTC.git
cd CNTC
make prereqs        # deps + pfcpsim + doctor   (uses sudo)
# (or, Python only:  pip install -e .  -> installs the `cntc` and `upfbench` CLIs)

Confirm the rig is ready:

make doctor         # preflight: deps, sudo, interfaces, (TRex/hugepages/VFs if DPDK)

Describe the UPF under test in a campaign config. The wizard writes one for you, or copy and edit configs/my-upf.yaml.

make configure      # interactive wizard -> configs/<campaign>.yaml

The key fields are:

  • adapter — the UPF plugin: sdcore_bess, oai_upf, or open5gs_upf.
  • mode — the traffic path: af_packet (portable) or dpdk / af_xdp / cndp (line-rate).
  • N4 / PFCP address of the UPF, and the N3 access interface (plus gNB/UPF IPs and TEID).
  • baseline — a reference results.json, required only for the performance profile.

The full end-to-end run — every suite plus N3 robustness, then grade, then certify — is a single command:

make run CONFIG=configs/my-upf.yaml CAMPAIGN=MY-UPF-001

Or run just the certification set (the hardware-independent conformance suites), or one suite at a time:

make run-conformance CONFIG=configs/my-upf.yaml CAMPAIGN=MY-UPF-001   # pfcp + n3neg, then grade
make run-perf        CONFIG=configs/my-upf.yaml CAMPAIGN=MY-UPF-001   # performance + load + pfcp
make run-n3neg       CONFIG=configs/my-upf.yaml CAMPAIGN=MY-UPF-001   # N3 robustness only

Every run writes campaigns/<id>/scorecard.md (and .html) and embeds a verdict block in results.json. The exit code is non-zero unless the result is PASS, so it drops straight into CI. Re-grade any past run with make verdict CAMPAIGN=<id>.

CNTC VERDICT  -  profile: conformance  (catalog v0.2.0)
 [PASS] * CF-01  PFCP association setup / release      status == pass
 [PASS] * CF-02  Session establishment                 status == pass
 ...
 [PASS] * NT-01  Malformed GTP-U robustness (no crash) status == pass
 [PASS] * NT-02  Unknown-TEID handling                 status == pass
 essential gate (all): 7 passed / 0 failed / 0 na  (of 7)
RESULT:  PASS
  • PASS — every essential test passed.
  • FAIL — an essential test failed (for example, the UPF crashed on malformed N3 input).
  • INCOMPLETE — an essential test did not run; it is never silently promoted to a pass.

A CNTC certificate is issued automatically only when every essential test passes:

make certify CAMPAIGN=MY-UPF-001    # writes certificate.md / .html / .json  (PASS only)

It records exactly what was verified — the profile, catalog version, the standards covered (TS 29.244 for PFCP/N4 and TS 29.281 for GTP-U/N3), and the essential-gate result — so it is auditable against the campaign that produced it. A run that fails or skips an essential test yields no certificate.

A live, view-only web dashboard renders every campaign — scorecards, per-suite charts, findings, and the certificate:

make dashboard      # http://<host>:8050   (or: make dashboard-bg  to run detached)

The dashboard (and, optionally, the test runner) ship as Kubernetes manifests:

make k8s-deploy     # live dashboard as a Deployment + Service (NodePort 30850)
make k8s-run        # run the whole suite in-cluster as a Job
make k8s-clean      # tear both down

One adapter per UPF keeps them comparable. Validated today:

  • SD-Core BESS-UPF — DPDK / AF_XDP / CNDP / AF_PACKET.
  • OAI-UPF — simpleswitch.
  • Open5GS-UPF — adapter present.
  • doctor flags a missing dependency — re-run make prereqs.
  • tcpreplay hangs or is denied — the generator runs sudo tcpreplay; add a passwordless-sudo (NOPASSWD) rule for it.
  • Throughput looks tiny (~0.002 Mpps) — that is the af_packet generator ceiling, not the UPF; switch to DPDK/TRex for real numbers.
  • N3 forwarding is zero — the UPF access MAC can change on pod restart (macvlan); leave the N3 remote MAC blank so it is resolved live.