TOSSI-IMS: VoNR, ViNR and SMS on a 5G SA Core
A 5G standalone core will move packets all day. Ask it to carry a phone call and you find out how much of the specification a data-only deployment never touches.
This guide deploys a working IMS voice and messaging service on top of an open 5G SA core, with no HSS anywhere in the path. Kamailio provides a stock P/I/S-CSCF; a focused set of patches to the SD-Core SMF, PCF and AMF makes the core capable of carrying voice; and SETU, a signalling bridge, joins the two over Rx, Cx and an SMS ingest. Subscriber keys never leave the core - SETU reads them over Nudr and computes the IMS-AKA vectors itself with MILENAGE.
The result is VoNR and ViNR calls on dedicated guaranteed bearers, plus SMS over IMS, on commercial handsets over a real 5G SA radio.

The complete TOSSI-IMS stack. On the left, an SD-Core control plane on single-node RKE2 via aether-onramp: NRF, UDM, AUSF, NSSF and UDR unchanged, with the SMF, PCF and AMF carrying a ◆ patched marker; a bess-UPF in af_packet mode with gtppsc: true. The SMF advertises the P-CSCF inside the PDU session PCO; the PCF authorizes media over N5 and drives a dedicated GBR bearer down through the SMF. In the centre, SETU: connectors face the core over N5 and Nudr, adapters face the IMS over Rx, Cx and SMS, and a session fabric with a write-ahead log sits between them. On the right, a stock Kamailio P-CSCF, I-CSCF and S-CSCF on host networking with rtpengine and a MySQL store - unchanged except that their Rx and Cx Diameter peers point at SETU. Along the bottom, the user plane: a handset on the O-RU, a gNB terminating N2 at the AMF and N3 GTP-U at the UPF, RTP riding a 5QI-1 or 5QI-2 guaranteed bearer, and SIP riding the default flow to the P-CSCF.
Overview
Registration needs something to challenge the handset the way an HSS would. A call needs the PCF to authorize media and the SMF to turn that decision into an actual guaranteed-bit-rate bearer. And before any of that, the handset has to be told where the IMS is, or it attaches happily and never registers.
Open cores implement these to varying degrees. SD-Core, built and hardened for data, implemented several of them incompletely or not at all - which is not a criticism of SD-Core so much as a description of what happens when a code path has no users.
TOSSI-IMS closes those gaps where the specification puts them, and bridges the rest:
The 5G core learns to carry voice. Twenty-five files across the SMF, PCF and AMF. Every change sits inside its network function's own module - no forked openapi, nas or ngap, and go.mod byte-identical to the upstream tag. Moving to a newer SD-Core release is a rebase, not a re-derivation.
The IMS is stock. Kamailio runs unmodified. The only edit is pointing its Rx and Cx Diameter peers at SETU.
SETU carries the seam. It answers Cx from the core's Nudr, authorizes media over N5, and shuttles SMS between SIP and the core. Its internals are documented separately - see SETU.
What you end up with is a handset that registers with full IMS-AKA, places a VoNR or ViNR call on a bearer that comes up per call and is released on hangup, and sends and receives SMS - against an open core, with no proprietary HSS.

