Skip to content

SD-Core is an open 5G standalone core. Kamailio is an open IMS. Put them side by side and you still cannot make a phone call. Not because either is missing a feature, but because nothing connects them, and nothing in the 3GPP specifications describes what that connection should look like. SETU is the piece that closes it.

The deployment across two hosts. Host A runs the RAN: 5G UEs attach through a LiteON RU and a fronthaul switch with PTP grandmaster to a monolithic OCUDU gNB, whose DU carries SSB, PRACH, PDCCH, PDSCH, PUSCH, PUCCH, MAC, RLC, scheduler and F1AP-C/U, and whose CU carries RRC, PDCP, F1AP, NGAP, SDAP and GTP-U, over DPDK on Ubuntu Server 22.04. Host B runs the core: SD-Core network functions AMF, SMF, UPF, NRF, NSSF, UDR, UDM and AUSF alongside a router, NMS and MongoDB; SETU with its cx-hss-shim, rx-n5-gateway and sms-router services and diameter, dpeer, milenage and sessionstore packages; and the Kamailio IMS with P-CSCF, I-CSCF, S-CSCF and rtpengine.
The deployment as built and verified: an OCUDU gNB driving a LiteON O-RU on one host, and SD-Core, SETU and a stock Kamailio IMS on the other. Commercial handsets attach over a real 5G SA radio.

The result is a working voice service. Calls, video calls and text messages, verified on commercial handsets over a real 5G SA radio, with no HSS, no policy server and no SMS centre anywhere in the deployment.

Why an open core and an open IMS cannot talk to each other

An IMS does not work out who a subscriber is on its own. It asks a database, the HSS or Home Subscriber Server, for the credentials it needs to challenge the handset and prove the SIM is genuine. Nor does it decide by itself that a call deserves protected bandwidth: it asks a policy server, which then instructs the network to reserve it. Both of those conversations happen in Diameter, a protocol considerably older than 5G.

A 5G core holds the same information and makes the same decisions. It simply exposes them as modern service APIs over HTTP/2: Nudr for subscriber data, N5 for media policy.

So the IMS asks questions the core can answer, in a language the core does not speak. Both sides are specified to the byte. The translation between them is specified nowhere.

The practical consequence is that every deployment writes that translation itself, by hand, against one core, once, and then writes it again for the next one.

What SETU replaces

SETU, Sanskrit for "bridge", presents the IMS with exactly the interfaces it already expects: an HSS on Cx, a policy server on Rx. Kamailio has no idea it is talking to anything unusual, which is why Kamailio needed no code changes at all.

A comparison of the conventional approach and the SETU approach across three functions. Conventionally, authentication goes from the S/I-CSCF over Cx to an HSS holding a copy of the subscriber keys, giving two roots of trust that can diverge on the sequence number; policy goes from the P-CSCF over Rx to a PCRF that is a Diameter island the 5G PCF never sees, so the call is best effort only and the precondition fails; and SMS goes from the S-CSCF to an IP-SM-GW and an SMSC, two extra nodes to run and license. With SETU, the same S/I-CSCF talks Cx to an adapter that reads the UDR over Nudr, so there is one root of trust shared with 5G-AKA; the P-CSCF talks Rx to a gateway that posts to the 5G PCF over N5, which installs a PCC rule and gives a 5QI-1 guaranteed bearer; and the S-CSCF hands SMS to an adapter that terminates it directly, with no SMSC at all.
A conventional IMS bolts an HSS, a policy server and an SMS centre onto the core, each holding its own subscriber state. SETU answers all three of those surfaces from the 5G core itself.

Behind that familiar face, SETU answers from the 5G core. It reads subscriber key material over Nudr and computes the authentication challenge itself, and it turns a media authorization request into the service API call the core understands. Every authorized session is journaled to a write-ahead log, so a restart mid-call does not leave reservations stranded inside the core.

One consequence is worth stating plainly: subscriber keys never leave the core. There is no second copy of anyone's credentials to provision, secure, or keep in step with the first.

Inside the bridge

SETU is three stateless Go services sharing one module, with no third-party dependencies.

