Build guide/ 5G SA testbed/ Kali Linux

Open5GS + srsRAN on Kali

Building the 5G Standalone core and NR gNodeB that this dissertation testbed runs on. Neither project builds out of the box here: Kali's libmongoc packaging breaks the Open5GS build in three separate ways, the srsRAN GitHub repository was archived in December 2025, and a stock core will start cleanly and then register nothing.

Verified environment Confirmed on the running testbed  2026-09-05
Host OS
Kali Rolling 2026.3
Compiler
GCC 15.3.0
CMake
4.3.4
Meson / Ninja
1.11.1 / 1.13.2
MongoDB
8.0.29
libmongoc
2.3.3-1was 2.3.1 at first build — see Step 3
UHD
4.9.0.1
Open5GS
v2.7.7
srsRAN Project
release_25_10commit d2f4b70
UERANSIM
v3.2.6optional — software UEs

Scope

This gets you to a cell that real handsets attach to, and a core that registers them. It does not cover the project's NGAP proxy, feature extraction or ML pipeline.

The order below is load-bearing. Step 3 must complete before Step 4 or the Open5GS build fails at the first #include; Step 5 must complete before anything registers at all.

Hardware

ItemRequirement
SDRUSRP B210, on a genuine USB 3.0 port. 20 MHz n78 needs 23.04 Msps, which USB 2.0 cannot sustain.
UEs5G SA handsets with programmable MILENAGE USIMs, and/or UERANSIM software UEs
RF containmentFaraday bag or shielded room. Band n78 is licensed spectrum.
Host≥16 GB RAM

Step 00UHD firmware and B210 detection

One-time per machine:

shell
uhd_images_downloader        # FPGA images → /usr/lib/uhd/images/
uhd_find_devices             # expect: type=b200, plus your board serial
Verify before you build

uhd_find_devices succeeding is not proof the link is fast enough — it succeeds on USB 2.0 too. Check the negotiated speed now, rather than after an hour of compiling.

shell
lsusb -t   # the B210 line must end in 5000M (USB 3.0), not 480M (USB 2.0)

Step 01System dependencies

shell
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y \
  meson ninja-build build-essential flex bison \
  cmake libsctp-dev libgnutls28-dev libgcrypt-dev \
  libssl-dev libidn11-dev libbson-dev libmicrohttpd-dev \
  libcurl4-gnutls-dev libnghttp2-dev libyaml-dev \
  libtalloc-dev libpcsclite-dev pcscd libtins-dev \
  libfftw3-dev libmbedtls-dev libboost-all-dev \
  libconfig++-dev libyaml-cpp-dev libzmq3-dev cppzmq-dev \
  libuhd-dev uhd-host python3-pip
If apt refuses to run

A "dpkg was interrupted" error is typically an abandoned interactive iperf3 postinst prompt. Clear it non-interactively: sudo DEBIAN_FRONTEND=noninteractive dpkg --configure -a

Step 02MongoDB 8.0

Kali has no MongoDB package of its own — use the upstream Debian repository.

shell
curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc | \
  sudo gpg --dearmor -o /usr/share/keyrings/mongodb-server-8.0.gpg

echo "deb [ signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] \
  https://repo.mongodb.org/apt/debian bookworm/mongodb-org/8.0 main" | \
  sudo tee /etc/apt/sources.list.d/mongodb-org-8.0.list

sudo apt-get update && sudo apt-get install -y mongodb-org
sudo mkdir -p /var/lib/mongodb /var/log/mongodb
sudo mongod --dbpath /var/lib/mongodb --logpath /var/log/mongodb/mongod.log --fork

mongosh --quiet --eval "db.adminCommand('ping')"

Step 03Kali libmongoc fixups

This is what makes a Kali install differ from the upstream Open5GS instructions, and the step most likely to break again later.

Debian and Kali ship libmongoc 2.x under the mongoc2 name, with headers in a versioned directory and a pkg-config file that does not use the upstream name. Open5GS 2.7.7 expects the 1.x layout — a libmongoc-1.0.pc and a top-level <mongoc.h>. Three distinct failures follow.

Fix 1 — pkg-config name symlinks

shell
PC=/usr/lib/x86_64-linux-gnu/pkgconfig
sudo ln -sf $PC/mongoc2.pc        $PC/libmongoc-1.0.pc
sudo ln -sf $PC/mongoc2-static.pc $PC/libmongoc-static-1.0.pc
sudo ln -sf $PC/bson2.pc          $PC/libbson-1.0.pc
sudo ln -sf $PC/bson2-static.pc   $PC/libbson-static-1.0.pc
Without this