Architecture
Four parts: an SD-Core control plane with three patched NFs, a bess-UPF user plane, the Kamailio IMS, and SETU joining the two.
SD-Core control plane
NRF, UDM, AUSF, NSSF and UDR run unmodified and provide discovery, subscriber data and authentication. The AMF terminates N2. The SMF owns the N4 relationship with the UPF and installs each PDU session's forwarding and QoS rules. The PCF makes policy decisions and, for voice, authorizes media over N5.
Subscribers are provisioned through the omec sub-provision simapp with provision-network-slice: true, on PLMN 001/01. Key material lives in the core's own databases and is read back over Nudr by SETU at Cx time. There is no separate HSS process.
The bess-UPF user plane
SD-Core's bess-UPF in af_packet mode over a macvlan on the N3/N6 NIC - no DPDK. The one setting that matters for voice is gtppsc: true. Real gNBs send N3 GTP-U carrying the 0x85 PDU Session Container extension header, and without that flag every uplink packet misses the PDR lookup and the datapath fails quietly. A guaranteed bearer carrying no media looks exactly like a working call nobody can hear.
The Kamailio IMS
Stock Kamailio 6.1 P-CSCF, I-CSCF and S-CSCF on host networking, with rtpengine anchoring media and MySQL behind the S-CSCF. The configuration is conventional IMS: av_mode=0 on the S-CSCF so it takes real authentication vectors over Cx rather than computing them locally, IMS-AKA with IPsec on Gm, a single-contact policy to survive stale registrations, on-net tel: to sip: translation, an Rx AAR fired once the SDP is known, and single-lookup SMS routing.
None of that is TOSSI-specific. What makes it work against a core instead of an HSS is one change at each end of the seam: the P-CSCF's Rx peer and the I/S-CSCF's Cx peer point at SETU.
SETU
SETU presents the HSS surface the IMS expects - Cx UAR/UAA, LIR/LIA, SAR/SAA, MAR/MAA - and the policy surface it needs on Rx, and answers both from the 5G core. Cx authentication vectors are computed with MILENAGE from key material fetched over Nudr. Rx media authorization becomes an N5 Npcf_PolicyAuthorization app-session. Every authorized session is journaled to a write-ahead log, so a restart mid-call doesn't strand policy rules inside the PCF.