The IMS testbed architecture in four layers. At the top, the Kamailio IMS: P-CSCF on UDP/TCP 5060 with IPSec 5100 and 6100 acting as the Rx AF and controlling rtpengine, I-CSCF on 4060 doing topology hiding and Cx UAR on REGISTER and LIR on INVITE, S-CSCF on 6060 as registrar and service execution with Cx MAR and SAR, and rtpengine as media anchor. Below that the IMS seam: Cx Diameter on 3869 carrying UAR, LIR, SAR and MAR; Rx Diameter on 3868 carrying AAR, STR and RAR; and HTTP POST to /mo on 8090. In the middle, SETU: cx-hss-shim is the HSS, rx-n5-gateway is the PCRF, sms-router is the SMSC, over shared packages diameter, dpeer, milenage, sessionstore, sbi, qosconv and metrics. Below that the core seam: Nudr v2 REST, N5 Npcf_PolicyAuthorization POST to app-sessions, and SIP MESSAGE via X-SMS-Relay. At the bottom, SD-Core: UDR/UDM holding the one subscriber database, PCF, SMF, AMF and UPF.
SETU's three services and the two seams they sit between. It holds no long-term subscriber state: the subscriber lives in the UDR, and the only live state is the Rx to N5 session mapping, which survives a restart through the write-ahead log.

The cx-hss-shim is the HSS: it selects the S-CSCF, runs MILENAGE f1 to f5 over the key material it reads from the UDR, and writes the advanced sequence number back. The rx-n5-gateway is the policy server: it maps the SDP media description to a 5QI and a guaranteed bit rate, posts an app-session to the PCF, and releases it on termination. The sms-router is the SMS centre: it decodes the submit TPDU, transcodes between GSM-7 and UCS-2, and injects the delivery back into the S-CSCF.

What changed inside the core

The IMS needed nothing. The OCUDU gNB needed nothing. The core needed work: twenty-five files across three of its network functions.

FunctionBranchWhat it learned to do
SMF (sessions)setu-ims-rel1tell the handset where the IMS is; turn a policy decision into a reserved-bandwidth channel a handset will accept
PCF (policy)setu-ims-rel1match a call to the right subscriber's session; treat voice and video as guaranteed rather than best effort; release what it reserved
AMF (mobility)setu-ims-rel1resilience only, no voice logic. One bad message must not take down every subscriber

One rule held throughout: every change stays inside the network function it belongs to. No forked shared libraries, and dependency files identical to the upstream release. That sounds like housekeeping, but it decides whether this work survives. Upgrading to a newer SD-Core release is a rebase, not a fresh start.

Each branch begins with the untouched upstream tree as its first commit, so the difference between upstream and ours is one git diff away.

How a call, a registration and an SMS flow end to end

An end-to-end message sequence across UE, gNB, AMF, SMF, UPF, PCF, UDR, SETU and the Kamailio IMS, in four phases. Attach and P-CSCF discovery: registration request and PDU session establishment on the ims DNN, N11 CreateSmContext, N7 SM policy create, N4 PFCP session establish, then PDU Session Accept carrying the P-CSCF address in PCO container 0x000C. IMS registration with IMS-AKA: REGISTER over Gm to the P-CSCF and I-CSCF, Cx UAR and UAA, Cx MAR, a Nudr GET for the authentication subscription returning K, OPc and SQN, MILENAGE f1 to f5 producing RAND, AUTN, XRES, CK and IK with the sequence number advanced, a 401 challenge, REGISTER with RES, Cx SAR, a Nudr GET for am-data returning the MSISDN, and 200 OK registered. VoNR and ViNR call: INVITE with audio and video SDP, Rx AAR, translation to N5 with the grant journalled to the write-ahead log, N5 POST to app-sessions, the PCF binding the app-session to the PDU session by UE IPv4, N7 notify to the SMF, NAS Modification Command and NGAP ModifyRequest establishing 5QI-1 for audio and 5QI-2 for video, RTP flowing on the guaranteed bearer, then BYE, Rx STR and app-session delete releasing the bearer. SMS over IP: a SIP MESSAGE carrying an RP-DATA SMS-SUBMIT, HTTP POST to /mo, TPDU decode and rebuild as DELIVER, MESSAGE to the recipient and a submit report back to the originator.
Every message on the wire, end to end, as deployed and verified. The diamonds mark the 3GPP network functions that were modified to make IMS possible: 25 files across the SMF, PCF and AMF, all inside the network function module.

A call. A subscriber presses dial. The INVITE reaches the P-CSCF, which asks SETU over Rx to authorize the media. SETU turns that into an app-session on the PCF, the PCF instructs the SMF, and the SMF brings up the reserved channel on the radio. The call connects and audio rides the guarantee. On hangup the reverse happens: the release travels back through SETU and the reservation is torn down.

