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 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.
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 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.
| Function | Branch | What it learned to do |
|---|---|---|
| SMF (sessions) | setu-ims-rel1 | tell the handset where the IMS is; turn a policy decision into a reserved-bandwidth channel a handset will accept |
| PCF (policy) | setu-ims-rel1 | match 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-rel1 | resilience 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
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.
| Capability | Result |
|---|---|
| Registration | full challenge-and-response, accepted by the handset, no HSS on the network |
| Voice | dedicated reserved channel per call, released on hangup |
| Video | second reserved channel alongside voice |
| Messaging | both directions, plain text and emoji, with delivery reports |
| Teardown | clean across repeated calls, nothing accumulating |
| Restart safety | SETU 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 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.
- SETU: github.com/coranlabs/SETU
- Integration and build environment: TOSSI-Foundation/TI, at
CORE/sdcore-ims/ - Network functions: sdcore-smf, sdcore-pcf, sdcore-amf, all on
setu-ims-rel1
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.