Skip to main content

SIPhon

siphon-sip

Mock library and type stubs for SIPhon scripts — enables unit testing without the Rust binary and provides rich context for LLM-assisted script authoring.

Install

pip install siphon-sip

The PyPI distribution is siphon-sip (matching the siphon-sip crate). The import package is still siphon_sdkfrom siphon_sdk import ….

What is SIPhon?

SIPhon is a high-performance SIP proxy, B2BUA, and IMS platform written in Rust with Python scripting. Scripts use decorators to handle SIP events:

from siphon import proxy, registrar, auth, log

@proxy.on_request
def route(request):
    if request.method == "REGISTER":
        if not auth.require_digest(request, realm="example.com"):
            return
        registrar.save(request)
        request.reply(200, "OK")
        return

    contacts = registrar.lookup(request.ruri)
    if not contacts:
        request.reply(404, "Not Found")
        return

    request.record_route()
    request.fork([c.uri for c in contacts])

This SDK lets you test these scripts with pytest — no Rust binary needed.

Quick start

from siphon_sdk.testing import SipTestHarness
from siphon_sdk.types import Contact

harness = SipTestHarness(local_domains=["example.com"])
harness.load_script("scripts/proxy_default.py")

# Pre-populate the registrar
harness.registrar.add_contact(
    "sip:alice@example.com",
    Contact(uri="sip:alice@192.168.1.5:5060"),
)

# Test REGISTER challenge
result = harness.send_request("REGISTER", "sip:alice@example.com",
                              from_uri="sip:alice@example.com")
assert result.status_code == 401  # digest challenge

# Test INVITE routing
result = harness.send_request("INVITE", "sip:alice@example.com")
assert result.action == "fork"
assert "sip:alice@192.168.1.5:5060" in result.targets

Testing B2BUA scripts

harness = SipTestHarness()
harness.load_script("scripts/b2bua_default.py")

harness.registrar.add_contact(
    "sip:bob@example.com",
    Contact(uri="sip:bob@10.0.0.2:5060"),
)

result = harness.send_invite(ruri="sip:bob@example.com")
assert result.action == "fork"
assert result.targets == ["sip:bob@10.0.0.2:5060"]

# Test BYE handling
result = harness.send_bye(initiator_side="a")
assert result.was_terminated

Testing extension scripts (SMPP, HTTP)

The opt-in siphon extensions inject extra namespaces (smpp, http) at runtime. The SDK mocks them too, with dedicated harnesses, so extension scripts are testable from the same pip install siphon-sip — no running SMSC or HTTP listener required:

from siphon_sdk.smpp_testing import SmppTestHarness

harness = SmppTestHarness()
harness.load_script("scripts/gateway.py")
assert harness.bind("esme1", password="s3cret")
reply = harness.submit_sm(source_addr="15550100", destination_addr="15550101",
                          short_message=b"hi")
assert reply.ok
from siphon_sdk.http_testing import HttpTestHarness
from siphon_sdk.http import MockResponse

harness = HttpTestHarness()
harness.add_response(MockResponse(status=200, body=b'{"ok":true}'))
harness.load_script("scripts/api.py")

resp = harness.request("GET", "/users/42")
assert resp.status == 200

Inline scripts

Test scripts without separate files:

harness = SipTestHarness()
harness.load_source("""
from siphon import proxy

@proxy.on_request
def route(request):
    if request.source_ip_in(["10.0.0.0/8"]):
        request.relay()
    else:
        request.reply(403, "Forbidden")
""")

result = harness.send_request("INVITE", "sip:bob@host", source_ip="10.1.2.3")
assert result.was_relayed

result = harness.send_request("INVITE", "sip:bob@host", source_ip="8.8.8.8")
assert result.status_code == 403

Async handlers + RTPEngine

harness = SipTestHarness()
harness.load_source("""
from siphon import proxy, rtpengine

@proxy.on_request
async def route(request):
    if request.method == "INVITE" and request.body:
        await rtpengine.offer(request, profile="srtp_to_rtp")
    request.relay()
""")

