Skip to content

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-bin package. It is a drop-in siphon binary — same CLI, same siphon.yaml, plus the modules.
  • http is on by default there. A bare cargo build -p siphon-bin gets you the http namespace. 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 full for all of them), and features are additive, so --features smpp gives you http and smpp. Drop HTTP with --no-default-features.
  • Configured in siphon.yaml. An extensions: map points each enabled module at its own config file:
extensions:
  smpp: /etc/siphon/smpp.yaml
  sigtran: /etc/siphon/sigtran.yaml
  • Loud on mismatch. If extensions.smpp is 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 optional sctp transport 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

# siphon.yaml
extensions:
  smpp: /etc/siphon/smpp.yaml

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:

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

cargo build -p siphon-bin --release --features http

2. Point siphon at the HTTP config

# siphon.yaml
extensions:
  http: /etc/siphon/http.yaml

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:

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

# siphon.yaml
extensions:
  sigtran: /etc/siphon/sigtran.yaml

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:

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").