OAI CN5G is the OpenAirInterface 5G core network, and it is the core behind every Duranta tutorial on COSMOS. This page explains what the core's containers do, how the Duranta image starts them with Docker Compose, where subscribers come from, why the gNB configuration has to be re-pointed at the AMF on every start, and how to tell a healthy core from one that only looks healthy. Read it before your first Duranta tutorial, or when a Duranta gNB reports No AMF is associated.
A 5G Standalone (SA) core authenticates subscribers, admits UEs, allocates their IP addresses and forwards their traffic to a data network. The 5G SA fundamentals page explains the core's role and the attach sequence; this page covers one implementation of it.
OAI CN5G is the core-network project of the OpenAirInterface Software Alliance, the same organisation that develops the OAI RAN from which Duranta is forked. Source and documentation: gitlab.eurecom.fr/oai/cn5g. Each network function (NF) is a separate C++ service shipped as a Docker image (oaisoftwarealliance/oai-<nf>), and the project provides Docker Compose files that start a complete core on one host.
On COSMOS, OAI CN5G solves one problem: it gives the Duranta gNB a core from the same software family, so a Duranta cell works end to end with no licence and no second server. It runs on the RAN host next to nr-softmodem.
Figure — The 5G SA architecture. In OAI CN5G each box in the core is a Docker container on one bridge network; the gNB reaches the AMF container over N2 and the UPF container over N3, and the oai-ext-dn container stands in for the data network on N6.
The Duranta image uses OAI's basic-nrf deployment: the file docker-compose-basic-nrf.yaml in /opt/oai-cn5g/oai-cn5g-fed/docker-compose. It starts eight NFs plus MySQL, each in its own container on one Docker bridge network.
| Container | Function | What it does |
|---|---|---|
oai-amf |
Access and Mobility Management Function (AMF) | Terminates N2 from the gNB (NGAP on SCTP port 38412), accepts NG Setup, and handles registration, authentication and NAS security for each UE. |
oai-smf |
Session Management Function (SMF) | Creates PDU sessions, allocates the UE's IP address from the DNN's pool, and programs the UPF over N4 (PFCP, UDP 8805). |
oai-upf |
User Plane Function (UPF) | Carries user traffic: GTP-U tunnels from the gNB on N3 (UDP 2152) in, IP packets to the data network on N6 out. |
oai-nrf |
NF Repository Function (NRF) | Service discovery. The other NFs register with it and look each other up through it — the nrf in basic-nrf. |
oai-ausf |
Authentication Server Function (AUSF) | Runs 5G-AKA authentication for a registering UE. |
oai-udm |
Unified Data Management (UDM) | Produces authentication vectors and subscription data from the subscriber record. |
oai-udr |
Unified Data Repository (UDR) | Reads and writes subscriber records in MySQL for the UDM. |
mysql |
subscriber database | The oai_db database: authentication data (IMSI, keys, sequence number), access-and-mobility data and session-management data per subscriber. |
oai-ext-dn |
external data network | Not a 3GPP NF: a traffic-generator container that plays the data network. It routes the UE pool back to the UPF and NATs UE traffic to the internet. It is also where the tutorials run the iperf3 server. |
All the NFs read one shared configuration file, conf/basic_nrf_config.yaml, mounted into each container. It holds the PLMN, the slices, the NAS security algorithm lists and the DNNs. On COSMOS it serves PLMN 001/01, DNN oai with IPv4 sessions, and the UE pool 192.168.100.0/22 — the same PLMN and pool as the other COSMOS stacks.
The containers sit on a Docker bridge called demo-oai-public-net (host interface demo-oai), subnet 192.168.70.128/26. The compose file carries fixed addresses for each container, but those lines are commented out, so Docker assigns the addresses in start order. The AMF, the ext-DN and every other container can therefore come up at a different address after any compose up. Two consequences run through every Duranta tutorial:
oai-ext-dn, read with docker inspect, never to a remembered address. The address .131, for example, is not always the ext-DN: it can belong to mysql.OAI CN5G is part of the Duranta image, duranta.ndz (currently duranta-w39-20261001), loaded with omf load. The compose files are in /opt/oai-cn5g/oai-cn5g-fed/docker-compose, and the container images are baked into the image, so starting the core pulls nothing from the network. The core runs on the same server as the Duranta gNB or O-DU.
Danger: Never run
docker image prune -aordocker system prune -aon a testbed host. It deletes every image not attached to a running container — including the baked OAI CN5G images when the core is stopped — and the only recovery is to reload the node's image. To free disk after your own build, remove the image and container IDs your run created, or dangling layers withdocker image prune -f(without-a).
Bring the core up and wait for the containers to report healthy:
ssh root@<node>
cd /opt/oai-cn5g/oai-cn5g-fed/docker-compose
docker compose -f docker-compose-basic-nrf.yaml up -d
docker exec -i mysql mysql -uroot -plinux oai_db < /root/duranta/provision.sql # subscriber 001010123456780
docker compose -f docker-compose-basic-nrf.yaml ps # all NFs "healthy"
What to look for: every container healthy in the ps output. The core is ready only when the AMF is healthy; a container that is Up but not yet healthy is still starting.
The same start appears in the O-RAN 7.2 tutorials in a shorter form:
docker compose -f /opt/oai-cn5g/oai-cn5g-fed/docker-compose/docker-compose-basic-nrf.yaml up -d
docker ps --format '{{.Names}}\t{{.Status}}' | grep oai
Eight NFs plus mysql should show Up. To stop the core:
docker compose -f /opt/oai-cn5g/oai-cn5g-fed/docker-compose/docker-compose-basic-nrf.yaml down
The over-the-air tutorial wraps all of this — core up, subscribers, the AMF address, the gNB — in the image's one-shot /root/duranta/start-all.sh, which waits for the AMF container to report healthy before continuing.
Because Docker assigns the AMF's address at start-up, the amf_ip_address in a gNB .conf goes stale whenever the containers are re-created. A gNB with a stale address logs [SCTP] Connect failed and No AMF is associated to the gNB; the UE then completes RRC setup and is released before any NAS message reaches the core. The shipped /root/duranta/gnb-rfsim.conf carries a hardcoded AMF address that drifts in exactly this way.
The fix is to read the AMF's current address from Docker and write it into the configuration after the core is up and before the gNB starts. The image carries a helper for this:
bash /root/patch-amf.sh <conf>
It reads the address with docker inspect on oai-amf, rewrites the ipv4 value on the amf_ip_address line of the given .conf, and prints the new value. The soft-only tutorial does the same inline, together with the gNB's own N2/N3 bind addresses, which must be the host's address on the demo-oai bridge:
AMFIP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' oai-amf)
sed -i "s#amf_ip_address.*ipv4.*#amf_ip_address = ({ ipv4 = \"$AMFIP\"; });#" /root/duranta/gnb-rfsim.conf 2>/dev/null || true
GNBIP=$(ip -4 -o addr show demo-oai | grep -oE '192\.168\.70\.[0-9]+' | head -1) # the gNB must bind N2/N3 on the demo-oai bridge
sed -i "s/GNB_IPV4_ADDRESS_FOR_NG_AMF *= *\"[0-9.]*\"/GNB_IPV4_ADDRESS_FOR_NG_AMF = \"$GNBIP\"/; s/GNB_IPV4_ADDRESS_FOR_NGU *= *\"[0-9.]*\"/GNB_IPV4_ADDRESS_FOR_NGU = \"$GNBIP\"/" /root/duranta/gnb-rfsim.conf
The O-RAN 7.2 bring-up scripts and start-all.sh perform this step themselves. When you start a gNB by hand, run it every time.
The MySQL container initialises oai_db from an SQL file mounted into it at creation, database/oai_db2.sql. On the COSMOS image that seed file is built from the cosmos-sim-db inventory, so a freshly created core already holds the COSMOS subscribers on PLMN 00101. The tutorials then apply /root/duranta/provision.sql on top, which writes the specific test subscribers they use (for example the soft UE 001010123456780 and the Quectel card 001010000000032) with PLMN 00101, DNN oai and IPv4 sessions.
Where the data lives decides what survives:
| Event | Subscriber rows |
|---|---|
docker compose down then up -d |
kept — MySQL stores /var/lib/mysql in an anonymous Docker volume |
| host reboot | kept |
docker compose down -v |
lost; the next up re-seeds from oai_db2.sql |
omf load of the image |
lost; the core starts from the image's seed file |
So a subscriber added by hand to the live database is runtime state: it disappears with the next image load. Anything that must be present on every fresh node has to be in the seed file baked into the image, or be re-applied by the bring-up (as provision.sql is).
The image also carries a reconciler that checks the live database against the inventory and adds only what is missing, generating the rows with the package's own generator rather than from hand-written keys:
python3 /root/cosmos-sims-check.py oai-cn5g # report missing subscribers
python3 /root/cosmos-sims-check.py oai-cn5g --provision # add only the missing ones
Warning: SIM key material belongs on core servers only. Run provisioning on the host that runs the core; never put Ki or OPc into console parameters, bundle variables or wiki pages. A bundle carries the IMSI only.
Two properties of the subscriber rows cause failures that look like radio problems:
sst 1 and sst 1 / sd 000001. A subscriber row whose session data names another slice (upstream example rows use sst 222 / sd 00007B) authenticates and then cannot be admitted.INSERT stops at the first duplicate. Re-applying an SQL file of plain INSERT statements to a seeded database aborts at the first existing row with ERROR 1062 Duplicate entry, and the rest of the file is never applied. provision.sql uses INSERT … ON DUPLICATE KEY UPDATE for this reason.Container health is necessary but not sufficient. Check the core from the gNB's side as well:
| Check | Command | Expected |
|---|---|---|
| Containers | docker compose -f docker-compose-basic-nrf.yaml ps |
every container healthy |
| NG Setup | grep -aE 'is in service\|NGAP_REGISTER_GNB_CNF' /tmp/oai-gnb.log (soft-only tutorial) |
the gNB registered with the AMF and the cell is in service |
| NG Setup (7.2 O-DU) | DU console | associated AMF 1 |
| UE registered | AMF log | 5GMM-REGISTERED for the UE's SUPI |
| Data network | docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' oai-ext-dn then ping it from the UE |
0 % loss |
| Internet via NAT | docker exec oai-ext-dn ping 8.8.8.8 |
replies |
| Open5GS | OAI CN5G | Aether SD-Core | |
|---|---|---|---|
| Pairs with | OCUDU (srsRAN) | Duranta (OAI) | OCUDU, as a separate core host |
| Deployment | native processes on the RAN host | Docker Compose on the RAN host | Kubernetes (RKE2) + Helm on its own host |
| Subscriber store | MongoDB | MySQL, seeded from SQL | MongoDB, pushed from Helm values by simapp |
| Addresses | fixed (loopback) | assigned by Docker at start-up | fixed Service externalIP for the AMF |
Choose OAI CN5G when you run Duranta: every Duranta tutorial is written and verified against it, and the bring-up scripts already handle its dynamic addresses. It is also the natural core for OAI-native experiments, since the RAN and core come from the same project. Its limitations: the container images track OAI's develop tags, so the baked images are the tested versions and pulling new ones changes the core; the addresses move on every re-creation; and the core is built for one host. For a core that runs on its own server as Kubernetes workloads, see Aether SD-Core; for the OCUDU pairing, see Open5GS.
The tutorials that use OAI CN5G, in learning-path order:
| Step | Tutorial | Modality | Status |
|---|---|---|---|
| 1 | Duranta soft-only — gNB + nrUE over RFsim | no radio, one host | verified 2026-09-08 |
| 2 | Duranta over the air — USRP + commercial modem | USRP N310, Quectel modem | verified 2026-09-08 |
| 3 | Duranta 7.2 with a soft O-RU (nr-oru) |
O-RAN 7.2, soft O-RU + USRP B210 | verified 2026-09-16 |
| 4 | Duranta 5G SA over O-RAN 7.2 — commercial O-RUs | O-RAN 7.2, Benetel RAN650 / LiteOn FlexFi | verified 2026-09-11 (Benetel); measured 2026-09-22 |
The core does not limit throughput in these measurements; the radio, carrier and RAN server do. The rows show what a Duranta cell on OAI CN5G delivers. Platform A = 2× Intel Xeon Gold 6226 @ 2.70 GHz (24 cores / 24 threads, HT off); platform B = 2× Intel Xeon Gold 6126 @ 2.60 GHz (24 cores / 48 threads). The full matrix across platforms is on the Duranta page.
| Modality | UE | Platform | DL, Mbit/s | UL, Mbit/s | Date · image |
|---|---|---|---|---|---|
| Soft-only (RFsim) | nr-uesoftmodem |
A | 136–158 | 81–88 | 2026-09-17 · duranta-20260917 |
| Soft-only (RFsim) | nr-uesoftmodem |
B | 114–115 | 62.4–62.7 | 2026-09-08 · duranta-20260907c |
O-RAN 7.2, soft O-RU (nr-oru + B210, 20 MHz n78) |
Quectel RM520N-GL | B | 30.7–34.3 | 7.3 | 2026-09-16 · duranta-20260916 |
| O-RAN 7.2, Benetel RAN650 (100 MHz n78) | Quectel RM520N-GL | B | 186–218 | 57–61 | 2026-09-22 |
| O-RAN 7.2, LiteOn FlexFi (100 MHz n78) | Quectel RM520N-GL | B | 157–163 | 38–39 | 2026-09-22 |
| Symptom | Cause | Fix |
|---|---|---|
gNB: NGSetup fails / AMF not associated |
the AMF container's address changed; the .conf holds a stale one |
bash /root/patch-amf.sh <conf>, or re-run start-all.sh, then restart the gNB |
gNB: sctp_bindx … Cannot assign requested address |
the gNB .conf binds N2/N3 to an address the host does not have (OAI's stock file uses 192.168.71.140) |
set GNB_IPV4_ADDRESS_FOR_NG_AMF / _NGU to the host's demo-oai address |
UE reaches RRC_CONNECTED, then Authentication Reject |
the UE's IMSI and keys differ from the database | provision the subscriber from the inventory; for the soft UE use the tutorial subscriber 001010123456780 |
Modem: Unknown DNN reject |
the modem's APN is not the core's DNN | AT+CGDCONT=1,"IP","oai"; clear the back-off with AT+CFUN=1,1 |
| UE registers but no data, or UL works and DL replies never return | stale UPF tunnel state (F-TEID) after failed PDU cycles | docker compose restart oai-upf oai-smf; if that is not enough, down and up all NFs |
UE pings the ext-DN but not 8.8.8.8 |
ext-DN NAT or route stale | confirm with docker exec oai-ext-dn ping 8.8.8.8; restart the stack so oai-ext-dn re-reads UE_NETWORK=192.168.100.0/22 |
iperf3 → connection refused at an address that answers ping |
that address belongs to another container this boot | target docker inspect … oai-ext-dn |
Amarisoft lteue stuck in registering, Wrong security header type for security mode complete (0) |
the AMF lists the NULL algorithms (NIA0/NEA0) first, and the gNB offers only nea0 |
put NIA2/NEA2 first in the AMF's supported_integrity_algorithms / supported_encryption_algorithms and restart oai-amf; set the gNB's ciphering_algorithms = ( "nea2", "nea0" ) |
lteue: clean registration, then PDU session establishment reject |
lteue defaults attach_pdn_type to ipv4v6; this SMF serves only IPv4 for the DNN and rejects the mismatch rather than downgrading it |
add attach_pdn_type: "ipv4" to the UE's ue_list entry in the lteue config |
Author(s): COSMOS team · Last reviewed: 2026-09-24 (new page; facts from the Duranta tutorials, see each for its verification date) · Tested image/release: duranta.ndz (duranta-20260917; current duranta-w39-20261001) · Tags: oai, oai-cn5g, 5gc, core, docker, duranta, 5g, sa, overview