Skip to content

Stateful proxy

A proxy routes requests without taking part in the media or owning the dialog. This recipe is a residential/edge proxy: it challenges REGISTER, routes calls to registered contacts, loose-routes in-dialog traffic, and drops garbage before it costs you anything.

Script

from siphon import proxy, registrar, auth, log

DOMAIN = "example.com"

@proxy.on_request
def route(request):
    # 1. Drop malformed / torture traffic before any work (RFC 4475).
    #    Scoped to out-of-dialog requests; dropped silently so we don't
    #    fingerprint the server to scanners.
    if not request.in_dialog and not proxy.sanity_check(request):
        return

    # 2. Local OPTIONS keepalive.
    if request.method == "OPTIONS" and request.ruri.is_local and not request.ruri.user:
        request.reply(200, "OK")
        return

    # 3. In-dialog requests follow the dialog's route set.
    if request.in_dialog:
        if request.loose_route():
            request.relay()
        else:
            request.reply(404, "Not Here")
        return

    # 4. REGISTER.
    if request.method == "REGISTER":
        if not auth.require_digest(request, realm=DOMAIN):
            return
        registrar.save(request)
        return

    # 5. Out-of-dialog INVITE/MESSAGE/…: location lookup + fork.
    if not request.ruri.user:
        request.reply(484, "Address Incomplete")
        return
    contacts = registrar.lookup(request.ruri)
    if not contacts:
        request.reply(404, "Not Found")
        return
    request.record_route()
    request.fork(contacts)

The routing primitives

Call What it does
request.relay() / request.relay("sip:next@host") Forward to the next hop (or an explicit target). Like Kamailio t_relay().
request.fork(targets, strategy="parallel"\|"sequential") Fork to many targets; first 2xx wins (parallel) or try in order (sequential).
request.record_route() Insert this proxy into the dialog's route set so in-dialog requests come back.
request.loose_route() Consume the top Route and route the request onward (RFC 3261 §16.12).
request.reply(code, reason) Send a response from the proxy.

The transaction layer handles CANCEL matching, Max-Forwards, retransmissions and ACK-for-non-2xx automatically — you don't write routes for those.

Aggregating fork results

For a parallel fork, SIPhon aggregates the branches per RFC 3261 §16.7 (first 2xx wins, best error otherwise). To act when all branches fail:

@proxy.on_failure
def failure_route(request, reply):
    # e.g. send to voicemail, or relay the best error upstream
    reply.relay()

And to touch responses on the way back (rewrite headers, strip internal info):

@proxy.on_reply
def reply_route(request, reply):
    reply.remove_header("X-Internal-Trace")
    reply.relay()

Test it

# register a contact, then place a call to it
python3 deploy/ha-demo/sipcli.py register 127.0.0.1 5060 bob 127.0.0.1 5080
python3 deploy/ha-demo/sipcli.py invite   127.0.0.1 5060 bob     # -> 1xx (routed)

See also