Migrating from Kamailio / OpenSIPS¶
A practical concept map for engineers coming from Kamailio or OpenSIPS, plus the one topic that trips people up: state sharing and clustering.
This is not a feature-by-feature parity claim. SIPhon's protocol engine is faithful
to the same RFCs (transactions per RFC 3261 §17, registrar §10, proxy §16), so the
concepts map directly — what changes is the surface: a Python API + YAML instead
of a routing-script DSL + modparam.
The mental-model shift¶
| Kamailio / OpenSIPS | SIPhon |
|---|---|
kamailio.cfg / opensips.cfg routing-script DSL |
Python handlers (@proxy.on_request, @b2bua.on_invite, …) |
modparam("module", "param", value) |
one siphon.yaml, documented inline |
loadmodule "x.so" |
nothing — capabilities are namespaces you import |
| Edit cfg → restart (or limited rtimer reloads) | Edit script → inotify hot-reload, no restart |
$avp(...), $var(...), $dlg_val(...) |
ordinary Python variables and objects |
| Test with a live instance + SIPp | Unit-test scripts with the siphon-sip mock SDK, plus SIPp |
The biggest day-to-day change: routing logic is real code — functions, imports,
a debugger, pytest — not an expression language.
Routing & transactions¶
| Kamailio / OpenSIPS | SIPhon |
|---|---|
t_relay() |
request.relay() |
t_relay() to a fixed target / $du |
request.relay("sip:next@host:port") |
t_load_contacts() + t_next_contacts() (serial/parallel forking) |
request.fork(targets, strategy="parallel"\|"sequential") |
record_route() |
request.record_route() |
loose_route() |
request.loose_route() |
failure_route[...] |
@proxy.on_failure (and per-relay on_failure=) |
onreply_route[...] |
@proxy.on_reply |
sl_send_reply() / t_reply() |
request.reply(code, reason) |
$ru, $rU, $rd |
request.ruri, request.ruri.user, request.ruri.host |
$fU/$tU, $ft/$tt |
request.from_uri/to_uri, request.from_tag/to_tag |
is_method("INVITE") |
@proxy.on_request("INVITE") or request.method == "INVITE" |
has_totag() / loose-route dialog check |
request.in_dialog |
CANCEL handling, Max-Forwards enforcement, retransmission absorption and ACK-for-non-2xx are done by the SIPhon transaction layer automatically — you don't write routes for them (see the framework-handles-automatically notes in the API reference).
Registrar / user location¶
| Kamailio / OpenSIPS | SIPhon |
|---|---|
save("location") |
registrar.save(request) (also sends the 200 OK) |
lookup("location") |
registrar.lookup(uri) → list[Contact] |
registered("location") |
registrar.is_registered(uri) |
usrloc DB modes / db_url |
registrar.backend: memory \| redis \| postgres |
nathelper / fix_nated_* |
request.fix_nated_contact() / fix_nated_register() / add_contact_alias() |
Auth¶
| Kamailio / OpenSIPS | SIPhon |
|---|---|
www_authorize() / auth_check() |
auth.require_www_digest(request, realm) |
proxy_authorize() |
auth.require_proxy_digest(request, realm) |
pv_www_authenticate() w/ AKA |
auth.require_aka_digest() / auth.require_ims_digest() |
State, data & dispatch¶
| Kamailio / OpenSIPS | SIPhon |
|---|---|
htable ($sht(...)) |
cache namespace (local LRU + optional Redis, shared live) |
dispatcher module + ds_select_dst() |
gateway namespace (gateway.select(group, key=…)) + YAML gateway.groups |
dialog module ($dlg_val, profiles) |
the B2BUA call object (@b2bua.*, call.*) for true call control |
rtpengine_*() |
call.media.anchor() / the rtpengine namespace |
sqlops / avpops against a DB |
plain Python (use a DB client in an async handler, off the hot path) |
Module state habit to unlearn: in cfg you'd stash cross-request state in
htable. In SIPhon scripts, don't hold cross-request state in module-level Python dicts/lists — use thecachenamespace (Redis-backed, survives hot-reload, shared across nodes). Module-level constants are fine.
Clustering & shared state — read this carefully¶
This is where expectations differ most, because Kamailio and OpenSIPS both grew explicit state-replication subsystems and SIPhon deliberately did not.
What you're used to:
- OpenSIPS
clusterer— distributed user location via full-sharing (full-mesh broadcast; every node mirrors the whole location table) or federation (partitioned), with active/backup sharing tags and seed-node sync. - Kamailio
dmq/dmq_usrloc— replicate usrloc/dialog/htable between nodes over theKDMQSIP method, with node auto-discovery (and the requirement that all nodes run the same major version).
What SIPhon does instead — and why — is covered in full in scaling-and-redundancy.md. The short version:
- There is no live cross-node replication engine. Multi-node redundancy is a front LB + DNS SRV + a shared Redis registrar backend.
- The Redis backend gives durability + a boot-time snapshot, not live replication: a node reads its own in-memory location table at lookup time and reloads the full snapshot from Redis on restart. So a binding registered on node A is reachable for terminating calls on node A; to get any-node terminating delivery, hash REGISTER and INVITE for an AoR to the same node at the LB (subscriber affinity).
- The
cacheandsubscribe_statenamespaces are shared live via Redis (the closest analogue to a replicatedhtable).
Porting your topology¶
| If you ran… | Port it to… |
|---|---|
OpenSIPS clusterer full-sharing usrloc |
Front LB with subscriber affinity (consistent hash on AoR) + shared Redis registrar. No mesh to operate. |
| OpenSIPS federation / partitioned usrloc | The same affinity hash is your partitioning — each AoR deterministically maps to one node. |
Kamailio dmq_usrloc active/active pair |
Active/active behind a front LB + DNS SRV, shared Redis; or active/standby on a VIP with re-register convergence. |
Kamailio dmq for htable replication |
Point all nodes at one Redis and use the cache namespace (already shared live). |
dispatcher health-checked gateway pools |
gateway.groups with probe.enabled: true (per-node probing). |
What's intentionally different (don't expect it)¶
- Live dialog/transaction replication across nodes — not provided. A node death drops its in-flight calls; new calls fail over via DNS SRV. (This matches Kamailio/ OpenSIPS without an active replication module.)
- A cluster membership protocol — there isn't one to configure, monitor, or debug. Redis + DNS are the only shared infrastructure.
- Version-locked node meshes — not applicable; nodes don't talk a replication protocol to each other, so they don't have to move in lockstep.
If your scale genuinely requires N-way live usrloc replication with zero affinity, that's a deliberate design discussion — see the "when would you miss one" section of scaling-and-redundancy.md.
Where to look next¶
- scaling-and-redundancy.md — the full state model.
- deployment.md — ready-to-run topologies and the ops runbook.
- The Python API reference and the
sdk/mock library — for the per-method surface you'll port routes onto.