Extensions (SMPP, HTTP, SIGTRAN)¶
SIPhon's core speaks SIP. Protocol functionality beyond SIP — SMPP, HTTP and
SIGTRAN/SS7 today — is provided by opt-in extension modules. They
are not part of the default binary: you enable a module at build time and
configure it through the extensions: block in siphon.yaml. Each module adds
a scriptable Python namespace your routing scripts can use, alongside the
built-in proxy, registrar, cache, and friends.
How extensions work¶
- Not in the standard binary.
cargo install siphon-sip, and the default container image, contain no extensions at all. - Enabled at build. An extension-capable build is produced by the
siphon-binpackage. It is a drop-insiphonbinary — same CLI, samesiphon.yaml, plus the modules. httpis on by default there. A barecargo build -p siphon-bingets you thehttpnamespace. It is the one module with no deployment prerequisite — no libsctp, no upstream SMSC bind, nothing to provision — and the one most scripts reach for. Every other module is opt-in (--features smpp,--features sigtran, or--features fullfor all of them), and features are additive, so--features smppgives you http and smpp. Drop HTTP with--no-default-features.- Configured in
siphon.yaml. Anextensions:map points each enabled module at its own config file:
- Loud on mismatch. If
extensions.smppis configured but the running binary was not built with that feature, siphon logs a warning and skips the module — it never silently ignores configuration. (This mirrors the optionalsctptransport feature.)
SMPP (SMS, SMPP 3.4)¶
The SMPP extension turns siphon into a scriptable SMPP node — it accepts ESME binds and can hold outbound binds to upstream SMSCs. Your script decides policy; siphon handles the wire protocol, sessions, timers, and windowing.
1. Build with the feature¶
# Native binary (http comes from the default feature set; smpp is additive)
cargo build -p siphon-bin --release --features smpp
# …or a container image (mount your config + script at runtime)
docker build -f siphon-bin/Dockerfile --build-arg FEATURES=smpp -t siphon-smpp siphon-bin/
2. Point siphon at the SMPP config¶
The smpp.yaml schema (inbound listener, outbound binds, routing) is documented
in the siphon-smpp repository.
3. Handle PDUs in your script¶
from siphon import smpp, log
@smpp.on_bind
async def authorise(bind):
log.info(f"bind from {bind.system_id}")
return bind.accept()
@smpp.on_pdu("submit_sm")
async def handle(pdu, session):
log.info(f"{pdu.source_addr} -> {pdu.destination_addr}")
# ...route / persist / throttle...
return pdu.reply(message_id="abc123")
Scripts hot-reload exactly like the SIP side — edit and the next PDU uses the new code.
Further reading¶
The full smpp namespace (PDU types, bind handling, outbound submit/deliver,
delivery receipts), the complete smpp.yaml schema, and deployment examples live
in the siphon-smpp docs and repository:
- 📖 Documentation: https://smpp.siphon-sip.org/
- 💻 Source: https://github.com/siphon-project/siphon-smpp
HTTP (route serving + outbound client)¶
The HTTP extension lets routing scripts serve inbound HTTP (@http.route)
and call out (http.Client) from the same asyncio loop they use for SIP —
useful for webhooks, health/readiness endpoints, small REST surfaces, and
provisioning callbacks. The server is axum + rustls (HTTP/1.1 and HTTP/2, TLS and
mutual TLS); the client is pooled reqwest.
Enable it for the client alone — even if you never serve a route
If a script makes outbound HTTP calls on the hot path (a REST lookup per
INVITE, a provisioning callback, an auth token refresh), enable the http
feature and use http.Client rather than reaching for a pure-Python library
(requests / httpx / urllib). With http.Client the entire round-trip
runs in Rust on siphon's Tokio runtime — connection pooling, TLS, and
HTTP/1.1 + HTTP/2 framing — and each call is a real awaitable that hands the
asyncio driver loop back while the request is in flight, so the driver keeps
dispatching other handlers. A synchronous Python client instead does the
protocol work in the interpreter and blocks its driver loop for the whole
round-trip, stalling every other handler that shares it. Same pooled client
across calls, no per-call setup:
from siphon import http, proxy
api = http.Client("api") # named, pooled — construct once, reuse
@proxy.on_request("INVITE")
async def screen(request):
verdict = await api.get(f"/screen/{request.from_uri.user}")
if verdict.status != 200:
request.reply(403, "Blocked")
return
request.relay()
You do not need to declare an http.servers listener to use the client —
an http.yaml with only a clients: block is enough.
1. Build with the feature¶
2. Point siphon at the HTTP config¶
3. Serve routes in your script¶
from siphon import http
@http.route("/healthz")
def healthz(req):
return http.Response(status=200, body=b"ok")
@http.route("/users/{id}", methods=["GET"])
async def get_user(req):
async with http.Client("api") as client:
upstream = await client.get(f"/v1/users/{req.path_params['id']}")
return http.Response(status=upstream.status, body=upstream.body)
Further reading¶
The full http namespace (Request/Response/Client, middleware, startup
hooks, path/query params, TLS/mTLS), the http.yaml schema, and examples live in
the siphon-http docs and repository:
- 📖 Documentation: https://http.siphon-sip.org/
- 💻 Source: https://github.com/siphon-project/siphon-http
SIGTRAN / SS7 (M3UA, M2PA, SUA)¶
The SIGTRAN extension turns siphon into a scriptable SS7 signalling node. It carries MTP3 user traffic over kernel SCTP (M3UA per RFC 4666, M2PA per RFC 4165, SUA per RFC 3868), resolves MTP3 routes and SCCP Global Title Translation in Rust at line rate, and hands locally-addressed dialogues to your script for MAP / CAP / INAP termination. Your script decides policy and programs the routing tables; siphon owns the associations, the ASPSM/ASPTM handshake, link alignment, SSNM route state, and the TCAP transaction engine.
Typical roles: STP (transit + screening), HLR / HSS front-end, terminating SMSC, CAMEL SCP, or an IN SCP terminating the SSF-SCF dialogue.
1. Build with the feature¶
async-sctp links libsctp, so unlike the other modules this one has a system
dependency at both build and run time:
sudo apt-get install -y libsctp-dev # libsctp1 at runtime
cargo build -p siphon-bin --release --features sigtran
The container image installs both already — build it with
--build-arg FEATURES=sigtran (or full), and run it with host networking so
the SCTP associations see real addresses.
2. Point siphon at the SIGTRAN config¶
The node — point code, associations, application servers, MTP3 routes, GTT rules,
owned subsystems — is described in sigtran.yaml:
# sigtran.yaml
node:
point_code: 1000
variant: ITU
associations:
- { id: msc-1, adaptation: m3ua, role: server, addrs: [10.1.0.10], port: 2905 }
application_servers:
- { name: msc, traffic_mode: override, routing_context: 100, asps: [msc-1] }
mtp3_routes:
- { dpc: 2000, as: msc, priority: 1 }
sccp:
local_ssns: [8] # SSNs we own; inbound traffic for them terminates locally
siphon reads this at startup and builds the node before your script loads, so the script's decorators register into the very node the live transport then drives.
3. Route and terminate in your script¶
from siphon import ss7, gsm_map
# Program the Rust routing tables live (they stay in Rust on the hot path).
ss7.routes.add(dpc=3000, as_="msc", priority=1)
ss7.routes.cache("15550100", dpc=2000, ssn=6) # GT translation, resolved in Rust
@gsm_map.on_operation("mo-forward-sm")
async def on_mo(dlg, arg):
# arg.sm_rp_oa / sm_rp_da / sm_rp_ui are the raw addresses + TPDU bytes.
dlg.reply(gsm_map.mo_forward_sm_res()) # returnResultLast in a closing End
dlg.end()
Scripts hot-reload like the SIP side; routing state lives in Rust, so a reload drops nothing in flight.
Four namespaces, not one
SMPP and HTTP each expose a single namespace object. SIGTRAN mounts four
(ss7, gsm_map, gsm_cap, inap) plus the SigtranError exception,
siphon.configure / siphon.metrics, and the shared types — so it is
composed through SiphonServer::register_module_extension rather than
register_namespace_with. Nothing about that is visible to a script author;
it is simply from siphon import … for all of it.
Further reading¶
The full ss7 / gsm_map / gsm_cap / inap namespaces, every sigtran.yaml
field, the loopback test seam (siphon.configure(...) → node.deliver(...), which
drives a handler with no live peer), and the STP / HLR / SMSC / SCP recipes live
in the siphon-sigtran docs and repository:
- 📖 Documentation: https://sigtran.siphon-sip.org/
- 💻 Source: https://github.com/siphon-project/siphon-sigtran
Testing extension scripts¶
The siphon-sip SDK (pip install
siphon-sip) ships mocks and pytest harnesses for the extension namespaces
alongside the SIP ones, so you can unit-test SMPP and HTTP scripts without a
running SMSC or listener — and get type hints / docstrings while authoring:
from siphon_sdk.smpp_testing import SmppTestHarness
from siphon_sdk.http_testing import HttpTestHarness
def test_submit_sm():
h = SmppTestHarness()
h.load_script("scripts/gateway.py")
assert h.bind("esme1", password="s3cret")
reply = h.submit_sm(source_addr="15550100", destination_addr="15550101",
short_message=b"hi")
assert reply.ok
def test_healthz():
h = HttpTestHarness()
h.load_script("scripts/api.py")
assert h.request("GET", "/healthz").body == b"ok"
SIGTRAN scripts are tested differently — there is no SDK mock for the ss7 /
gsm_map / gsm_cap / inap namespaces. Instead siphon-sigtran ships an
in-process loopback seam: siphon.configure("sigtran.yaml") returns a node you
can hand a genuine assembled Begin (real TCAP inside a real SCCP UDT) and read
back the reply MSUs your handler produced, with no peer and no socket. See
Testing your handlers.
Available modules¶
| Module | Feature | Status | Namespace | Docs |
|---|---|---|---|---|
| SMPP 3.4 | smpp |
Available | smpp |
smpp.siphon-sip.org |
| HTTP / HTTPS | http |
Available | http |
http.siphon-sip.org |
| SIGTRAN / SS7 | sigtran |
Available | ss7, gsm_map, gsm_cap, inap |
sigtran.siphon-sip.org |
Turn several on at once with --features full, or name them individually
(--features "smpp,sigtran").