meson setup fails with Dependency libmongoc-1.0 not found.

Fix 2 — header shims

Kali installs headers at /usr/include/mongoc-<version>/mongoc/mongoc.h, but Open5GS does #include <mongoc.h>. Create a one-line shim at the top of each versioned include directory.

Derive the paths — don't hardcode the version

This testbed was first built against mongoc 2.3.1; Kali has since moved to 2.3.3. A shim written for one version is silently invisible to the next, and the build then fails with the same fatal error: mongoc.h: No such file or directory as if the fixup had never been applied at all.

shell
MONGOC_INC="$(pkg-config --variable=prefix libmongoc-1.0)/include/mongoc-$(pkg-config --modversion libmongoc-1.0)"
BSON_INC="$(pkg-config --variable=prefix libbson-1.0)/include/bson-$(pkg-config --modversion libbson-1.0)"

echo '#include "mongoc/mongoc.h"' | sudo tee "$MONGOC_INC/mongoc.h"
echo '#include "bson/bson.h"'     | sudo tee "$BSON_INC/bson.h"

Verify the shim resolves before building anything:

shell
printf '#include <mongoc.h>\n#include <bson.h>\nint main(void){return 0;}\n' > /tmp/probe.c
gcc $(pkg-config --cflags libmongoc-1.0) -c /tmp/probe.c -o /dev/null && echo "shim OK"
Re-run after any libmongoc upgrade

The shims live inside the versioned include directory, so an apt upgrade that bumps libmongoc-dev strands them in the old directory. Existing binaries keep working — only rebuilds break, which makes this a confusing failure to hit months later.

Step 04Build Open5GS 2.7.7

shell
git clone --depth 1 --branch v2.7.7 https://github.com/open5gs/open5gs.git
cd open5gs

Fix 3 — disable the bundled test suite

tests/common/context.c calls mongoc_collection_count(), removed in libmongoc 2.x. The tests fail to compile even though the daemons build fine.

shell
sed -i 's/^if build_tests/if false # build_tests/' meson.build

A build-only change: it disables Open5GS's own unit tests and touches no core functionality, which is why the repository ships no patch file for it.

Build and install

shell
meson setup build --prefix=/usr/local
ninja -C build -j"$(nproc)"
sudo ninja -C build install
sudo ldconfig

