Real RAN Integration Guide
This guide covers every parameter and network change required to connect a physical or third-party software gNB to this 5GC implementation. It is structured as a top-to-bottom checklist: complete each section in order and you will have a working end-to-end 5G SA system.
1. Network Topology
Section titled “1. Network Topology”The core uses five isolated Docker bridge networks. In a real deployment, the gNB must reach two of them (N2 and N3); all other interfaces remain internal.
┌──────────────────────────────────────────────────┐ │ 5GC Server │ │ │ gNB ──── N2 (NGAP) ──► AMF :38412/SCTP (172.30.1.0/24 in dev) │ ──── N3 (GTP-U) ──► UPF :2152/UDP (172.30.3.100 in dev) │ │ │ │ │ └── N6 (TUN upfgtp0) ──► Internet / DN │ │ │ │ Internal SBI (172.30.0.0/24): NRF/AUSF/UDM/... │ └──────────────────────────────────────────────────┘In production the AMF and UPF bind 0.0.0.0 so Docker publishes their ports to the host. The
gNB must point at the host’s external IP.
2. PLMN (MCC / MNC)
Section titled “2. PLMN (MCC / MNC)”PLMN must match across the entire system: all NFs, every gNB, and every UE SIM/config.
Current value: MCC 001, MNC 01 (test PLMN, TS 23.003 §4.6)
Single file to edit
Section titled “Single file to edit”All NFs read PLMN from one shared file:
config/operator.yamlplmn: mcc: "001" # ← change here only mnc: "01" # ← change here onlyEvery NF container mounts this file at /etc/5gc/operator.yaml (see docker-compose.yml).
Changing config/operator.yaml and restarting the stack is all that is required — no per-NF
config file needs to be touched.
Subscriber database
Section titled “Subscriber database”When MCC/MNC changes you must also update the seeded subscriber IMSIs. SUPI format is
imsi-<MCC><MNC><MSIN>, e.g. for MCC=310, MNC=410: imsi-310410000000001.
The UDR seed code reads the PLMN from the runtime SUPI format; update UE_COUNT seeded IMSIs
via the management portal (http://localhost:8080) or the UDR API after boot.
3. AMF Identity (5G-GUTI components)
Section titled “3. AMF Identity (5G-GUTI components)”These three fields identify the AMF globally (TS 23.003 §2.10.1). They affect the 5G-GUTI assigned to each UE and appear in NGAP NG Setup. Change them to match your operator’s allocation.
File: nf/amf/config/dev.yaml
amf_region_id: 0x01 # 8-bit — unique within PLMNamf_set_id: 0x001 # 10-bit — unique within regionamf_id: 0x01 # 6-bit — unique within set4. Tracking Area Code (TAC)
Section titled “4. Tracking Area Code (TAC)”The TAC is broadcast by the gNB and verified by the AMF. A mismatch causes NG Setup Failure.
There is no TAC field in the AMF config — the AMF accepts any TAC that the gNB announces in the NG Setup Request as long as the PLMN matches. Set the TAC in your gNB configuration and it will be stored in the UE context after Initial Registration.
For UERANSIM the field is tac: 1 in config/ueransim/gnb.yaml. For real gNBs set it in the
gNB O&M/configuration tool.
5. Network Slices (S-NSSAI)
Section titled “5. Network Slices (S-NSSAI)”The four default slices are:
| Slice | SST | SD | Label |
|---|---|---|---|
| internet | 1 | 000001 |
eMBB best-effort |
| gold | 1 | 000002 |
eMBB premium |
| silver | 2 | 000001 |
URLLC |
| bronze | 3 | 000001 |
MIoT |
Single file to edit
Section titled “Single file to edit”All NFs that need the slice list (AMF, SMF, NSSF) read it from the same shared file as PLMN:
config/operator.yamlsnssais: - sst: 1 sd: "000001" # internet - sst: 1 sd: "000002" # gold - sst: 2 sd: "000001" # silver - sst: 3 sd: "000001" # bronzeOne edit propagates to AMF (NG Setup Response), SMF (NRF profile), and NSSF (selection policy).
The gNB slice list (UERANSIM: slices[] in config/ueransim/gnb.yaml; real gNB: O&M config)
must be kept in sync manually — it is an external tool that cannot read this file.
URSP rules in nf/pcf/config/dev.yaml (default_ursp.rules[].route_descriptors[].snssai) also
reference slices — update those manually if you add or remove slices (this is a policy rule, not
the slice list).
6. N2 Interface — AMF NGAP
Section titled “6. N2 Interface — AMF NGAP”The AMF listens for NGAP connections on port 38412.
File: nf/amf/config/dev.yaml
ngap: address: "0.0.0.0:38412" # binds all interfaces inside the containerdocker-compose.yml publishes this port to the host:
ports: - "38412:38412/sctp" # host_port:container_portWhat to configure on the gNB
Section titled “What to configure on the gNB”| gNB parameter | Value |
|---|---|
| AMF IP address | <server-host-IP> (the machine running Docker) |
| AMF NGAP port | 38412 |
| PLMN | match §2 |
| Slices | match §5 |
SCTP note: N2 is real SCTP (
github.com/ishidawataru/sctp,nf/amf/internal/ngap/ngap.go), and every NGAP DATA chunk is sent with PPID 60 (TS 38.412 §7). The Docker host needs thesctpkernel module loaded (sudo modprobe sctp).
7. N3 Interface — UPF GTP-U
Section titled “7. N3 Interface — UPF GTP-U”GTP-U user-plane traffic flows between the gNB and the UPF on UDP port 2152.
Critical parameter: n3.ip
Section titled “Critical parameter: n3.ip”File: nf/upf/config/dev.yaml
n3: address: "0.0.0.0:2152" # GTP-U listener ip: "172.30.3.100" # ← THE IP THE gNB MUST REACHThe n3.ip value is advertised to the gNB inside the N3 address field of the PDU Session Resource
Setup Request (via N2SM Transfer). It must be the IP that the gNB can actually route to.
In the development environment 172.30.3.100 is a Docker bridge address visible only on the host.
For a real gNB you have two options:
Option A — gNB on the same host (e.g. software gNB in another container or local process)
Keep 172.30.3.100 and ensure the gNB can reach that Docker bridge network.
Option B — gNB on a remote machine
Change n3.ip to the host’s external NIC IP (or a routable IP on the N3 path):
n3: address: "0.0.0.0:2152" ip: "203.0.113.10" # replace with actual N3 interface IP of the serverAlso update upf_n3_addr in SMF so it inserts the correct IP into N2SM transfers:
upf_n3_addr: "203.0.113.10" # must match upf.n3.ipAnd update the docker-compose port binding so GTP-U is reachable from outside:
# docker-compose.yml — upf serviceports: - "2152:2152/udp" # already there — no change needed if binding 0.0.0.08. N4 Interface — SMF ↔ UPF (PFCP)
Section titled “8. N4 Interface — SMF ↔ UPF (PFCP)”PFCP runs on UDP port 8805 and is internal between SMF and UPF. No change is required unless you move SMF and UPF to separate hosts.
| Parameter | Location | Value |
|---|---|---|
| UPF N4 bind | nf/upf/config/dev.yaml → n4.address |
0.0.0.0:8805 |
| SMF peer UPF | nf/smf/config/dev.yaml → peers.upf |
upf:8805 (Docker DNS) |
If running on separate hosts replace upf with the UPF host IP.
9. N6 Interface — UPF Data Network Routing
Section titled “9. N6 Interface — UPF Data Network Routing”The UPF creates a Linux TUN interface (upfgtp0) and uses iptables NAT to route UE traffic to
the internet (or a test DN).
File: nf/upf/config/dev.yaml
ue_ip_pool: "10.60.0.0/16" # IP range allocated to UEs by the SMF
n6: tun_name: "upfgtp0" # TUN interface name (arbitrary) tun_addr: "10.60.0.254/16" # Gateway IP assigned to the TUN gateway_ip: "172.30.6.1" # Next-hop toward the DN (N6 Docker bridge GW)At startup the UPF executes (see nf/upf/internal/tun/tun.go):
ip link set upfgtp0 upip addr add 10.60.0.254/16 dev upfgtp0iptables -P FORWARD ACCEPTiptables -t nat -A POSTROUTING -s 10.60.0.0/16 ! -o upfgtp0 -j MASQUERADEWhat this means for a real deployment
Section titled “What this means for a real deployment”- UE IP pool (
10.60.0.0/16) must not overlap with any existing subnet on the server. - MASQUERADE translates UE IPs to the UPF’s outbound IP — the DN sees traffic from the UPF, not the UE. This is correct for most scenarios.
- If you need the DN to see actual UE IPs (e.g., fixed-IP UEs, IMS), remove the MASQUERADE rule
and add a static route on the DN router:
ip route add 10.60.0.0/16 via <UPF-N6-IP>. - For internet access the host/server must have a default route and NAT to the internet. The UPF
container inherits the host’s N6 gateway via the
n6-netDocker bridge.
SMF UE IP pool
Section titled “SMF UE IP pool”The SMF must use the same pool as the UPF:
ue_ip_pool: "10.60.0.0/16" # must match upf.ue_ip_pool10. Subscriber Provisioning (UDR / UDM)
Section titled “10. Subscriber Provisioning (UDR / UDM)”Default test subscriber
Section titled “Default test subscriber”| Field | Value |
|---|---|
| SUPI | imsi-001010000000001 |
| K | 465B5CE8B199B49FAA5F0A2EE238A6BC |
| OPc | E8ED289DEBA952E4283B54E88E6183CA |
| AMF | 8000 |
| SQN | 000000000001 |
| Algorithm | Milenage |
The UDR seeds subscribers on startup using the UE_COUNT environment variable
(docker-compose.yml → udr → environment.UE_COUNT). Additional IMSIs follow the pattern
imsi-<MCC><MNC>000000000N where N increments from 1.
Adding real subscribers
Section titled “Adding real subscribers”Use the management portal at http://<server>:8080 → Subscribers → Add Subscriber, or
call the UDR API directly:
curl -X POST http://localhost:8005/nudr-dr/v2/subscription-data/<SUPI>/authentication-data \ -H "Content-Type: application/json" \ -d '{ "supi": "imsi-<MCC><MNC><MSIN>", "k": "<K-hex>", "opc": "<OPc-hex>", "sqn": "000000000001", "amf": "8000", "auth_management_field": "8000" }'SUCI Profile A (X25519 ECIES, TS 33.501 §6.12)
Section titled “SUCI Profile A (X25519 ECIES, TS 33.501 §6.12)”If the UE uses protectionScheme: 1 (SUCI Profile A), the UDM must have the home-network private
key:
# This is the TS 33.501 Annex C.3 published test vector — included for out-of-the-box dev use.# Replace with a freshly generated key for any real deployment.hn_private_key_x25519: "c80949d0c3e4d73a54f8b49fbee7793c5c1de649d7e26ef8b05e0a1e0c8c12e9"The corresponding public key that goes in the UE/SIM is:
61cdb319f72eddfbac55c06c3ec38d15828880a259cbc11cc03ca92abb60fb5e
Generate a fresh key pair for production:
openssl genpkey -algorithm X25519 | openssl pkey -text -noout11. Security — NAS Ciphering
Section titled “11. Security — NAS Ciphering”File: nf/amf/config/dev.yaml
security: null_ciphering: true # dev only — disables NAS encryption (NEA0)Set null_ciphering: false in production. With true the AMF negotiates NEA0 with every
UE regardless of UE capability, sending NAS in plain text. This is useful for Wireshark capture
during development but violates TS 33.501 §6.7.2 in production.
When null_ciphering: false the AMF negotiates NEA2 (AES-CTR) + NIA2 (AES-CMAC) by default.
Ensure the real UE/SIM supports 128-NIA2 and 128-NEA2.
12. PKI / TLS Certificates
Section titled “12. PKI / TLS Certificates”All SBI interfaces (NRF, AUSF, AMF, UDM, UDR, SMF, PCF, NSSF) use mutual TLS.
Development certificates
Section titled “Development certificates”Located in pki/. Generated by make pki (calls scripts/gen-pki.sh). The CA is
pki/ca.crt. Each NF has pki/<nf>.crt and pki/<nf>.key.
Certificate paths in configs:
sbi: cert_file: "/etc/5gc/pki/amf.crt" key_file: "/etc/5gc/pki/amf.key" ca_file: "/etc/5gc/pki/ca.crt"Production
Section titled “Production”Replace dev certificates with operator-signed ones (PLMN CA per TS 33.501 §13). Mount them at
the same paths or change cert_file/key_file/ca_file in each NF config.
The gNB does not need TLS certificates — N2 (NGAP) is not TLS-protected in this implementation. N3 (GTP-U) has no authentication either (standard behaviour).
13. Docker-Compose — Exposing Ports to the Host
Section titled “13. Docker-Compose — Exposing Ports to the Host”The relevant ports already have host bindings in docker-compose.yml. Verify the host firewall
allows them:
| Port | Protocol | NF | Interface | Must be reachable by |
|---|---|---|---|---|
| 38412 | SCTP | AMF | N2 NGAP | gNB |
| 2152 | UDP | UPF | N3 GTP-U | gNB |
| 8805 | UDP | UPF | N4 PFCP | SMF (internal) |
| 8080 | TCP | Portal | Management UI | operator browser |
| 8000–8007 | TCP | NFs | SBI HTTPS | internal only |
| 9100–9109 | TCP | NFs | Prometheus | internal / monitoring |
# Open N2 and N3 on the host firewall (example — adjust for your distro/cloud):sudo iptables -A INPUT -p sctp --dport 38412 -j ACCEPTsudo iptables -A INPUT -p udp --dport 2152 -j ACCEPT14. UPF Routing — Host-Level Requirements
Section titled “14. UPF Routing — Host-Level Requirements”The UPF runs inside Docker with cap_add: [NET_ADMIN] and net.ipv4.ip_forward: 1. For the
N6 traffic to actually reach the internet from the host:
# On the host (not inside Docker):# 1. Enable IP forwarding if not already on:echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward
# 2. If you want UPF container to reach the internet through the host NIC,# the n6-net Docker bridge (172.30.6.0/24) needs a MASQUERADE rule on the host:sudo iptables -t nat -A POSTROUTING -s 172.30.6.0/24 -j MASQUERADE
# 3. Verify the n6-net bridge gateway is routable from within the UPF container:docker exec upf ip route show # should show a default via 172.30.6.1The UPF’s internal iptables MASQUERADE (set up by tun.Setup) handles 10.60.0.0/16 → N6, and
the host-level MASQUERADE handles 172.30.6.0/24 → internet.
15. Full Parameter Change Checklist
Section titled “15. Full Parameter Change Checklist”Use this as a pre-flight list before bringing the core up against a real gNB.
PLMN change
Section titled “PLMN change”- ☐
config/operator.yaml—plmn.mcc,plmn.mnc(one file, all NFs) - ☐ UDR subscriber seed — provision new SUPIs with updated IMSI format
- ☐
config/ueransim/gnb.yamlandconfig/ueransim/ue*.yaml—mcc,mnc - ☐
config/packetrusher/packetrusher.yaml—mcc,mnc - ☐ Real gNB O&M config — MCC/MNC fields
Slices change
Section titled “Slices change”- ☐
config/operator.yaml—snssais[](one file → AMF + SMF + NSSF) - ☐
nf/pcf/config/dev.yaml—default_ursp.rules[].route_descriptors[].snssai(policy rule, not slice list) - ☐
config/ueransim/gnb.yaml—slices[] - ☐ Real gNB O&M config — slice list
N2 (AMF NGAP) change
Section titled “N2 (AMF NGAP) change”- ☐ gNB AMF IP → set to server host IP
- ☐ gNB AMF port →
38412 - ☐ Host firewall allows SCTP 38412 inbound;
sctpkernel module loaded on the host
N3 (UPF GTP-U) change
Section titled “N3 (UPF GTP-U) change”- ☐
nf/upf/config/dev.yaml—n3.ip→ routable IP of the UPF host - ☐
nf/smf/config/dev.yaml—upf_n3_addr→ same IP asn3.ip - ☐ Host firewall allows UDP 2152 inbound
- ☐ gNB N3 IP configured to reach UPF N3 address
UPF data-plane change
Section titled “UPF data-plane change”- ☐
nf/upf/config/dev.yaml—ue_ip_pool(no overlap with existing subnets) - ☐
nf/smf/config/dev.yaml—ue_ip_pool(identical to UPF value) - ☐
nf/upf/config/dev.yaml—n6.tun_addr(gateway within pool, e.g.10.60.0.254/16) - ☐ Host-level iptables MASQUERADE on n6-net bridge (see §14)
- ☐ IP forwarding enabled on host
Subscriber provisioning
Section titled “Subscriber provisioning”- ☐ Real subscriber K, OPc, MSIN loaded into UDR
- ☐ SUPI format:
imsi-<MCC><MNC><MSIN> - ☐ If SUCI Profile A:
nf/udm/config/dev.yaml—hn_private_key_x25519
Security
Section titled “Security”- ☐
nf/amf/config/dev.yaml—security.null_ciphering: falsefor production - ☐ TLS certificates replaced with operator-signed certs (optional for lab, required for field)
Rebuild and restart
Section titled “Rebuild and restart”After any config change:
make downmake up # or make up-obs for observability stackAfter changing Go source (SCTP, seed data, etc.):
make build # rebuilds all NF imagesmake up16. Connectivity Verification
Section titled “16. Connectivity Verification”Once the gNB is connected, verify each interface in sequence:
# 1. N2 — NG Setup completed (AMF accepted the gNB)docker logs amf | grep "NG Setup"
# 2. Initial registration — UE registered and got 5G-GUTIdocker logs amf | grep "RegistrationAccept"
# 3. Authentication — 5G-AKA challenge/response exchangeddocker logs ausf | grep "AuthenticationConfirmation"
# 4. PDU Session — UE has an IP addressdocker logs smf | grep "SessionEstablishment"docker logs smf | grep "ue_ip"
# 5. GTP-U — user-plane tunnel updocker logs upf | grep "SessionEstablishment"
# 6. Data connectivity — ping from UE# (from UE, after PDU session established)ping -I <ue-pdu-iface> 8.8.8.8
# 7. Verify UPF receives GTP-U trafficdocker exec upf tcpdump -i eth1 -n 'udp port 2152' -c 1017. Quick Reference: All Config File Locations
Section titled “17. Quick Reference: All Config File Locations”config/operator.yaml ← PLMN (MCC/MNC) + full slice list — THE ONE FILE TO EDIT All NF containers mount it at /etc/5gc/operator.yaml
nf/amf/config/dev.yaml AMF identity (region/set/id), N2 port, timers, peers, securitynf/smf/config/dev.yaml SBI port, ue_ip_pool, upf_n3_addr, peersnf/upf/config/dev.yaml n3.address, n3.ip, n4.address, ue_ip_pool, n6.*nf/nrf/config/dev.yaml SBI port, heartbeat_timeout_secnf/ausf/config/dev.yaml SBI port, peers (nrf, udm)nf/udm/config/dev.yaml SBI port, hn_private_key_x25519, peers (nrf, udr)nf/udr/config/dev.yaml SBI portnf/nssf/config/dev.yaml SBI port, peers (nrf)nf/pcf/config/dev.yaml SBI port, default QoS policy, default_ursp rules (policy, not slice list)docker-compose.yml Docker networks (subnets), fixed IPs (UPF 172.30.3.100), port bindingspki/ TLS certificates — regenerate with `make pki`Made and developed by Francisco Javier Curieses Sanz · Docs mirrored from claudia-5gc @ v2.3.1