result = harness.send_request("INVITE", "sip:bob@host",
                              body=b"v=0\\r\\n...",
                              content_type="application/sdp")
assert result.was_relayed
assert harness.rtpengine.operations == [("offer", "srtp_to_rtp")]

Controlling mock behavior

# Auth: allow or deny all
harness.auth._allow = True  # all auth checks pass

# Rate limiting
harness.proxy._utils._rate_limit_allow = False  # simulate overload

# Cache: pre-populate
harness.cache.set_data("cnam", {"key": "value"})

# Registrar: add contacts directly
harness.registrar.add_contact("sip:alice@host", Contact(uri="sip:alice@1.2.3.4"))

# Log: inspect captured messages
assert any("error" in msg for level, msg in harness.log.messages)

# Reset between tests
harness.reset()

Result assertions

RequestResult provides convenient properties:

Property Description
.action Primary action: "reply", "relay", "fork", "silent_drop"
.status_code SIP status code (200, 401, 404, etc.)
.reason Reason phrase
.targets Fork targets list
.strategy Fork strategy ("parallel" / "sequential")
.was_relayed True if relay() was called
.was_forked True if fork() was called
.was_dropped True if handler returned without action (silent drop)
.record_routed True if record_route() was called
.request The mock Request object for header inspection

API reference

Namespaces

Import Description
proxy Stateful/stateless proxy decorators and utilities
registrar Address-of-record contact store
auth SIP digest authentication
b2bua Back-to-back user agent call control
log Structured logging
cache Named cache (local LRU + Redis)
rtpengine RTPEngine media proxy operations
gateway Destination groups, load balancing, health probing
cdr Call detail records
diameter Diameter protocol (Cx, Ro, Rx, Rf, Sh)
presence SUBSCRIBE/NOTIFY, PIDF presence
li Lawful intercept (ETSI X1/X2/X3, SIPREC)
registration Outbound REGISTER client (trunk registration)

Request properties

Property Type Description
method str SIP method ("INVITE", "REGISTER", etc.)
ruri SipUri Request-URI
from_uri SipUri | None From header URI
to_uri SipUri | None To header URI
from_tag str | None From-tag
to_tag str | None To-tag (None for initial requests)
call_id str | None Call-ID
cseq (int, str) | None CSeq tuple
in_dialog bool Both tags present
max_forwards int Max-Forwards value
body bytes | None Message body
content_type str | None Content-Type
transport str "udp", "tcp", "tls", "ws", "wss"
source_ip str Sender IP
auth_user str | None Authenticated username
event str | None Event header

Request methods

Method Description
reply(code, reason) Send SIP response
relay(next_hop=None) Forward to destination
fork(targets, strategy="parallel") Fork to multiple targets
record_route() Insert Record-Route
loose_route() -> bool RFC 3261 loose routing
get_header(name) -> str | None Get header value
set_header(name, value) Set header
remove_header(name) Remove header
has_header(name) -> bool Check header exists
has_body(content_type) -> bool Check body type
set_ruri_user(value) Set R-URI user part
set_ruri_host(value) Set R-URI host
source_ip_in(cidrs) -> bool CIDR membership check
generate_icid() -> str Generate charging ID
add_path(uri) Prepend Path header
prepend_route(uri) Prepend Route header
fix_nated_register() NAT fixup for REGISTER
fix_nated_contact() NAT fixup for Contact

Registrar

Method Description
save(request, force=False) Save REGISTER bindings
lookup(uri) -> list[Contact] Look up contacts (sorted by q-value)
is_registered(uri) -> bool Check if URI has contacts
service_route(uri) -> list[str] Get stored service routes (RFC 3608)
set_service_routes(aor, routes) Store service routes for an AoR
save_pending(request) IMS: save binding in pending state
confirm_pending(uri) IMS: promote pending to active after SAR
asserted_identity(uri) -> str | None IMS: stored P-Asserted-Identity
reginfo_xml(aor, state, version) -> str Generate reginfo XML (RFC 3680)
on_change Decorator: fires on registration state changes

lookup() returns Contact objects:

Field Description
uri Contact URI as advertised by the UE
received Transport source of the REGISTER as a SIP URI (sip:<ip>:<port>;transport=<proto>), or None. Prefer it over uri when routing — the Contact URI may carry a private/NAT address (contact.received or contact.uri)
q Quality value (0.0–1.0), higher wins
expires Seconds remaining on the binding
age_secs Seconds since the binding was created or refreshed
path RFC 3327 Path headers stored with the binding
instance_id / instance_epoch Which siphon instance/process accepted the REGISTER
is_local True when this process accepted the binding
flow_token / flow Captured inbound flow for RFC 5626 connection reuse
params Contact-header params preserved from the REGISTER (RFC 3840 feature tags)
kind "ue" (routable) or "as" (AS capability record)

Auth

Method Description
require_www_digest(request, realm) -> bool 401 challenge
require_proxy_digest(request, realm) -> bool 407 challenge
require_digest(request, realm) -> bool Alias for www_digest
verify_digest(request, realm) -> bool Verify without challenge
require_ims_digest(request, realm) -> bool IMS AKA via Diameter Cx MAR
require_aka_digest(request, realm) -> bool Local Milenage AKA (no HSS)

B2BUA call

Each B-leg gets a fresh Call-ID and From-tag by default, fully decoupling the two SIP dialogs. Use keep_call_id() to opt out of Call-ID regeneration.

Property/Method Description
call.id UUID
call.state "calling", "ringing", "answered", "terminated"
call.from_uri A-leg From URI
call.ruri A-leg Request-URI
call.reject(code, reason) Reject call
call.dial(uri, timeout=30) Dial single target
call.fork(targets, strategy, timeout) Fork to multiple
call.terminate() End call (BYE both legs)
call.keep_call_id() Copy A-leg Call-ID to B-leg (From-tag always unique)
call.set_credentials(user, pass) B-leg digest auth credentials (auto 401/407 retry)
call.media.anchor(engine) Anchor media through RTPEngine
call.media.release() Release media anchor
call.session_timer(expires, min_se, refresher) Per-call RFC 4028 session timer
call.record(srs_uri) Start SIPREC recording
call.stop_recording() Stop SIPREC recording

License

MIT

Release files for siphon-sip 1.8.6

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for siphon-sip 1.8.6
File Size Uploaded
siphon_sip-1.8.6.tar.gz 231.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for siphon-sip 1.8.6
File Interpreter ABI Platform
siphon_sip-1.8.6-py3-none-any.whl Python 3 none any Details

Total release size: 403.8 kB

Release files / siphon_sip-1.8.6.tar.gz

Download URL siphon_sip-1.8.6.tar.gz
Size 231.6 kB
Tags Source
SHA-256 checksum
How to use checksums
3842f285290ff50ef020a1dd389879ded107f78f3298b14b6fc0fcd8a0f08607
BLAKE2b-256 checksum
How to use checksums
cbac3c9e6103a287ff6162918d98885355c430a7c0bfaca818e57d2778199ec1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 8, 2026.

Transparency log

Release files / siphon_sip-1.8.6-py3-none-any.whl

Download URL siphon_sip-1.8.6-py3-none-any.whl
Size 172.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1d0558d7315b957244df3c6d68d8df640393cdc1340c6345d22f19b76aa60034
BLAKE2b-256 checksum
How to use checksums
62ff3868b48aed6888bce8d4e644b30f27124eaf13704b9c3dbd272cf234e4a9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 8, 2026.

Transparency log

Release history Release notifications | RSS feed

1.10.0

2 release files

1.9.1

2 release files

1.9.0

2 release files

1.8.7

2 release files

This release

1.8.6 This release

2 release files

1.8.5

2 release files

1.8.4

2 release files

1.8.3

2 release files

1.8.2

2 release files

1.8.1

2 release files

1.8.0

2 release files

1.7.1

2 release files

1.7.0

2 release files

1.6.0

2 release files

1.5.1

2 release files

1.5.0

2 release files

1.4.1

2 release files

1.4.0

2 release files

1.3.0

2 release files

1.2.1

2 release files

1.2.0

2 release files

1.1.1

2 release files

1.1.0

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page