/usr/local/bin/open5gs-amfd -v     # Open5GS v2.7.7
ArtefactLocation
Binaries/usr/local/bin/open5gs-*d
Configs/usr/local/etc/open5gs/*.yaml
SUCI home-network keys/usr/local/etc/open5gs/hnet/

Step 05Configure Open5GS for PLMN 001/01

Symptom if you skip this

A stock install starts, registers nothing, and returns 504 Gateway Timeout on every discovery request. Four separate config problems cause it, and all four must be fixed together — fixing three still yields a dead core.

1 — NRF must allow the test PLMN

nrf.yaml defaults to mcc: 999, mnc: 70 and rejects every NF registering as anything else: PLMN-ID[MCC:001,MNC:01] is not allowed.

nrf.yaml
nrf:
  serving:
    - plmn_id:
        mcc: 001
        mnc: 01
  sbi:
    server:
      - address: 127.0.0.10
        port: 7777

2 — Every NF needs an explicit serving section

ausf, udm, udr, pcf, nssf and bsf ship without one, so they register under the default PLMN 999/70 and the AMF cannot discover them for 001/01.

each NF yaml
<nf>:
  serving:
    - plmn_id:
        mcc: 001
        mnc: 01

3 — Route through the NRF, not the SCP

Every NF config defaults to client.scp: http://127.0.0.200:7777. The SCP expects a SEPP for inter-PLMN routing, which a single-PLMN private network does not have, so discovery fails with No SEPP configured. Change client.scp to client.nrf in every NF config.

each NF yaml
  sbi:
    client:
      nrf:
        - uri: http://127.0.0.10:7777

4 — SBI address map

NFSBI addressNFSBI address
NRF127.0.0.10UDM127.0.0.12
SMF127.0.0.4PCF127.0.0.13
AMF127.0.0.5NSSF127.0.0.14
UPF127.0.0.7BSF127.0.0.15
AUSF127.0.0.11UDR127.0.0.20

AMF — NGAP, GUAMI, TAI and network name

amf.yaml
amf:
  sbi:
    server:
      - address: 127.0.0.5
        port: 7777
    client:
      nrf:
        - uri: http://127.0.0.10:7777
  ngap:
    server:
      - address: 127.0.0.5
  guami:
    - plmn_id: { mcc: 001, mnc: 01 }
      amf_id: { region: 2, set: 1 }
  tai:
    - plmn_id: { mcc: 001, mnc: 01 }
      tac: 1
  plmn_support:
    - plmn_id: { mcc: 001, mnc: 01 }
      s_nssai:
        - sst: 1
  security:
    integrity_order: [NIA2, NIA1, NIA0]
    ciphering_order: [NEA0, NEA1, NEA2]
  network_name:
    full: srsRAN 5G Test
    short: srsTest
  amf_name: open5gs-amf0
  time:
    t3512:
      value: 540

SMF and UPF — UE subnet

smf.yaml & upf.yaml
  session:
    - subnet: 10.45.0.0/16
      gateway: 10.45.0.1
      dnn: internet

smf.yaml additionally takes DNS (8.8.8.8, 8.8.4.4) and mtu: 1400. The gtpc/gtpu and freeDiameter entries in the installed file are 4G EPC legacy, unused in 5G SA — leave them alone.

ogstun comes up DOWN

The UPF creates the TUN interface but neither brings it up nor assigns the gateway address. Until you do, a UE gets an IP and passes no traffic.

shell
sudo ip link set ogstun up
sudo ip addr add 10.45.0.1/16 dev ogstun

UDM home-network keys

udm.yaml references six SUCI concealment keys under /usr/local/etc/open5gs/hnet/, installed by ninja install. UDM will not start if they are missing. Test USIMs using the SUCI null-scheme never exercise them, but they must still be present.

Step 06Provision subscribers

Subscribers go directly into MongoDB. The full profile set — five physical USIMs and three UERANSIM software profiles — is in COMP997_srsRAN_subscribers.md, with a ready-to-paste insertMany block.

Key material

Ki and OPc are redacted throughout the public repository. Substitute your own USIM credentials for the REDACTED placeholders. The IMSI prefix 001010000000xxx is not sensitive: MCC 001 / MNC 01 is the 3GPP test PLMN, assigned to no real operator.

mongosh — document shape
{
  imsi: "001010000000001",
  msisdn: [], imeisv: [],
  security: { k: "REDACTED", op: null, opc: "REDACTED",
              amf: "8000", sqn: NumberLong("0") },
  ambr: { downlink: { value: 1, unit: 3 }, uplink: { value: 1, unit: 3 } },
  slice: [{ sst: 1, default_indicator: true,
            session: [{ name: "internet", type: 3,
                        ambr: { downlink: { value: 1, unit: 3 },
                                uplink:   { value: 1, unit: 3 } },
                        qos: { index: 9, arp: { priority_level: 8,
                               pre_emption_capability: 1,
                               pre_emption_vulnerability: 1 } } }] }],
  access_restriction_data: 32, network_access_mode: 0,
  subscriber_status: 0, operator_determined_barring: 0, __v: 0
}
verify
mongosh open5gs --eval 'db.subscribers.find({}, {imsi:1, _id:0}).sort({imsi:1})'
First attach always logs one auth failure

A new SIM's first attach logs Authentication failure(Synch failure[count=0]). That is normal MILENAGE SQN resync and registration completes on the immediate retry. If authentication keeps failing, reset: mongosh open5gs --eval 'db.subscribers.updateMany({}, {$set: {"security.sqn": NumberLong("0")}})'

Step 07Build srsRAN Project 25.10

The repository is archived

srsRAN_Project was archived on GitHub in December 2025. Its default branch now holds only a README pointing at GitLab — so a plain git clone appears to succeed and gives you nothing. You must clone an explicit release tag.

shell
git clone --depth 1 --branch release_25_10 https://github.com/srsran/srsRAN_Project.git
mkdir -p srsRAN_Project/build && cd srsRAN_Project/build

cmake .. \
  -DCMAKE_BUILD_TYPE=Release \
  -DENABLE_EXPORT=ON \
  -DENABLE_UHD=ON \
  -DENABLE_ZEROMQ=ON \
  -DBUILD_TESTING=OFF

make -j"$(nproc)" gnb

Binary lands at build/apps/gnb/gnb. srsRAN builds cleanly under GCC 15 — unlike Open5GS, it needs no patches. It also declares cmake_minimum_required(VERSION 3.14), comfortably above the 3.5 floor CMake 4.x enforces, so no CMAKE_POLICY_VERSION_MINIMUM workaround is needed.

gNB configuration

gnb.yml
cu_cp:
  amf:
    addr: 127.0.0.5
    port: 38412
    bind_addr: 127.0.0.1
    supported_tracking_areas:
      - tac: 1
        plmn_list:
          - plmn: "00101"
            tai_slice_support_list:
              - sst: 1

ru_sdr:
  device_driver: uhd
  device_args: type=b200,serial=YOUR_B210_SERIAL,num_recv_frames=64,num_send_frames=64
  srate: 23.04
  otw_format: sc12
  tx_gain: 89
  rx_gain: 50

cell_cfg:
  dl_arfcn: 632628
  band: 78
  channel_bandwidth_MHz: 20
  common_scs: 30
  plmn: "00101"
  tac: 1
  pci: 1

log:
  filename: /tmp/gnb.log
  all_level: info

Two settings are not optional:

  • num_recv_frames=64,num_send_frames=64 — without these the B210 underruns regardless of USB generation.
  • tx_gain: 89 (the B210 maximum) and rx_gain: 50. At tx_gain: 80 some handsets never saw the cell while others attached fine — a confusing failure, because the cell is genuinely on-air throughout.

Step 08UERANSIM v3.2.6 — optional

Only needed for the SW-Std / SW-Ext / SW-Min software UE profiles. Skip if you are working with physical handsets only.

Kali / GCC 15 build fixups

UERANSIM's 2022-era code relies on standard-library headers that older GCC pulled in transitively; GCC 15 does not. Two changes:

  1. src/ext/yaml-cpp/emitterutils.cpp — add #include <cstdint>.
  2. Top-level CMakeLists.txt — force-include globally, which is cheaper than patching every offending file:
CMakeLists.txt
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -include cstdint -include cstring -include cstdio -include string")

Capability-enquiry patch

Stock UERANSIM never sends the message this project studies

There is no RRC UECapabilityEnquiry / UECapabilityInformation implementation on either the gNB or the UE side, so no UERadioCapabilityInfoIndication is ever emitted — meaning the NGAP proxy has nothing to intercept for software profiles. ueransim.patch in the repository adds it.

shell
git apply /path/to/ueransim.patch
cp /path/to/ueransim-config/gnb.yaml config/gnb.yaml
make build      # produces build/nr-gnb, build/nr-ue, build/nr-cli
Two known quirks

If nr-gnb is left running across a restart of whatever it connects to, it can wedge in a broken internal AMF-context state (AMF context not found with id: 0) while its SCTP transport still looks healthy. Restart it fresh rather than trusting its reconnect logic.

Its GTP/UDP task will also fail to bind (Address already in use) when a real srsRAN gNB is running, since both claim port 2152 on loopback. Harmless for NGAP signalling; user-plane data will not flow for UERANSIM UEs in that configuration.

Bring-up and verification

Start order matters: MongoDB, then NRF and SCP, then the remaining NFs, then AMF and SMF, then UPF, then the gNB.

minimal manual sequence
sudo mongod --dbpath /var/lib/mongodb --logpath /var/log/mongodb/mongod.log --fork

for nf in nrf scp ausf udm udr pcf nssf bsf amf smf upf; do
  sudo /usr/local/bin/open5gs-${nf}d > /tmp/${nf}.log 2>&1 &
  sleep 1
done

sudo ip link set ogstun up
sudo ip addr add 10.45.0.1/16 dev ogstun 2>/dev/null || true
sudo sysctl -w net.ipv4.ip_forward=1

UPLINK=$(ip route show default | awk '{print $5; exit}')
sudo iptables -t nat -A POSTROUTING -s 10.45.0.0/16 -o "$UPLINK" -j MASQUERADE

sudo /path/to/srsRAN_Project/build/apps/gnb/gnb \
  -c /root/.config/open5gs/gnb.yml > /tmp/gnb.log 2>&1 &

Verification checklist

CheckCommandExpect
AMF listening on N2ss -lntu | grep 38412a listening socket
NG Setup succeededgrep -i "ng setup" /tmp/gnb.logsuccess, PLMN 00101
UE registeredgrep "Registration complete" /tmp/amf.logone line per UE
UE got an IPgrep "UE IPv4" /tmp/smf.logaddress in 10.45.0.0/16
Data pathping -I ogstun 10.45.0.2replies
RF underflowsgrep -c underflow /tmp/gnb.lognear zero

Open5GS logs carry ANSI colour codes; strip them with sed 's/\x1b\[[0-9;]*m//g' before grepping if matches look oddly absent.

Confirming the cell is actually transmitting

Protocol-level evidence is the primary check: PRACH detection in /tmp/gnb.log, NG Setup, and a successful registration. For an independent RF-layer confirmation, a wideband hackrf_sweep is not reliable — it produces noisy readings with no stable peak. Use GQRX narrowband instead.

GQRX settingValue
Frequency3489420000 Hz
Sample rate20000000
LNA / IF gain16 dB
VGA / BB gain20 dB
RF ampoff — the B210 transmits at max gain right beside it

The carrier appears as a sharp spike around −35 to −40 dBFS against a −85 to −90 dBFS noise floor, with visible TDD burst structure in the waterfall.

Two GQRX traps

The LNA / VGA / RF-amp sliders live in the Input controls tab of the Receiver Options dock, not in "Configure I/O devices". And GQRX does not auto-start — click ▶ (Start/Stop DSP) or the waterfall stays black no matter how correct your settings are.

Troubleshooting

Install-time and bring-up failures encountered on this testbed, with root causes.

SymptomCauseFix
apt-get fails: "dpkg was interrupted"Abandoned interactive postinst promptDEBIAN_FRONTEND=noninteractive dpkg --configure -a
meson: Dependency libmongoc-1.0 not foundKali names the file mongoc2.pcpkg-config symlinks — Step 3, Fix 1
fatal error: mongoc.h: No such file or directoryHeaders live at mongoc-<ver>/mongoc/mongoc.hHeader shims — Step 3, Fix 2. If this appears after a previously working build, libmongoc-dev was upgraded and the shim is stranded in the old directory.
implicit declaration of function 'mongoc_collection_count'Removed in libmongoc 2.x; Open5GS tests still call itDisable tests — Step 4, Fix 3
git clone of srsRAN yields only a READMEGitHub repo archived Dec 2025Clone --branch release_25_10
Every AMF discovery returns 504; SCP logs No SEPP configuredNFs routing via SCP, which needs a SEPPclient.scpclient.nrf
NRF logs PLMN-ID[MCC:001,MNC:01] is not allowednrf.yaml still serving default 999/70Step 5, item 1
AMF discovery for AUSF/UDM returns emptyThose NFs registered under PLMN 999/70Add serving: to every NF
UDM will not startMissing hnet/ SUCI key filesReinstall — they come from ninja install
UE gets an IP but no traffic; ping -I ogstun → "Network is unreachable"UPF leaves ogstun DOWNip link set ogstun up
One handset sees the cell, another does nottx_gain too low for that device at that distancetx_gain: 89, rx_gain: 50
Thousands of Real-time failure in RF: underflow, despite NG Setup succeedingB210 on USB 2.0. Both lsusb and uhd_find_devices succeed on USB 2.0 — presence is not proof of link speed.Move to a true USB 3.0 (xhci_hcd) root hub; confirm 5000M in lsusb -t. Isolated single-slot underflows under load remain normal.
UE registers but never gets a PDU session; gNB loops UE did not request a PDU session … Requesting UE releaseDevice's APN/DNN profile requests something other than internet (often ims) with no fallbackFix on the device's APN config, not the network. Check /tmp/amf.log for the requested DNN.
Recurring Ue requested DNN "ims" Not Supported every ~16sHandset probing for a VoNR/IMS bearer Open5GS does not provideHarmless. Disable VoNR on the handset, or ignore.
Registration succeeds, then drops every 15–90sRF link quality — PUSCH SINR collapsing to −20…−35 dB means uplink is failing outrightAntenna positioning. Not a software fault.
CPE unreachable by ping despite a working sessionMany CPEs silently drop unsolicited ICMPCheck for live flows instead: tcpdump -i ogstun host <ip>

Network parameters

MCC / MNC001 / 01  (3GPP test PLMN)
TAC1
Bandn78  (TDD, 3300–3800 MHz)
DL ARFCN632628  (3489.42 MHz)
SSB ARFCN632256
Channel bandwidth20 MHz
Subcarrier spacing30 kHz
S-NSSAISST=1  (eMBB)
AMF NGAP127.0.0.5:38412
UE IP pool10.45.0.0/16
UPF TUNogstun  (10.45.0.1/16)
DNNinternet
DNS8.8.8.8, 8.8.4.4
TX / RX gain89 dB / 50 dB