A registration. The same shape over Cx. The I-CSCF asks who should serve the subscriber, the S-CSCF asks for an authentication challenge, and SETU answers both from the core's own subscriber data. The handset is challenged, answers, and is registered, with no HSS in the path.

An SMS. Skips the reserved channel entirely. The message arrives as SIP, SETU decodes and rebuilds it, delivers it to the recipient, and returns a delivery report to the sender.

What was verified

Commercial handsets on a real 5G SA radio, an OCUDU gNB driving a LiteON O-RU. Not a simulator, and not UE emulation.

CapabilityResult
Registrationfull challenge-and-response, accepted by the handset, no HSS on the network
Voicededicated reserved channel per call, released on hangup
Videosecond reserved channel alongside voice
Messagingboth directions, plain text and emoji, with delivery reports
Teardownclean across repeated calls, nothing accumulating
Restart safetySETU killed mid-session and restarted, nothing stranded on the core

The single check that separates a working call from one that merely looks right: the reserved channel must appear when the call starts and disappear when it ends. A channel that comes up and stays is a call that never really hung up, and the residue builds until calls start failing for reasons that look unrelated.

Failures worth knowing about

Several failures looked successful at one layer while being broken at another. Each cost us about a day.

Reserving bandwidth of zero. A reserved channel with no bandwidth figure attached is still a reserved channel. The network sets it up, reports success, and the handset then refuses the call because the guarantee it was promised never materialised. Everything looks correct except the outcome.

Two systems sharing one counter. IMS and 5G authentication draw on the same anti-replay counter. Use it without advancing it and the next attempt is rejected as a replay. Registration then fails in a way that looks exactly like poor radio coverage, and you will spend a day on the radio.

A user plane that fails in silence. A real base station adds a small header to user traffic. With that handling switched off, every packet from the handset is discarded while the call setup, the reserved channel and every log you would think to check all report success. A reserved channel carrying no audio looks precisely like a working call that nobody can hear.

These behaviours are documented as part of the implementation, so they do not need to be rediscovered in every deployment.

Beyond the first implementation

Because SETU speaks published 3GPP service APIs rather than one vendor's dialect, the same bridge should work against any core exposing them. SD-Core is where we proved it, not where it has to end.

SETU sits between two groups. On the left, IMS and voice platforms reached via adapters: Kamailio IMS and other IMS cores, each with P/I/S-CSCF and rtpengine, unmodified on Cx, Rx and SIP/SMS. In the centre, SETU maps Cx to UDR, Rx to N5 and PCF, and SIP/SMS to the 5G core, through one canonical model behind a versioned API. On the right, any 5G core reached via connectors: SD-Core, Free5GC, Open5GS, Magma, OAI and other 5G cores.
One shared model in the middle. Add a core, write a connector; add an external system, write an adapter.

SETU stays separated from both the IMS and the core rather than embedding IMS logic inside SD-Core. That separation is the point. The result is not that SETU replaces part of either project, but that SD-Core can provide IMS services through Kamailio without the two being rewritten around each other.

Why this matters

SD-Core already provides subscriber registration, mobility, policy, sessions and user-plane connectivity. IMS extends that into the services expected of a complete mobile network: voice, video, messaging and policy-controlled reserved resources, on commercial handsets over a real 5G radio.

It does that without introducing a second subscriber system. SD-Core remains the system of record. Kamailio remains responsible for IMS. SETU provides the bridge between them, and the whole integration stays open.

Get it

The full deployment guide, with the build script, configuration and verification steps, is in the documentation: IMS on SD-Core with Kamailio, joined by SETU.

Everything builds from one repository:

git clone https://github.com/TOSSI-Foundation/TI.git
cd TI/CORE/sdcore-ims
./build-nf-images.sh --verify

That fetches the three network functions at setu-ims-rel1 and SETU at main, then builds them all. The --verify step reaches into each finished image and checks that code which exists only because of this work is actually present, which is the difference between "an image built" and "the right image built". It catches the most common mistake, which is building from the wrong branch.

The implementation is published under Apache-2.0. Standards: TS 29.214 (Rx), TS 29.228 and TS 29.229 (Cx), TS 29.514 (N5), TS 29.505 (Nudr), TS 24.501 (NAS and PCO), TS 24.011 and TS 23.040 (SMS), TS 38.413 (NGAP).

Built on SD-Core from the OMEC project and the Open Networking Foundation, deployed with aether-onramp, and on Kamailio and rtpengine, together with OCUDU and the LiteON O-RU used in the live radio deployment. Our thanks to their maintainers.