Bring up a complete 5G Standalone network — the Duranta gNB (nr-softmodem), the native OAI CN5G core, and OAI's soft UE (nr-uesoftmodem) — entirely in software on one node over the OAI RF simulator (RFsim). No SDR, no RF, no radio reservation. Register the UE, open a PDU session, reach the internet, and measure throughput.
This is the fastest way to see the full OAI/Duranta 5G SA stack work end-to-end. The gNB runs as an RFsim server and OAI's nr-uesoftmodem as an RFsim client, exchanging baseband samples over a TCP socket instead of a USRP — so the whole stack (gNB + OAI CN5G core + UE) runs on a single COSMOS server node. It is license-free and needs no radio hardware, so it runs on any COSMOS node.
Because there is no air interface, throughput is high and limited only by the CPU (DL 114–148 / UL 62–80 Mbit/s on the current image, depending on the server — see Performance). For the over-the-air version with a real USRP and a commercial modem, see the companion Duranta OTA tutorial.
After completing this tutorial you will be able to:
nr-uesoftmodem in software over RFsim on one node.LD_LIBRARY_PATH requirement.192.168.100.0/22 pool, and reach the internet.| Difficulty | Beginner–Intermediate |
| Estimated time | 20–30 min |
| Domain / sandbox | any COSMOS server node (no radio needed) |
| Topic group | Cellular (4G/5G/O-RAN) |
| Last verified | 2026-09-08 — three-mode parity green on srv19.osc (image duranta-20260917, Duranta 2026.w37) |
| Upstream source | Duranta (LF Networking, an OpenAirInterface-based RAN + UE stack) — repo github.com/duranta-project/openairinterface5g, tag 2026.w36 (commit b3930e39f5, 2026-09-04) + two COSMOS patches (PUSCH TA-alias guard; Benetel M-Plane get-xml/get-yang) |
Background knowledge
ssh on Linux.Account & access
Devices / nodes
| Resource | Role | Qty | Notes |
|---|---|---|---|
| any COSMOS server node | gNB + OAI CN5G + nr-uesoftmodem (all-in-one) |
1 | no SDR — pure software |
Disk images
| Image | Load onto | Provides |
|---|---|---|
duranta.ndz |
your node | Duranta nr-softmodem + nr-uesoftmodem (/opt/duranta), OAI CN5G (/opt/oai-cn5g), configs, UHD 4.9 |
(duranta.ndz is a symlink to the latest dated build.)
Software components
| Component | Version | Source |
|---|---|---|
| Duranta gNB / nrUE (OAI fork) | preinstalled | /opt/duranta/cmake_targets/ran_build/build/{nr-softmodem,nr-uesoftmodem} |
| OAI CN5G (5GC) | develop (Docker) |
/opt/oai-cn5g/oai-cn5g-fed/docker-compose |
Spectrum / RF / special
00101, DNN oai, IMSI 001010123456780; UE pool 192.168.100.0/22 (shared with all COSMOS 5G stacks).Everything on one node; gNB ↔ UE over an RFsim TCP socket, UE ↔ core over the Docker bridge:
any COSMOS node
┌────────────────────────────────────────────────┐
│ OAI CN5G (Docker): AMF/SMF/UPF + oai-ext-dn │
│ UE pool 192.168.100.0/22, NAT → internet │
│ ▲ N2/N3 │
│ nr-softmodem ──RFsim TCP:4043──► nr-uesoftmodem │
│ (RFsim server, band n78) (RFsim client / UE) │
└────────────────────────────────────────────────┘
-r 0):ssh <username>@console.<domain>.cosmos-lab.org
omf load -i duranta.ndz -t <node> -r 0 -o 1200
omf tell -a on -t <node>
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"
The core hands out UE IPs from 192.168.100.0/22; the oai-ext-dn container NATs UE traffic to the internet.
OAI dlopens its lib*.so with an rpath baked to the original build dir, so every launch must set LD_LIBRARY_PATH to the (relocated) build directory:
B=/opt/duranta/cmake_targets/ran_build/build
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
cd "$B"
setsid bash -c "export LD_LIBRARY_PATH=$B; exec ./nr-softmodem -O /root/duranta/gnb-rfsim.conf --rfsim" >/tmp/oai-gnb.log 2>&1 &
sleep 10
grep -aE 'is in service|NGAP_REGISTER_GNB_CNF' /tmp/oai-gnb.log | tail -2
[NR_RRC] cell PLMN 001.01 Cell ID ... is in service
B=/opt/duranta/cmake_targets/ran_build/build
cd "$B"
# Since Duranta 2026.w36 (still true on 2026.w37) the UE needs the carrier on its command line (it asserts in get_freq_range_from_freq
# without it). The gNB prints the exact set in its log -- reuse it verbatim:
UEARGS=$(grep -a "Command line parameters for OAI UE" /tmp/oai-gnb.log | head -1 | sed -E 's/.*OAI UE: *//; s/\x1b\[[0-9;]*m//g')
echo "$UEARGS" # e.g. -C 3319680000 -r 106 --numerology 1 --band 78 --ssb 516
setsid bash -c "export LD_LIBRARY_PATH=$B; exec ./nr-uesoftmodem -O /root/duranta/ue-rfsim.conf --rfsim --rfsimulator.serveraddr 127.0.0.1 $UEARGS" >/tmp/oai-ue.log 2>&1 &
sleep 12
grep -aiE 'PDU Session|oaitun|192.168.100' /tmp/oai-ue.log | tail -3
ip -br addr show oaitun_ue1
[NAS] PDU Session Establishment Accept, UE IPv4 192.168.100.2
oaitun_ue1 UNKNOWN 192.168.100.2/22
DN=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' oai-ext-dn) # the ext-DN address is dynamic (see Troubleshooting)
ping -I oaitun_ue1 -c 4 $DN # OAI ext-DN
ping -I oaitun_ue1 -c 4 8.8.8.8 # the internet
4 packets transmitted, 4 received, 0% packet loss
The step-by-step above is Mode 1 (manual CLI). The same experiment ships as an Ansible playbook and a Jupyter notebook that read the same parameter file, use the same log parser and emit the same JSON result contract, so the three can be checked against each other.
On the console of your domain (console.sb1.cosmos-lab.org or console.osc.cosmos-lab.org):
cd ~ && wget -q https://www.cosmos-lab.org/files/cosmos-tutorials-duranta-soft.zip
unzip -o cosmos-tutorials-duranta-soft.zip # extracts into ~/tutorials/
cd ~/tutorials && ls duranta-soft
| File | Purpose |
|---|---|
duranta-soft_params.yml |
the single parameter source (paths on the image, PLMN/IMSI, timeouts, iperf window) |
duranta-soft.yml |
Mode 2 — the Ansible playbook (cosmos_preflight first, then core → gNB → UE → measure → contract) |
duranta-soft_manual.sh |
Mode 1 scripted: the exact commands of this page, node by node |
duranta-soft_tutorial.ipynb |
Mode 3 — the notebook (kernel on the console, papermill-runnable) |
duranta-soft_lib.py |
shared parser + result contract + the parity comparison spec |
duranta-soft_parity.sh |
runs all three modes on one node and asserts they agree |
duranta-soft_teardown.yml |
clean teardown (stops the named systemd units and the core) |
OMF_NODESis required. The inventory refuses to run without it, so a playbook can never touch a node you did not name. This tutorial takes exactly one node.
cd ~/tutorials
export OMF_NODES="srv1-lg1" # your reserved node (srv19 on osc)
ansible-playbook duranta-soft/duranta-soft.yml
The playbook starts the gNB and the UE as named systemd units (duranta-gnb, duranta-ue) with LD_LIBRARY_PATH set for the relocated build, rewrites the AMF address into gnb-rfsim.conf from docker inspect, derives the UE's carrier arguments from the gNB log, waits for oaitun_ue1 to carry an address (a deterministic readiness check, not a sleep), then measures ping and iperf3 to the local ext-DN and ping to 8.8.8.8. Outputs land in $DURANTA_SOFT_RUN_DIR (default /tmp/duranta-soft-$USER), including the contract duranta_contract_ansible.json.
export OMF_NODES="srv1-lg1"
/opt/cosmos-jupyter/venv/bin/papermill duranta-soft/duranta-soft_tutorial.ipynb duranta-soft/duranta-soft_tutorial-out.ipynb
or open the notebook in the console's JupyterLab and run it top to bottom; the last cell plots DL/UL throughput and prints the contract.
bash duranta-soft/duranta-soft_parity.sh srv1-lg1
=== duranta-soft parity ===
mode ue_ipv4 ping_rtt_ms ping_remote_loss_pct dl_mbps ul_mbps node
ansible 192.168.100.6 15.91 0.0 115.0 62.4 srv19
manual 192.168.100.7 16.92 0.0 115.0 62.7 srv19
jupyter 192.168.100.8 15.67 0.0 114.0 62.7 srv19
dl_mbps spread max/min = 1.01 (tol 1.35)
ul_mbps spread max/min = 1.00 (tol 1.40)
PARITY: PASS (same experiment in all modes; all checks green)
(run on srv19.osc, 2026-09-08). The pass criteria are deterministic: every mode reaches cell in service, RRC connected and a PDU-session address; local ping loss ≤ 5 %; DL in 30–400 and UL in 10–200 Mbit/s and within 35 %/40 % of each other across modes. Green parity is what sets this page's Last verified; re-running it after an image refresh is the regression test.
cell PLMN 001.01 … is in service.PDU Session Establishment Accept; oaitun_ue1 has an IP in 192.168.100.0/22.ping $DN (the oai-ext-dn address) and ping 8.8.8.8 = 0% loss.docker exec -d oai-ext-dn iperf3 -s; sleep 2 # give the server a moment before the first client
DN=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' oai-ext-dn) # the ext-DN address is dynamic (see Troubleshooting)
iperf3 -c $DN -B 192.168.100.2 -R -t 12 # DL
iperf3 -c $DN -B 192.168.100.2 -t 12 # UL
Measured (RFsim, no air link, TCP iperf3 receiver line), by the server's CPU platform. With no radio, the CPU is the only limit, so it matters more here than in any other modality:
| Date | CPU platform | Image | Method | DL, Mbit/s | UL, Mbit/s | RTT |
|---|---|---|---|---|---|---|
| 2026-09-14 | 1× AMD EPYC 9355P, 32 cores / 32 threads | duranta-20260907c |
the parity bundle, three modes | 442–447 | 198–201 | 25–45 ms |
| 2026-09-08 | 2× Intel Xeon Gold 6126 @ 2.60 GHz, 24 cores / 48 threads | duranta-20260907c |
the parity bundle, three modes (see above) | 114–115 | 62.4–62.7 | 16–17 ms |
| 2026-09-07 | 2× Intel Xeon Gold 6226 @ 2.70 GHz, 24 cores / 24 threads | duranta-20260907c (Duranta 2026.w36) |
manual | 148 | 80 | 13–15 ms |
| 2026-09-04 | 2× Intel Xeon Gold 6226 @ 2.70 GHz, 24 cores / 24 threads | duranta-20260905b |
manual | 145 | 81 | 13–16 ms |
| 2026-09-04 | 2× Intel Xeon Gold 6126 @ 2.60 GHz, 24 cores / 48 threads | duranta-20260905b |
manual | 121 | 54 | 13–16 ms |
| 2026-07-15 | not recorded | duranta.ndz of that date |
manual | ≈ 164 | ≈ 84 | ≈ 20 ms |
pkill -x nr-uesoftmodem; pkill -x nr-softmodem
docker compose -f /opt/oai-cn5g/oai-cn5g-fed/docker-compose/docker-compose-basic-nrf.yaml down
omf tell -a offh -t <node>
Bundle changes 2026-09-14 (republished
cosmos-tutorials-duranta-soft.zip): the parity harness now runs papermill with--cwd(the notebook no longer falls back to an older~/tutorialscopy) and runs the tutorial's teardown on every exit ("releasing the rig" / "rig released"). These harness changes were validated with the OCUDU bundles on 2026-09-14; the Duranta bundle itself was not re-run, so Last verified is unchanged.
| Symptom | Likely cause | Fix |
|---|---|---|
dlopen(libparams_libconfig.so) … No such file → exits |
relocated build, stale rpath | export LD_LIBRARY_PATH=/opt/duranta/cmake_targets/ran_build/build before launch |
gNB: sctp_bindx … Cannot assign requested address → No AMF is associated; nrUE stalls after RRCSetupComplete |
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 bridge address (Step 2) |
ping 192.168.70.131 works but iperf3 says connection refused / unable to send control message |
Docker assigns the demo-oai addresses in start order — .131 is not always oai-ext-dn (it was mysql on one boot) |
target docker inspect … oai-ext-dn (the $DN above), never a hardcoded address |
nrUE exits at start with Assertion (0) failed! … get_freq_range_from_freq() |
Duranta ≥ 2026.w36: the UE needs the carrier/band on its command line | append the gNB log's Command line parameters for OAI UE: set (-C … -r 106 --numerology 1 --band 78 --ssb 516) as in Step 3 |
nrUE reaches RRC_CONNECTED, then Authentication Reject |
ue-rfsim.conf IMSI/K/OPc differ from what provision.sql seeded |
use the tutorial subscriber 001010123456780 / K 00112233445566778899AABBCCDDEEFF / OPc 63BFA50EE6523365FF14C1F45F88737D (the shipped ue-rfsim.conf on duranta-20260907c already does) |
iperf3: unable to connect to server right after starting it |
server not up yet | docker exec -d oai-ext-dn iperf3 -s again and wait 2 s |
gNB: NGSetup fails / AMF not associated |
AMF container IP is dynamic | re-read docker inspect oai-amf and set amf_ip_address in the gNB .conf (Step 2) |
nrUE registers but no oaitun_ue1 / UL works but DL replies don't |
stale UPF F-TEID | docker compose restart oai-upf oai-smf |
UE ping 8.8.8.8 fails, ext-DN pings |
ext-dn NAT stale | confirm docker exec oai-ext-dn ping 8.8.8.8; the ext-dn UE_NETWORK must be 192.168.100.0/22 |
sb1/grid).2026.w36 (commit b3930e39f5); upstream OAI docs at gitlab.eurecom.fr/oai/openairinterface5g.omf-duranta; OAI libconfig + build — skill oai-gnb.basic-nrf config. Upstream: see the Duranta entry above.Author(s): COSMOS team · Last verified: 2026-09-08 (three-mode parity on srv19.osc, duranta-20260907c; first verified 2026-07-15) · Tested image/release: duranta.ndz = duranta-20260907c, built from Duranta tag 2026.w36 (Duranta/OAI nr-softmodem + nr-uesoftmodem + OAI CN5G), PLMN 00101, pool 192.168.100.0/22 · Bundle: cosmos-tutorials-duranta-soft.zip · Tags: duranta, oai, openairinterface, oai-cn5g, 5g, sa, rfsim, soft, nruesoftmodem