free5GC NEF on Aether SD-Core: northbound exposure for applications
This guide covers the integration of a free5GC Network Exposure Function with the Aether SD-Core control plane. It takes the NEF from two non-functional services to six working ones, adds GPSI to SUPI identifier translation as shared infrastructure, and wires four UDR route groups whose handler logic already existed but was never reachable over HTTP. An application outside the operator network can request QoS on a named flow, ask where a device is, describe how a device behaves, and negotiate a transfer window, addressing the device only by an external identifier.
Overview
A 5G core exposes nothing to the applications that run over it. An application platform knows its devices by an external identifier such as an MSISDN. It does not learn the IMSI, the serving cell, or the IP address the core assigned, and it has no path into policy or the user plane. 3GPP TS 23.502 clause 5.2.6 defines 36 Nnef_* services as the sanctioned way to cross that boundary.
Aether SD-Core does not ship a NEF. free5GC ships one, and of those 36 services it implemented two: PFD Management and Traffic Influence. Neither functioned against SD-Core. PFD Management failed because the UDR answered 501 Not Implemented on every application-data route, so nothing an application pushed could persist. Traffic Influence was disabled in the chart, and its policy did not reach the user plane.
This integration makes both work and adds four more services, with every result verified against the core's own state rather than against HTTP status codes. The practical outcome is that an application can ask the network for something and the effect can be observed in Mongo, in the PCF and SMF logs, or in the UPF's packet processing pipeline.
Architecture
The deployment divides into three parts: an SD-Core control plane that is almost entirely unchanged, the NEF that runs beside it, and the interfaces that join them.
SD-Core control plane
The control plane runs as the sd-core 4.1.3 chart deploys it, with two exceptions. The SMF and PCF run stock upstream images at versions newer than the chart pins, for reasons covered under integration findings. The AMF, NRF, UDM, AUSF, NSSF, webui, simapp and the BESS UPF are untouched. Only two network functions carry modified code: the NEF, and the UDR.
The NEF deployment
The NEF is not part of the sd-core Helm release. It is deployed from its own manifest, which matters for two reasons. Its server certificate must be reissued from the CA of the particular SD-Core install it runs against, and its image is swapped with kubectl set image because helm upgrade on the sd-core umbrella chart is broken at 4.1.3.
It registers an NF profile in SD-Core's NRF advertising six services at apiPrefix https://nef:29505, and discovers the PCF, UDM, AMF and UDR through the NRF the same way any other network function does.
Southbound consumers added by this work:
| Consumer | Interface | Used by |
|---|---|---|
udm_service.go | Nudm_SDM, Nudm_ParameterProvision | identifier translation, ParameterProvision |
amf_service.go | Namf_EventExposure | MonitoringEvent |
pcf_bdt_service.go | Npcf_BDTPolicyControl | BDTPNegotiation |
The northbound models are hand written. free5gc/openapi carries the southbound payloads but nothing application-facing from TS 29.122.
Northbound services
| Service | Northbound route | Spec | Backing NF |
|---|---|---|---|
| PFD Management | /3gpp-pfd-management/v1 | TS 29.122 | UDR, read by the SMF through the PFDF |
| Traffic Influence | /3gpp-traffic-influence/v1 | TS 29.522 | PCF |
| AsSessionWithQoS | /3gpp-as-session-with-qos/v1 | TS 29.122 clause 4.4 | PCF |
| MonitoringEvent | /3gpp-monitoring-event/v1 | TS 29.122 clause 5.3 | AMF |
| ParameterProvision | /3gpp-cp-parameter-provisioning/v1 | TS 29.122 clause 5.10 | UDM |
| BDTPNegotiation | /3gpp-bdt/v1 | TS 29.122 clause 5.9, TS 29.554 | PCF |
The first three concern traffic: identify it, route it, prioritise it. The last three concern devices: observe them, describe them, schedule around them. PFD Management is the identification layer the others build on, because a policy that applies to an application's traffic has no effect until PFDs define what that traffic is.
Interface map
| Interface | Protocol | Reference deployment |
|---|---|---|
| Northbound, AF to NEF | HTTPS, TS 29.122 | TCP 29505, TLS from the SD-Core CA |
| Nnrf | HTTPS | registration and discovery, heartbeat every 60 s |
| Npcf_PolicyAuthorization | HTTPS | AsSessionWithQoS, Traffic Influence |
| Npcf_BDTPolicyControl | HTTPS | BDTPNegotiation |
| Nudm_SDM | HTTPS | GPSI to SUPI, id-translation-result |
| Nudm_ParameterProvision | HTTPS | ParameterProvision |
| Namf_EventExposure | HTTPS | MonitoringEvent, TCP 29518 |
| Nudr_DataRepository | HTTPS | TCP 29504, application and policy data |
Identifier translation
Every new service needs the same thing first. An application addresses a device by GPSI, and every internal interface works in SUPI. The NEF resolves one to the other through the UDM's id-translation-result, and the resolution was retrofitted into Traffic Influence, which previously had no way to accept an external identifier at all.
SD-Core has no API to write this mapping. It is seeded directly into subscriptionData.identityData, along with the ppData document that ParameterProvision patches. Without both, every GPSI-addressed request fails.
The SUPI never crosses the northbound boundary. An application asks about an MSISDN and is answered about an MSISDN.
Validation depth by service
Each service is functional, and they differ in how far down the stack the effect can be observed.
| Service | Observable at | Evidence read from |
|---|---|---|
| AsSessionWithQoS | the UPF datapath | BESS appQERLookup rule count |
| Traffic Influence | the UPF, accepted | PCF session binding, SMF PFCP modification |
| PFD Management | the UDR and the SMF's view | /nudr-dr/v2/application-data/pfds |
| MonitoringEvent | live AMF state | cell id, TAC and PLMN from the AMF context |
| ParameterProvision | the UDR | subscriptionData.ppData in Mongo |
| BDTPNegotiation | the UDR | policyData.bdtData in Mongo |
One service, AsSessionWithQoS, is proven into the user plane. An application requests QoS for a named flow on a specific device. The PCF binds the request to that device's live session by IP address, builds a PCC rule, and the SMF carries it to the user plane over PFCP. Two new metering rules appear in the BESS datapath.
Defects fixed
Four defects, each verified as still open on upstream main.
UDR application data wiring. The influenceData and pfds route groups answered 501 while their handler logic sat unused in producer/. The change is routing only, with no new business logic.
UDR BDT data wiring. The same pattern on policy-data/bdt-data. This route is load bearing. The PCF reads it on every BDT create and treats any non-200 as fatal, so the entire TS 29.554 negotiation failed with the PCF answering the application 503 before any NEF code was involved.
NEF dropped SBI port. searchNFServiceUri tests the FQDN branches before the branch that appends the port, so any NF registered with an FQDN resolved to the scheme default. The NEF dialled https://amf on port 443 and timed out. Most free5GC NFs register an ipv4Address and no fqdn, which is why it had not surfaced.
NEF NRF heartbeat. The NEF registered once and disappeared from the NRF after a measured 243 seconds. This is invisible from inside the NEF because it caches discovered URIs, so everything it initiates keeps working. What breaks is every other NF's ability to discover it, including the SMF finding nnef-pfdmanagement for PFD notifications. After the fix the profile was still registered at 422 seconds, with 15 heartbeats recorded by the NRF.
Deployment
Prerequisites: a single-node microk8s cluster with a working SD-Core 4.1.3 install in namespace aether-5gc, Go 1.22 or newer to build the NEF, and Docker with the microk8s registry enabled at localhost:32000.
| Component | Version (reference deployment) |
|---|---|
| microk8s / Kubernetes | v1.28.15, channel 1.28/stable |
| SD-Core | sd-core-4.1.3 |
| User plane | bess-upf-1.7.2 |
| SMF | ghcr.io/omec-project/5gc-smf:rel-4.3.3 |
| PCF | ghcr.io/omec-project/5gc-pcf:rel-3.2.1 |
| AMF | ghcr.io/omec-project/5gc-amf:rel-3.1.1, as the chart deploys it |
| NEF | built from source, tagged from the git SHA |
| UDR | built from source, tagged from the git SHA |
1. Clone the three repositories
The script paths default to these locations.
git clone -b master <sdcore-nef> ~/sdcore-nef
git clone -b sdcore-integration <nef-repo> ~/nef-dev
git clone -b nef-support <udr-repo> ~/udr-dev
2. Deploy the NEF
sudo ~/sdcore-nef/deploy-nef.sh
This reissues the NEF's certificate from the live SD-Core CA. It is mandatory after any SD-Core install or reinstall, because the certificate is signed by the CA of that particular install.
3. Build the NEF and UDR images
./build-udr.sh # ~/udr-dev -> localhost:32000 -> kubectl set image
./build-nef.sh # ~/nef-dev -> localhost:32000 -> kubectl set image
Both tag the image from the git SHA of the source tree, so the tag differs on every machine and after every commit. Do not copy a tag from documentation.
4. Pin the required images
sudo ~/sdcore-nef/pin-images.sh # apply
sudo ~/sdcore-nef/pin-images.sh --check # report only
Run this after any SD-Core install or reinstall. The chart carries no image overrides and the Helm release records none, so a fresh install reverts the UDR, SMF and PCF silently. The UDR tag is resolved from the local registry rather than hardcoded.
5. Seed the identifier mapping
sudo ~/sdcore-nef/seed-identity.sh msisdn-0900000000 imsi-<the real one>
sudo ~/sdcore-nef/seed-identity.sh --show
The subscriber must also exist in SD-Core through webui or simapp with that IMSI and the matching key material. Otherwise it never registers, and the AMF refuses every MonitoringEvent request with 403 "UE is not served by this AMF".
To find the IMSI that is actually attached rather than assuming it:
kubectl -n aether-5gc logs deploy/amf --tail=3000 \
| grep -aoE 'imsi-[0-9]+' | sort | uniq -c
Verification
Everything below uses kubectl and curl only. The NEF, UDR and NRF are reachable by service name from inside the cluster, so the checks run in a throwaway pod rather than needing anything installed on the host.
1. NEF registration and discoverability
A NEF that is running is not the same as a NEF other network functions can find. Ask SD-Core's own NRF:
kubectl -n aether-5gc run nrf-$RANDOM --rm --attach --restart=Never --quiet \
--image=curlimages/curl:8.7.1 -- \
curl -sk "https://nrf:29510/nnrf-disc/v1/nf-instances?target-nf-type=NEF&requester-nf-type=AF"
Expect "nfStatus":"REGISTERED" and six services at apiPrefix https://nef:29505:
nnef-pfdmanagement
3gpp-traffic-influence
3gpp-as-session-with-qos
3gpp-cp-parameter-provisioning
3gpp-monitoring-event
3gpp-bdt
Re-run this more than five minutes after the NEF started. The profile used to expire after 243 seconds without a heartbeat, and the failure is invisible from the NEF's own side because it caches the URIs it has already discovered.
2. UDR route groups respond
This check distinguishes a working deployment from one where the API appears healthy but persists nothing.
kubectl -n aether-5gc run udr-$RANDOM --rm --attach --restart=Never --quiet \
--image=curlimages/curl:8.7.1 -- sh -c '
for p in application-data/pfds application-data/influenceData policy-data/bdt-data; do
printf "%-34s %s\n" "$p" "$(curl -sk -o /dev/null -w "%{http_code}" https://udr:29504/nudr-dr/v2/$p)"
done'
| Route | Stock SD-Core | Wired |
|---|---|---|
application-data/pfds | 501 | 200 |
application-data/influenceData | 501 | 400 |
policy-data/bdt-data | 501 | 200 |
influenceData answers 400 because it requires query parameters. What matters is that none of the three returns 501, which is the stock answer and means the route is not mounted.
3. Northbound service over TLS
kubectl -n aether-5gc run nef-$RANDOM --rm --attach --restart=Never --quiet \
--image=curlimages/curl:8.7.1 -- \
curl -sk -X POST -H 'Content-Type: application/json' \
-d '{"externalId":"msisdn-0900000000",
"notificationDestination":"http://af.example.com:8080/n",
"monitoringType":"LOCATION_REPORTING",
"maximumNumberOfReports":1}' \
https://nef:29505/3gpp-monitoring-event/v1/myapp/subscriptions
With a registered UE this returns 200 and the report inline, carrying the serving cell, tracking area and PLMN read from the AMF's live context:
"locationInfo": {"cellId":"00066c000","trackingAreaId":"000001","plmnId":"00101"}
A 403 "UE is not served by this AMF" means the GPSI maps to a SUPI the AMF is not serving. Check the mapping against the IMSI that is actually attached.
4. QoS request reaching the user plane
This is the only check that observes the effect in the datapath rather than in a database or a log. Read the UPF's rule table, send the request, then read it again.
kubectl -n aether-5gc exec upf-0 -c bessd -- \
/opt/bess/bessctl/bessctl show module appQERLookup | head -3
Then, with a UE that has an active PDU session, substituting its address:
kubectl -n aether-5gc run qos-$RANDOM --rm --attach --restart=Never --quiet \
--image=curlimages/curl:8.7.1 -- \
curl -sk -X POST -H 'Content-Type: application/json' \
-d '{"ueIpv4Addr":"192.168.100.1",
"notificationDestination":"http://af.example.com:8080/n",
"supportedFeatures":"01",
"qosReference":"9",
"flowInfo":[{"flowId":1,"flowDescriptions":[
"permit out ip from 192.168.100.1 to any",
"permit in ip from any to 192.168.100.1"]}]}' \
https://nef:29505/3gpp-as-session-with-qos/v1/myapp/subscriptions
Reading the rule table again shows two further rules. The chain can be followed independently at each hop:
kubectl -n aether-5gc logs deploy/pcf --since=2m | grep -iE 'session Binding|PCC rule'
kubectl -n aether-5gc logs deploy/smf --since=2m | grep -iE 'PFCP Session Modification'
Expect session Binding Success - UeIpv4[...] from the PCF and PFCP Session Modification Success from the SMF.
The UPF will log could not read GBRDL. It might be because of non-GBR flow. That is expected and is covered under known limits: the rules are installed, but with no bitrate attached.
UE requirements per check
| Service | UE required |
|---|---|
| PFD Management, ParameterProvision, BDTPNegotiation | none |
| MonitoringEvent | registered, but no PDU session needed |
| AsSessionWithQoS, single-UE Traffic Influence | an active PDU session |
The two that need a session need one because the PCF binds the application's request to the UE's IP address. Without a session there is no address and no binding.
Integration notes
Six behaviours are worth noting before reproducing this deployment.
Check upstream before patching a network function. AF policy was not reaching the user plane: the SMF discarded every policy update with no PDU Session AMBR, and behind that fault HandleSMPolicyUpdateNotify carried a bare // Update UPF / TODO, while the API answered 201 throughout. It was patched by hand before upstream was checked. omec's SMF already fixes both faults at rel-4.3.3, and handles session rules better than the hand written patch by activating a modified rule only when it is the active rule and has actually changed. The whole fix was a version bump. The chart pins old images, so the pinned version should be checked first.
Status codes are not evidence of success. The NEF turns a PCF failure into HTTP 200 with an empty body. An SMF that applies nothing still answers 201. A UDR that persists nothing looks healthy from outside. Every check in this work reads the component's own state instead.
A UE that passes traffic does not mean the SMF owns the session. After an SMF restart the UPF keeps enforcing rules for a context the new SMF knows nothing about. A handset will ping at zero percent loss while the control plane owns nothing. Confirm session ownership in the SMF log rather than inferring it from working traffic.
The NEF keeps all application state in memory. It must not be scaled beyond one replica, and the shipped HPA must stay disabled. Subscription identifiers only resolve on the pod that minted them.
helm upgrade is broken on the sd-core umbrella chart at 4.1.3, which calls b64dec on the existing CA before buildCustomCert. Use kubectl set image, which is what the build scripts do.
After any SMF restart the PFCP association drops. Rebuild the UE session. If a PDU session then fails with cause 72, "No established PFCP Association", restart upf-adapter first and then smf, and re-attach.
Request path
For one AsSessionWithQoS request travelling from an application to the packet processing pipeline:
Application (AF) external identifier or UE IP, flow described by IP filters
-> NEF :29505 TS 29.122 northbound, GPSI to SUPI through Nudm_SDM
-> PCF Npcf_PolicyAuthorization
binds the request to the UE's live session by IP
creates a PCC rule with traffic control data
-> SMF receives the policy update, builds the session rule
-> PFCP N4 session modification toward the user plane
-> BESS UPF appQERLookup gains metering rules
Each hop leaves evidence that can be read independently: the PCF logs session Binding Success with the UE's IPv4 address, the SMF logs PFCP Session Modification Success and a transition to SmStateActive, and the UPF's rule count changes.
Known limitations
Four gaps remain. Three are capabilities SD-Core has never implemented, and one is a property of the topology. All four were verified as still open on upstream main.
ParameterProvision has no consumer. ExpectedUeBehaviour appears nowhere in SD-Core's AMF. The data is written correctly to the UDR and the chain stops there. Closing it means paging optimisation in the AMF.
BDT has no consumer, and the PCF makes no real decision. No NF reads bdtData except the PCF that wrote it. getDefaultTransferPolicy returns the application's own requested window as the recommendation, with rating group 1 and no bit rate caps, and the code still carries // Only support one bdt policy, TODO: more policy for decision. The negotiation is structurally complete, but no traffic is shifted off peak.
MonitoringEvent cannot push continuously. The AMF has no outbound notification sender. It stores eventNotifyUri and never posts to it. The one-shot mode is itself defined by TS 29.122, so the service is complete for that mode, and the NEF's relay handler for continuous notifications is implemented and waiting for a compliant AMF.
Traffic Influence steering cannot be demonstrated. The policy reaches the UPF. The effect cannot be shown because a DNAI has nowhere to point in a single-UPF deployment.
Two further notes. The QER that reaches the UPF carries no bitrate, because SD-Core's PCF ignores qosReference entirely, so the UPF logs could not read GBRDL. It might be because of non-GBR flow. Real metering rules are installed with no rate attached. Mapping qosReference onto marBwUl and marBwDl on the media components would close this, and the PCF does honour those fields. Separately, the application-facing PFD API cannot create its own AF, because PostPFDManagementTransactions calls GetAf() and returns 404 instead of NewAf(), so a Traffic Influence POST is currently used to bootstrap the AF.
Source and references
- Integration, manifests, build and verification scripts:
sdcore-nef - NEF, four new services and two bug fixes:
Network_Exposure_Function, branchsdcore-integration - UDR, application data and BDT data route wiring:
sdcore_udr_nef, branchnef-support
This work builds on open source: the NEF from the free5GC project, and SD-Core from the OMEC project and the Open Networking Foundation. Thanks to their maintainers and communities.
Standards reference: 3GPP TS 23.502 (procedures and NEF services), TS 29.122 (northbound application APIs), TS 29.522 (traffic influence), TS 29.554 (background data transfer), TS 29.503 and TS 29.504 (UDM and UDR), TS 29.512 and TS 29.514 (PCF), TS 29.518 (AMF event exposure).