Interface map
| Interface | Protocol | Reference deployment |
|---|---|---|
| Gm (UE ↔ P-CSCF) | SIP over IPsec | IMS-AKA registration; SIP on the default 5QI-9 flow |
| Mw (CSCF ↔ CSCF) | SIP | P-CSCF ↔ I-CSCF ↔ S-CSCF |
| Rx (P-CSCF ↔ SETU) | Diameter | :3868 - AAR / AAA / STR for media authorization |
| Cx (I/S-CSCF ↔ SETU) | Diameter | :3869 - UAR, LIR, SAR, MAR; registration and IMS-AKA |
| SMS ingest (S-CSCF ↔ SETU) | HTTP | :8090 - TPDU decode and rebuild, both directions |
| N5 (SETU ↔ PCF) | HTTP/2 SBI | TS 29.514 - app-session create / notify / delete |
| Nudr (SETU ↔ UDR) | HTTP/2 SBI | TS 29.505 - authentication subscription and am-data |
| N2 (gNB ↔ AMF) | NGAP / SCTP | registration and mobility |
| N3 (gNB ↔ UPF) | GTP-U | UDP 2152, with the PSC extension header |
| N4 (SMF ↔ UPF) | PFCP | UDP 8805 |
| N6 (UPF ↔ network) | SGi | af_packet egress to the data network |
The NF patches: enabling voice in the core
This is where the difficulty of the problem lives. Everything below is scoped inside its network function's own module; nothing forks the SBI layer or the datapath, and every go.mod stays byte-identical to its upstream tag.
All three branches are published as setu-ims-rel1, with the unmodified upstream tree as the first commit - so a diff against that commit is exactly the delta and nothing else.
SMF - turn a policy decision into a real bearer
TOSSI-Foundation/sdcore-smf · upstream omec-project/smf v4.1.0 · 14 files modified, 1 added
Most of the voice-enabling work sits here.
P-CSCF discovery in the PDU session. The SMF injects the P-CSCF address into the Protocol Configuration Options of the PDU Session Establishment Accept, container 0x000C. Without it the handset attaches but never learns where to register, and IMS never starts.
Accepting the policy update at all. The PCF posts AF-triggered SM-policy updates to the bare notification URI, with no /update suffix. Stock routing has no such route, so the request 404s and the dedicated bearer is never even requested. This one is easy to miss because nothing logs an error that points at it.
Binding to the right subscriber. The GPSI is forwarded in the SM policy create, so the PCF can tie an app-session to the correct subscriber instead of falling back to the first UE in its map - which put two handsets on one SUPI and produced call failures that looked like radio problems.
QoS rules a handset will actually accept. Each of these was a 5GSM reject cause we watched a commercial device return. The Procedure Transaction Identity must be zero for a network-initiated modification. A PccRuleId-<n> must not parse to zero, which is a reserved identifier. A media flow arrives as two FlowInfos that are reverses of each other, and emitting both produces two filters with identical component values, which the UE rejects - they have to collapse into one bidirectional filter. Packet filters need unique 4-bit identifiers. The local address must be omitted, because standard VoLTE and VoNR filters leave the local side implicit. And uplink flows need local and remote swapped, since an IPFilterRule is written network-side while NAS filters are written UE-side.
Guaranteed bitrate on the radio. TS 38.413 requires GBRQosInformation for GBR 5QIs. Leave it out and the DRB comes up with no guaranteed bitrate, the handset can't complete its QoS precondition, and the call dies at 580 Precondition Failure - with every log on the network looking clean.
Policy state that survives a second call. The sent decision is committed into the SM context and popped, so builders stop re-reading the first call's stale state; the session-AMBR is preserved across a rule-only modification; and rule deletion is detected by absence, because the PCF signals removal by dropping a rule rather than nulling it. Before these, the first call after an attach worked and the second didn't, until you toggled the session.
Static IMSI-to-IP and a stable UPF node-id. Subscriber addressing is pinned per IMSI, and the PFCP node-id points at the UPF's Service ClusterIP so a UPF pod restart doesn't strand the association.
PCF - bind to the right session, and clean up after yourself
TOSSI-Foundation/sdcore-pcf · upstream omec-project/pcf v3.0.0 · 7 files modified, 1 added
Exact-IP session binding. The terminating leg of a call carries no GPSI, so the UE's IPv4 address is the only reliable join key. Stock code returned the first policy in the map regardless of address, which bound a callee's IP to whichever UE happened to be first and poisoned the association for every later call.
IMS media classified as GBR. When the media-type mapping falls through to its default, IMS voice is treated as 5QI 1. Otherwise the flow is authorized non-GBR and the precondition never completes.
Correct media-component removal. The removal path keyed on JSON map-key strings, which never matched the composite key used at creation, so ending a call silently removed nothing and voice rules accumulated until a modification grew too large to succeed.
ARP enums the SMF will decode. PreemptCap and PreemptVuln are non-pointer, non-omitempty enums, so leaving them unset marshals as "" and the SMF's strict decoder rejects the entire policy callback with a 400. Fixed on the producer side rather than by forking the shared model.
SelfHeal orphan purge. A lost BYE or a crashed gNB leaves an app-session whose GBR rules are never released. A new app-session for the same subscriber means any prior call is over, so stale sessions are purged at that point - making accumulation structurally impossible rather than something you restart the PCF to fix.
AMF - survive the paths voice actually exercises
TOSSI-Foundation/sdcore-amf · upstream omec-project/amf @ 88ba081 · 2 files modified
No IMS protocol logic here at all - IMS interworking lives in the SMF and PCF. What the AMF needed was resilience.
Running voice pushes the AMF through paths a data-only deployment rarely reaches: dedicated bearer modifications, released UEs whose timers are still armed, per-UE goroutines being torn down with messages still in flight. In stock code a panic on any of those takes down the whole AMF, and with it every subscriber on the network - not just the one whose message was malformed.
The per-UE event loop body is lifted into a closure so a panic can be recovered per iteration and the goroutine survives; SubmitMessage is guarded against a send on a channel a concurrent teardown has already closed; and the timer callback is guarded so a ticker firing against a released UE can't take the process with it.
Worth saying plainly: recovering from panics is not the same as fixing what panics. These guards keep a lab network usable while the underlying cause is diagnosed, and every recovery is a signal that something deserves a proper fix. They're deliberately narrow - three goroutine boundaries, not a blanket handler.
UPF - parse what real radios send
Configuration only, no image patch. Set gtppsc: true so the datapath expects the 0x85 PSC extension header that real gNB N3 traffic carries.
Deployment
Six parts: core, datapath, IMS, bridge, RAN, verify. Export your values once and the rest pastes.
export NODE=203.0.113.10 # this host's IP - the CSCFs, SETU and the gNB's N2/N3 all reference it
export CORE_NS=aether-ims # your SD-Core namespace
export DOM=ims.mnc001.mcc001.3gppnetwork.org
export REG=<your-registry> # registry holding the patched sdcore-smf / sdcore-pcf / sdcore-amf
Prerequisites. A single-node Kubernetes (RKE2) host with an SCTP-capable CNI, the aether-onramp repository, a 5G SA gNB with an O-RU, and at least one programmable SIM.
| Component | Version (reference deployment) |
|---|---|
| Kubernetes | RKE2, Canal CNI, SCTP enabled |
| SD-Core (via aether-onramp) | 4.1.3 |
| SMF | sdcore-smf, branch setu-ims-rel1 - ◆ patched |
| PCF | sdcore-pcf, branch setu-ims-rel1 - ◆ patched |
| AMF | sdcore-amf, branch setu-ims-rel1 - ◆ patched |
| UDM / UDR / AUSF / NRF / NSSF / webui | upstream SD-Core 4.1.3 |
| UPF | bess-UPF, af_packet mode, gtppsc: true |
| Kamailio P/I/S-CSCF | 6.1 upstream, config-only changes |
| rtpengine | upstream |
| SETU | 1.0 - coranlabs/SETU |
Part 1 - Deploy the 5G core
1.0 Cluster. Single-node RKE2 with an SCTP-capable CNI, since N2/NGAP is SCTP, and a NIC free for the UPF's N3/N6 af_packet path.
1.1 Install SD-Core. Control plane and bess-UPF via aether-onramp, PLMN 001/01, into $CORE_NS:
helm -n $CORE_NS upgrade --install sd-core-ims ./sd-core \
--values ims-values.yaml --create-namespace
kubectl -n $CORE_NS get pods # amf smf pcf udm udr ausf nrf nssf webui upf simapp → Running
1.2 Swap in the patched NFs. Build each from its branch, import into the RKE2 containerd namespace, and roll the deployment:
for nf in smf pcf amf; do
docker save $REG/sdcore-$nf:setu-ims-rel1 \
| sudo ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images import -
kubectl -n $CORE_NS set image deploy/$nf $nf=$REG/sdcore-$nf:setu-ims-rel1
done
1.3 Enable voice in the NF configs.
- SMF - advertise the P-CSCF in the PDU-session PCO (
pcscfInfos.ipv4=$NODE), and point the PFCP node-id at the UPF Service ClusterIP. - UPF (
upf.jsonc) - setgtppsc: true. - PCF - enable the SelfHeal orphan purge.
Then re-establish the PFCP association, UPF first:
kubectl -n $CORE_NS rollout restart deploy/upf-adapter deploy/smf
1.4 Provision subscribers. Through the omec sub-provision simapp with provision-network-slice: true, on PLMN 001/01. Each subscriber needs its K and OPc, an MSISDN, membership in a device group tied to the slice, the ims DNN for voice and internet for data:
kubectl -n $CORE_NS rollout restart deploy/simapp
kubectl -n $CORE_NS logs deploy/simapp | grep -i provision
Part 2 - Bring up the user-plane datapath
The af_packet UPF reaches the data network over a host macvlan. Wire the IMS UE pool out through your uplink NIC - substitute your own N6 and uplink interfaces:
sudo ip link add core-ims link <n6-nic> type macvlan mode bridge 2>/dev/null || true
sudo ip addr add 192.168.244.1/24 dev core-ims 2>/dev/null; sudo ip link set core-ims up
sudo sysctl -qw net.ipv4.ip_forward=1 net.ipv4.conf.all.rp_filter=0 net.ipv4.conf.core-ims.rp_filter=0
sudo ip route replace 192.168.111.0/24 via 192.168.244.3 dev core-ims
sudo iptables -t nat -C POSTROUTING -s 192.168.111.0/24 -o <uplink-nic> -j MASQUERADE 2>/dev/null \
|| sudo iptables -t nat -A POSTROUTING -s 192.168.111.0/24 -o <uplink-nic> -j MASQUERADE
Part 3 - Deploy the IMS
Stock Kamailio 6.1, all on host networking on $NODE, with rtpengine and MySQL. The three config trees (kam-pcscf, kam-icscf, kam-scscf) are conventional IMS configs; the only voice-specific choice is av_mode=0 on the S-CSCF so it takes real Cx MAR vectors from SETU.
3.1 MySQL store:
docker run -d --name kam-mysql --network host --restart unless-stopped \
-e MYSQL_ROOT_PASSWORD=ims -e MYSQL_ROOT_HOST=% mysql:8.0
3.2 Databases and seed data. Load the ims_usrloc_pcscf / ims_usrloc_scscf, ims_icscf, domain, ims_dialog, usrloc and dispatcher schemas shipped in the Kamailio image, then:
docker exec -i kam-mysql mysql -uroot -pims <<SQL
CREATE DATABASE IF NOT EXISTS pcscf; CREATE DATABASE IF NOT EXISTS scscf; CREATE DATABASE IF NOT EXISTS icscf;
CREATE USER IF NOT EXISTS 'scscf'@'%' IDENTIFIED WITH mysql_native_password BY 'heslo';
GRANT ALL ON scscf.* TO 'scscf'@'%'; FLUSH PRIVILEGES; -- repeat for pcscf / icscf
INSERT INTO scscf.domain (domain) VALUES ('$DOM');
INSERT INTO icscf.nds_trusted_domains (trusted_domain) VALUES ('$DOM');
INSERT INTO icscf.s_cscf (name, s_cscf_uri) VALUES ('scscf1','sip:$NODE:6060');
SQL
3.3 rtpengine:
docker run -d --name rtpengine --network host --restart unless-stopped \
--entrypoint /usr/local/bin/rtpengine drachtio/rtpengine:latest \
--interface=$NODE --listen-ng=127.0.0.1:2223 --table=-1 \
--port-min=30000 --port-max=30100 --foreground --log-level=6
3.4 The three CSCFs. Each mounts its config tree at /cfg; the host aliases resolve the IMS domain to $NODE, since there is no SRV or NAPTR anywhere in this deployment:
KIMG=ghcr.io/kamailio/kamailio-ci:6.1-alpine
docker run -d --name scscf --network host --restart unless-stopped \
--add-host hss.$DOM:$NODE --add-host scscf.$DOM:$NODE --add-host $DOM:$NODE \
-v $PWD/kam-scscf:/cfg --entrypoint sh $KIMG \
-c 'mkdir -p /var/run/kamailio_scscf; exec kamailio -f /cfg/kamailio.cfg -DD -E'
docker run -d --name icscf --network host --restart unless-stopped \
--add-host hss.$DOM:$NODE --add-host scscf.$DOM:$NODE --add-host icscf.$DOM:$NODE --add-host $DOM:$NODE \
-v $PWD/kam-icscf:/cfg --entrypoint sh $KIMG \
-c 'mkdir -p /var/run/kamailio_icscf; exec kamailio -f /cfg/kamailio.cfg -DD -E'
docker run -d --name pcscf --network host --restart unless-stopped --cap-add NET_ADMIN \
--add-host rxgw.$DOM:$NODE --add-host icscf.$DOM:$NODE --add-host scscf.$DOM:$NODE \
--add-host pcscf.$DOM:$NODE --add-host $DOM:$NODE \
-v $PWD/kam-pcscf:/cfg --entrypoint sh $KIMG \
-c 'mkdir -p /var/run/kamailio_pcscf; exec kamailio -f /cfg/kamailio.cfg -DD -E'
Point the seam at SETU. In the P-CSCF config, set the Rx cdp peer to $NODE:3868. In the I-CSCF and S-CSCF configs, set the Cx cdp peer to $NODE:3869. Nothing else on the IMS side changes.
Part 4 - Deploy SETU
SETU replaces any hand-built Cx, Rx and SMS bridges with one process. Source and full configuration reference: github.com/coranlabs/SETU.
git clone https://github.com/coranlabs/SETU.git && cd SETU
go build -o setu ./cmd/setu # or: docker build -f deploy/Dockerfile.build -t setu:1.0 .
Point it at the core's PCF and UDR, and at the S-CSCF:
PCF=$(kubectl -n $CORE_NS get svc pcf -o jsonpath='{.spec.clusterIP}')
UDR=$(kubectl -n $CORE_NS get svc udr -o jsonpath='{.spec.clusterIP}')
cat > setu.json <<JSON
{
"plmn": { "mcc": "001", "mnc": "01" },
"core": "sdcore",
"sdcore": { "pcf": "https://$PCF:29507", "udr": "https://$UDR:29504", "insecure": true,
"notifURI": "http://$NODE:7777/notif", "notifListen": ":7777" },
"rx": { "listen": ":3868", "hostIP": "$NODE", "walPath": "/var/lib/setu/rx-grants.wal" },
"cx": { "listen": ":3869", "hostIP": "$NODE", "scscf": "sip:$NODE:6060", "admin": ":9102" },
"sms": { "listen": "127.0.0.1:8090", "scscf": "$NODE:6060", "selfIP": "$NODE" }
}
JSON
docker run -d --name setu --network host --restart unless-stopped --stop-timeout 15 \
-v $PWD/setu.json:/etc/setu/setu.json:ro -v /var/lib/setu:/var/lib/setu \
setu:1.0 -config /etc/setu/setu.json -apps rx,cx,sms
Host networking, because it binds the well-known Rx and Cx ports and the SMS ingest. The --stop-timeout matters: SETU drains on SIGTERM and releases what it holds, and cutting that short is how you strand policy rules on the PCF. The repo carries a systemd unit and deploy/setu.example.json if you'd rather not run it in Docker.
Part 5 - Deploy the RAN
Any 5G SA gNB works. This reference drove a LiteON O-RU over a 7.2 fronthaul split. In the gNB config, point amf_ip_address at the AMF's N2 address, N3 at the UPF, and set PLMN 001/01 with the TAC and SST/SD matching your slice.
Confirm NGSetup succeeds and the gNB reports the served PLMN as 001/01. Then insert a provisioned SIM and let the handset attach - it registers over IMS-AKA and can place a call.
Verification
Commercial handsets, a LiteON O-RU 5G SA radio, SD-Core, SETU, Kamailio. Real calls, not a simulator.
| Result | |
|---|---|
| Registration | Full IMS-AKA cycle - UAR → MAR → 401 challenge → SAR → 200 - accepted by the handset, no HSS |
| VoNR | Dedicated 5QI-1 GBR bearer up per call, released on hangup |
| ViNR | 5QI-2 video bearer alongside voice |
| SMS | Both directions, GSM 7-bit and UCS-2 including emoji, with submit reports |
| Teardown | Clean release across repeated calls, no policy-rule accumulation |
| Restart safety | SETU killed mid-session and restarted, nothing stranded on the core |
Watching it work
The 5G side:
kubectl -n $CORE_NS get pods # AMF/SMF/PCF/UPF/UDR Running
kubectl -n $CORE_NS logs -f deploy/pcf # N5 app-session create/delete per call
kubectl -n $CORE_NS logs -f deploy/smf # dedicated-bearer QoS modification, PCO
SETU translating:
docker logs -f setu # REGISTER (Cx UAR/MAR/SAR) · AUDIO/VIDEO (Rx→N5) · SMS · CALL-END
The SIP ladder on the P-CSCF:
docker logs -f pcscf 2>&1
What actually proves it
A VoNR call produces an N5 app-session on the PCF with a 5QI-1 media component, an SMF QoS modification that establishes the GBR flow, and RTP that flows only while that bearer is up. Hang up and the app-session is deleted and the bearer released. If you want one check that separates a working call from a plausible-looking one, it's that the bearer appears and then disappears.
A registration produces a Cx UAR/UAA at the I-CSCF, a MAR/MAA carrying a Digest-AKAv1-MD5 vector SETU computed from Nudr key material, a 401 challenge, a second REGISTER with RES, a SAR/SAA with the IMSSubscription, and a 200 - with no HSS process anywhere on the network.
An SMS produces a SIP MESSAGE with a 3GPP SMS body, an HTTP POST into SETU, a TPDU decode and rebuild, delivery to the recipient, and a submit report back to the originator.
Integration findings
Things worth knowing before you reproduce this. Most of them cost us a day each.
GTP-U without gtppsc fails silently. Real gNBs put a 0x85 PSC extension header on N3. With the flag off, uplink misses the PDR lookup and the datapath drops everything - while signalling and the bearer both look completely healthy. This is the single most misleading failure in the stack, because every log you'd think to check is green.
A guaranteed bit rate of zero is not a guaranteed bit rate. Authorize a 5QI-1 flow without one and the bearer comes up, the handset never completes its QoS precondition, and the call dies at 580 Precondition Failure. The SDP tells you the bandwidth; use it.
IMS-AKA and 5G-AKA share a sequence number. Issue a vector without advancing it and the next authentication is rejected as a replay. Registration then collapses in a way that looks convincingly like a radio problem, and you will spend a day on the radio.
Kamailio logs to stderr, and most of its errors are benign. The P-CSCF emits a steady stream of tcp_read … Operation timed out and keep-alive failures from idle IPsec connections to sleeping handsets. They are not faults. For a clean operator view, filter at the view layer - pipe docker logs (with 2>&1, since it's all on stderr) through something that drops the noise and keeps the SIP ladder.
The PCF's session Location must be dialable from SETU. A PCF that builds the app-session Location from its own service name hands back an address the bridge host can't resolve. Deletes fail quietly, policy rules pile up, and calls start failing after the first one on a subscriber. SETU re-attaches the path to a reachable address and treats a 404-on-delete as success - but the core-side lesson is to make that URL resolvable in the first place.
Patched NF images are local-only and GC-vulnerable. Covered above, and worth repeating because of how it presents: data keeps working, voice stops, and nothing obviously changed.
Call flow

A subscriber presses dial. INVITE → Rx AAR → an N5 app-session on the PCF → media authorized → the SMF brings up the 5QI-1 or 5QI-2 GBR bearer → 200 OK, and RTP flows over the guaranteed bearer. BYE → Rx STR → N5 delete releases it.
Registration is the same shape over Cx - UAR → MAR → 401 challenge → SAR → 200 - authenticated with a vector SETU computes from the core's Nudr, with no HSS in the path.
SMS skips the bearer entirely: a SIP MESSAGE into SETU's ingest, TPDU decode and rebuild, delivered with a submit report.
Source and references
- SETU, the signalling bridge - coranlabs/SETU
- IMS-patched SMF - TOSSI-Foundation/sdcore-smf, branch
setu-ims-rel1 - IMS-patched PCF - TOSSI-Foundation/sdcore-pcf, branch
setu-ims-rel1 - IMS-patched AMF - TOSSI-Foundation/sdcore-amf, branch
setu-ims-rel1
This work builds on open source: SD-Core from the OMEC project and the Open Networking Foundation, deployed with aether-onramp; Kamailio and rtpengine from their respective communities. Our thanks to their maintainers.
Standards reference: TS 29.214 (Rx), TS 29.228 and 29.229 (Cx), TS 29.514 (N5), TS 29.505 (Nudr), TS 24.501 (NAS and PCO), TS 24.011 and 23.040 (SMS), TS 38.413 (NGAP).