Registrar¶
A SIP registrar accepts REGISTER, authenticates the subscriber, and stores their
Contact so calls can be routed to them later. This recipe is a registrar that also
proxies calls to the registered contacts.
Config¶
# siphon.yaml
listen:
udp: ["0.0.0.0:5060"]
tcp: ["0.0.0.0:5060"]
domain:
local: ["example.com"]
script:
path: "/etc/siphon/registrar.py"
registrar:
backend: redis # memory | redis | postgres | python
redis:
url: "redis://127.0.0.1:6379"
default_expires: 3600
max_expires: 7200
auth:
realm: "example.com"
backend: static # static | http | database | diameter_cx
# static credentials for a quick start (use http/database in production):
# credentials:
# alice: "secret"
backend: redis makes the registrar survive a restart — see
Scaling & redundancy for exactly what that does
(durability + a boot snapshot, not live cross-node sync).
Script¶
from siphon import proxy, registrar, auth, log
DOMAIN = "example.com"
@proxy.on_request
def route(request):
# In-dialog requests follow the established route set.
if request.in_dialog:
if request.loose_route():
request.relay()
else:
request.reply(404, "Not Here")
return
# REGISTER: challenge, then store the contact. registrar.save() also sends
# the 200 OK with the granted Expires.
if request.method == "REGISTER":
if not auth.require_digest(request, realm=DOMAIN):
return # 401 challenge already sent
request.fix_nated_register() # rewrite Contact with the observed source
registrar.save(request)
return
# Anything else (INVITE, MESSAGE, …): look up the AoR and route to it.
contacts = registrar.lookup(request.ruri)
if not contacts:
request.reply(404, "Not Found")
return
request.record_route()
request.fork(contacts) # ring all bindings; first 2xx wins
A few things worth knowing:
registrar.save(request)sends the 200 OK for you (with the granted Expires, clamped bymax_expires). You don't reply yourself.request.fork(contacts)passes theContactobjects (not just.uri). For a binding this node accepted, that routes over the captured inbound flow — the only way to reach a WebSocket UE (RFC 5626 §5.3 connection reuse). Non-local contacts fall back to URI routing.request.fix_nated_register()rewrites the Contact with the source the packet actually came from, so NAT'd clients are reachable. Pair it withnat:config.
React to registration changes¶
@registrar.on_change
def on_reg_change(aor, event_type, contacts):
# event_type: "registered" | "refreshed" | "deregistered" | "expired"
log.info(f"{aor} {event_type}: {len(contacts)} contact(s)")
Use this to push presence, notify an external system, or emit charging events.
Test it¶
Register and look up with any SIP client, or with the in-repo
sipcli.py:
If you enabled the admin API (admin.listen), confirm the binding over HTTP:
See also¶
- Real example:
examples/registrar_proxy.py(adds external-API contact notifications) andscripts/proxy_default.py. - Stateful proxy — routing and forking in more depth.
- Hardening & security — auth backends, rate limiting, bans.