Skip to content

SIPhon Feature Readiness Matrix

Overview

This document tracks the maturity of every SIPhon feature across three readiness levels. SIPhon runs in production today in a residential SIP registrar/proxy role and a 3GPP IMS deployment exercising Diameter Cx/Sh/Rx, iFC, IPsec, and 5G SBI policy control. Features validated on live traffic are marked Production.

Readiness Meaning
Production Running on live traffic today
Implemented Code-complete, unit/integration tested, not yet production-deployed
Planned Partially wired or design-only

Core SIP Engine

Feature Readiness Config Notes
Stateful proxy (RFC 3261 §16) Production script: @proxy.on_request Full transaction state machines; ICT Timer A RFC-compliant (capped at T2, fires in Proceeding, cancelled on final response)
B2BUA (RFC 3261 §6) Production script: @b2bua.on_invite Two-leg call control, per-leg Call-ID + From-tag, topology hiding. The call.fork/call.dial timeout= (default 30s) is now enforced: a B-leg INVITE sent fire-and-forget (no client transaction, so no Timer B) that never produces a final 2xx — dead/partitioned trunk — is failed within timeout..timeout+30s by the orphan sweep (fail_b2bua_call_on_timeout): CANCEL pending legs, @b2bua.on_failure(408), 408 Request Timeout to the A-leg, teardown. Previously the call leaked until the 24h orphan backstop. Unit-tested in take_timed_out_calls_only_unanswered_past_deadline. Outbound auth-retry 2xx ACK: a B-leg INVITE to an authenticating trunk (401/407 → credentialed CSeq-2 retry) is superseded in place (replace_b_leg), which drops the failed leg's actor handle so that actor emits CallEvent::Terminated onto the SHARED per-call classification channel. The dispatcher block-recvs that channel per response; consuming the stale Terminated desynced the stream so the retry leg's 200 OK was misclassified as the prior 18x's provisional — set_winner and the deferred B-leg ACK (RFC 3261 §14.1 late-ACK) were skipped, so the trunk's 200 OK retransmitted unacked and the call collapsed into a BYE storm ~5 s after answer (all outbound PSTN to an authenticating trunk). Fixed by recv_b_leg_classification_event skipping Terminated lifecycle events when reading a response classification (regression-tested in dispatcher::tests::b_leg_200_classifies_as_answered_after_auth_retry_supersede). Outbound retry member affinity (RFC 5923): the 401/407 credentialed re-INVITE and the RFC 4028 422 higher-Session-Expires re-INVITE are fresh pre-dialog transactions (new branch + CSeq, no To-tag), so the in-dialog connection-reuse path did not cover them — they re-resolved the trunk hostname and the RFC 3263 §4.2 A/AAAA shuffle could land the retry on a different member of a multi-member trunk (one DNS name) than the one that issued the nonce, drawing a second 401 on a strict trunk (auth loop) or splitting one INVITE transaction across two members (fragile CANCEL/BYE/session-timer correlation). Both retries now reuse the failed leg's established destination/transport/connection_id (select_b2bua_retry_destination) so the whole transaction stays on the nonce-issuing member; falls back to fresh DNS resolution only when the leg has no recorded destination (regression-tested in dispatcher::tests::b2bua_retry_reuses_established_member_not_resolved_sibling)
Parallel forking Production request.fork() Used for AS→subscriber delivery
Sequential forking Implemented request.fork(strategy="sequential")
Record-Route / Loose Route Production request.record_route() Mid-dialog routing proven
CANCEL propagation Production Core Matched to transaction automatically. Proxy-forwarded CANCEL reuses the INVITE branch + sent-by on the topmost Via per RFC 3261 §9.1/§16.10, so the downstream proxy/UAS matches CANCEL→INVITE (§17.2.3) and tears the alerting branch down — fixed a defect where handle_cancel_via_session minted a fresh branch, making the CANCEL unmatchable downstream so the callee kept ringing after the caller abandoned (regression-tested in dispatcher::tests::proxy_cancel_via_*). B2BUA CANCEL path (build_cancel_from_invite) builds the correct per-leg branch; additionally fixed a defect where a 401/407 digest or RFC 4028 422 retry on an outbound INVITE appended a fresh B-leg instead of superseding the failed one, so a caller CANCEL during alerting fanned out to the dead pre-auth transaction too (→ a spurious 481, RFC 3261 §9.1). Retries now replace the leg in place (CallActorStore::replace_b_leg), so CANCEL targets only the live branch (regression-tested in b2bua_auth_retry_supersedes_failed_leg_for_single_cancel). 2xx-after-CANCEL glare (RFC 3261 §9.1): when the callee answers a B-leg INVITE in the cancel window, the B2BUA used to drop the racing 200 OK as an unknown branch (the call was already removed), leaving the callee retransmitting 200 OK then BYEing the half-open dialog. handle_b2bua_cancel now preserves still-pending B-legs as zombie_cancelled entries (32 s window); the racing 2xx is ACKed (§13.2.2.4) and immediately BYEd (§15) by handle_zombie_cancelled_2xx.
In-dialog sequential routing Production request.loose_route() End-to-end 2xx ACK follows the dialog route set (top remaining Route after self-consumption, else R-URI), not the cached INVITE next-hop — correct through non-Record-Routing hops (transparent iFC AS, I-CSCF). RFC 5923 connection reuse: when the route-set next hop still resolves to the peer the dialog was established with, in-dialog requests (B2BUA BYE/re-INVITE/UPDATE/PRACK/2xx-ACK, proxy 2xx-ACK) keep the established connection/address instead of re-resolving — so a load-balanced trunk behind one DNS name (load-balanced Record-Route) is not re-shuffled (RFC 3263 §4.2) onto a sibling member that holds no dialog state; still resolves fresh for a genuinely divergent next hop. Validated proxy/B2BUA × UDP/TCP, 0 failures/retransmits
UAC-originated pre-loaded Route Production proxy.send_request(headers={"Route": "<sip:host;lr>"}) Next-hop selection for a script-originated out-of-dialog request now follows RFC 3261 §8.1.2 / §16.4: when the headers carry a Route (a pre-loaded route set) and no explicit next_hop, the request is sent to the first Route entry's ;lr loose-route target — the R-URI stays in the Request-Line and the Route rides along. Previously the Route was carried but ignored, and the destination was always resolved from the R-URI's home domain, so a script pre-loading the serving S-CSCF (e.g. MMTel-AS reg-event SUBSCRIBE/refresh/UN-SUBSCRIBE) took an extra I-CSCF hop + Cx LIR/LIA per operation. Precedence: explicit next_hop > first Route URI > R-URI. Regression-tested end-to-end (send_request_python_kwargs_preserve_body_and_content_type scenarios 5–6) + unit-tested (resolve_send_target_*, route_next_hop_*, parse_first_route_uri_*)
Call transfer (REFER, RFC 3515) Implemented B2BUA @b2bua.on_refer
Cancel teardown hook Implemented @proxy.on_cancel / @b2bua.on_cancel Fires once when a relayed (proxy) or B2BUA INVITE is CANCELled before a final response (RFC 3261 §9) — the only script teardown signal for a cancelled-before-answer call, which neither on_reply/on_failure (proxy: the 487 is generated at the transaction layer, never reaching a reply handler) nor on_bye (b2bua: no dialog was ever established) deliver. Receives the original INVITE (proxy fn(request)) / the Call (b2bua fn(call)); fire-and-forget, does not gate the 487. Exists to release per-call resources no BYE will ever clear (Diameter Rx/N5 QoS sessions, rtpengine media anchors). The B2BUA hook fires only in Calling/Ringing, so a 2xx that wins the cancel/answer glare (independently ACK+BYE'd by handle_zombie_cancelled_2xx) never triggers it — no answered call is torn down. Engine-registration unit tests (script::engine::tests::{proxy,b2bua}_on_cancel_decorator_registers_handler) + SDK dispatch tests (sdk/tests/test_on_cancel.py).
Reply-time proxy reject Implemented reply.reject(code, reason) (in @proxy.on_reply) Fail an in-progress proxied INVITE from the reply context — the proxy-side equivalent of B2BUA call.reject(), needed because IMS P-CSCF media authorization (N5 sbi.create_session / Rx diameter.rx_aar) runs at answer time, when the negotiated SDP is available, and a failure must reject the leg (e.g. 503) rather than proceed medialess. On a provisional (1xx) — typically a reliable 183 in the VoLTE preconditions / early-media flow where the SDP answer rides the provisional — records the reject and returns True; the dispatcher then sends code reason upstream to the UAC via the server transaction (retransmission + UAC-ACK absorption) and CANCELs every pending downstream branch (reusing cancel_fork_branches, RFC 3261 §9). The straggler 487 the CANCEL draws back is absorbed via a new ProxySession.final_response_sent guard (the single-target relay path has no fork-aggregator final_forwarded to dedup it), so no second final reaches the UAC. On a final (≥200) — UAS already answered — it is a no-op returning False (a proxy cannot retract a 2xx); the script branches on the bool (log + reply.relay(), best-effort). Takes precedence over relay(). Unit-tested (script::api::reply::tests decision logic + proxy::session::tests flag) + SDK-mirrored/tested (reply.reject, sdk/tests/test_reply_reject.py). End-to-end SIPp-validated (sipp/reject_{uac,uas}.xml + reject_proxy.py): caller gets 100503 Media Authorization Failed (To-tag added, 183 suppressed), UAS gets the CANCEL on the INVITE's Via branch (RFC 3261 §9.1) and its 487 is ACKed and absorbed — both endpoints 1 Successful / 0 Failed / 0 Retrans / 0 Unexpected, siphon 0 WARN/ERROR. SIPp validation also surfaced + fixed a pre-existing loop: a non-compliant ACK (fresh branch instead of the INVITE's, §17.1.1.3) carrying the 503's To-tag + an R-URI pointing at the proxy was matched by by_dialog_key and relayed to the proxy's own address in handle_ack_via_session, stacking a Via per hop until the datagram exceeded the 8192-byte UDP buffer (truncated → parse-error drop). Two guards: (1) reject now drops the dead by_dialog_key entry (ProxySessionStore::remove_dialog_key — a rejected INVITE forms no dialog), and (2) handle_ack_via_session reuses the existing is_own_address loop check to silently drop an ACK whose resolved next-hop is one of our own listeners (RFC 3261 §16.3).
Session timers (RFC 4028) Implemented session_timer: UAC/UAS/B2BUA refresher modes
PRACK (RFC 3262) Implemented Core Reliable provisional responses; B2BUA terminates 100rel per-leg — auto-PRACKs a reliable-provisional B-leg and strips Require:100rel/RSeq toward a non-100rel A-leg (framework-auto, preset-independent)

Transports

Feature Readiness Config Notes
TCP Production listen.tcp AS-facing; RFC 3261 §18.3 stream framing with Content-Length extraction; outbound distributor falls back to the ConnectionPool when an OutboundMessage arrives without a matching inbound connection (covers UAC fire-and-forget paths like in-dialog NOTIFY from subscribe_state.notify() whose Route header points at a destination with no live inbound socket — previously the message was built but silently dropped at the connection-map lookup). Wedge-hardened (all stream transports): the per-listener outbound distributor routes with a non-blocking try_send instead of send().await. A single non-reading peer (toll-fraud scanner that never ACKs its 401s, or a stream peer whose far end stalls) fills its bounded per-connection channel; an awaiting send parked there while holding the connection_map shard read guard, stalling outbound for every connection (head-of-line) and blocking the accept loop's insert on the same shard — accept stops, the backlog fills, the engine wedges (no logs) until restart. try_send keeps the guard only for the synchronous send and sheds a backed-up peer. Reproduced + regression-guarded black-box on a real container at --cpus 0.5 by scripts/wedge_test.sh (run-tests.sh --wedge) — probe times out pre-fix, answered post-fix. Outbound ConnectionPool establishment hardened: the connect is bounded by a fail-fast timeout (TCP_CONNECT_TIMEOUT, 5 s) so a doomed ESP-over-TCP send to a UE whose IPsec SA was just torn down (no SYN-ACK, no RST) can no longer block the PyExecutor worker indefinitely and trip the script-executor watchdog → process abort; and concurrent first-sends to the same destination coalesce onto one connection under a per-destination lock, so the fixed protected source port (pcscf_port_c) cannot hit EADDRNOTAVAIL/EADDRINUSE on a second bind/connect of the same 4-tuple. Regression-guarded by connect_fails_fast_to_blackhole and concurrent_sends_coalesce_onto_one_connection in transport::pool
TLS Production listen.tls Subscriber-facing, TLS 1.3 validated; RFC 3261 §18.3 stream framing; outbound distributor wedge-hardened with non-blocking try_send (see TCP)
TLS 1.3 Production tls.method: TLSv1_3
TLS 1.2 Implemented tls.method: TLSv1_2
mTLS (client cert verification) Implemented tls.verify_client: true, tls.client_ca Client certificate required and verified against the tls.client_ca PEM bundle; applies to listen.tls and listen.wss (shared TLS block). Fails closed at startup if verify_client is set without client_ca (previously verify_client was silently ignored on the SIP listener — read only by the X1 LI interface). TLS handshake bounded by a 10 s timeout (half-open-handshake / slowloris defense).
UDP Production listen.udp
WebSocket (WS) Implemented listen.ws RFC 7118, browser/WebRTC clients; outbound distributor wedge-hardened with non-blocking try_send (see TCP). MT routing (INVITE → WS-registered UE) works via RFC 5626 §5.3 connection reuse: every binding captures its inbound flow (no flow_token= needed), registrar.lookup() returns it as contact.flow, and request.fork(contacts) / request.relay(flow=) / call.fork(contacts) / call.dial(flow=) route over the captured connection on both proxy and B2BUA. Connections register in a unified cross-transport StreamConnections registry (also backs Flow.is_alive); send_to_target has a WS/WSS arm that reuses the connection and drops (no caller-echo) on miss.
Secure WebSocket (WSS) Implemented listen.wss Outbound distributor wedge-hardened with non-blocking try_send (see TCP). MT routing via connection reuse — same flow-based path as WS (see the WS row).
SCTP Implemented listen.sctp RFC 4168, IMS inter-node; outbound distributor wedge-hardened with non-blocking try_send (see TCP)
Per-socket advertised address Production listen.tls[].advertise
Global advertised address Implemented advertised_address: Fallback for 0.0.0.0 binds
DSCP/ToS marking Implemented listen.dscp RFC 4594 signaling QoS; default CS3 (24); per-listener override

Registrar

Feature Readiness Config Notes
Redis backend Production registrar.backend: redis Persistent across restarts
Memory backend Implemented registrar.backend: memory Ephemeral
PostgreSQL backend Implemented registrar.backend: postgres
Python custom backend Implemented registrar.backend: python
Expires control (default/min/max) Production registrar.{default,min,max}_expires
Max contacts per AoR Production registrar.max_contacts
Bind AoR to authenticated user Implemented registrar.enforce_auth_aor_match Rejects (403) a REGISTER whose AoR (To-URI user) ≠ the authenticated digest user — anti account-takeover / forced-deregister. Checked before the force-clear so a spoofed AoR can't first wipe the victim's bindings. Default off (IMS deployments authorize via the implicit registration set, where the public identity ≠ private auth identity).
Redis TTL slack Production registrar.redis.ttl_slack_secs Race condition buffer
GRUU (RFC 5627) Implemented
Service-Route (RFC 3608) Production Via registrar.set_service_routes() / service_route()
Registration state change hooks Production @registrar.on_change Callbacks on insert/delete/expire
Liveness — flow-failure dereg (RFC 5626 §4.2.2) Implemented registrar.liveness.enabled TCP/TLS/WS/WSS connection close (peer FIN/RST, read error, idle timeout, or CRLF-keepalive failure) deregisters the bindings that arrived on that connection. Transport notifies the registrar over a close channel → Registrar::unregister_flow(connection_id), which uses a ConnectionId → AoR reverse index (connection_index) to drop only the affected bindings (O(bindings-for-that-connection), scanner-churn-safe) and emit Deregistered. Default off.
Liveness — IPsec idle dereg (UDP + TCP/TLS/WS) Implemented registrar.liveness.{enabled,keepalive_interval_secs,idle_multiplier,probe_timeout_ms} Detects a dead UE on the production Gm without a SIP de-REGISTER, on any SIP transport — the XFRM SA use-time is the liveness signal, so a TCP+IPsec registration whose UE silently dies (radio loss, no FIN/RST) is reaped on the same ~idle_multiplier × keepalive_interval window as a UDP UE, rather than waiting for the CRLF-keepalive timeout (minutes). The 30 s sweep polls kernel XFRM SA inbound use-time (one XFRM_MSG_GETSA netlink dump — no per-packet hot-path cost; the UE's RFC 6223 keepalive keeps the SA warm); eligibility is by SA match (UE IP), which naturally excludes non-IPsec bindings. A suspect binding is probed with one OPTIONS over its actual transport (stream probes ride the captured inbound connection); no answer → deregister. SA teardown (sweep_expired) also drops the matching binding. EPC-independent backstop for SMF crash / PCRF-no-Rx-ASR / silent radio loss. Idle-probe path needs kernel XFRM + lab validation. Default off.
Liveness — network dereg cascade Implemented registrar.liveness.dereg_mode: network_dereg\|local_only Removing a binding emits Deregistered@registrar.on_change (the authoritative/S-CSCF path; the script sends the terminated reg-event NOTIFY). For a P-CSCF cache binding (carries a flow_token) under network_dereg, also synthesizes a de-REGISTER (Expires: 0) on the UE's behalf routed via the stored Service-Route so the registrar of record clears it too. local_only skips the upstream REGISTER. Network-dereg routing needs a split P-CSCF/S-CSCF lab to validate end-to-end.
Outbound registration (registrant) Production registrant: UAC REGISTER to upstream trunks
IMS UE registration (soft-UE, AKA + IPsec sec-agree) Implemented registration.add(auth="aka", k=, opc=, ipsec=True, ue_port_c=, ue_port_s=) or YAML registrant.entries[].{auth: aka, aka:, ipsec:} siphon registers INTO an IMS core as a handset: IMS-AKAv1-MD5 (RFC 3310 — RES is the binary digest password) over IPsec sec-agree (3GPP TS 33.203). Milenage f1*/f5*/AUTS re-sync (TS 35.208 Test Set 1 vectors). Initial REGISTER offers Security-Client (UE SPIs/ports + Require: sec-agree); the 401 records Security-Server; the protected re-REGISTER echoes Security-Verify and egresses from the UE protected client port over the four UE-side SAs (create_ue_sa_pair — same netlink + CK/IK derivation as the P-CSCF, only the four XFRM policy directions mirror via SaRole); the protected 200 OK tightens the SA hard-lifetime to the granted Expires + Timer-F grace. Service-Route / P-Associated-URI captured for MO routing; AUTS re-sync is a fallback (a fresh stateless UE never emits it). Message construction unit-tested against 3GPP/RFC vectors; the kernel SA install is root-gated. NOT yet validated end-to-end against a live P-CSCF. Example: examples/ims_ue_b2bua.{py,yaml}.
IMS UE B2BUA bridge (plain SIP ↔ IMS) Implemented examples/ims_ue_b2bua.py, call.dial(flow=, route=), registration.flow()/service_route() Bidirectional B2BUA over the soft-UE registration. MT (IMS→tester): the protected-port A-leg bridges to a plain-SIP tester; A-leg responses egress back over the SA via inbound-flow pinning. MO (tester→IMS): dials the B-leg over the UE→P-CSCF SA flow (registration.flow(impu, ue_ip)call.dial(flow=), sourced from the UE protected client port), carrying the captured Service-Route (registration.service_route(impu)call.dial(route=)) and asserting the IMPU via P-Preferred-Identity (intra-trust preset preserves P-*). Direction detected by call.source_ip == pcscf. SDK-tested both directions (sdk/tests/test_ims_ue_b2bua.py); needs live-core + root validation.
Proxy-side binding cache Implemented registrar.save_proxy(request, reply) P-CSCF caches what S-CSCF granted; reads Expires from reply (not request), bypasses local max_expires cap, +32 s Timer F grace, no auto-200 OK (proxy relays upstream's response)
Path-token MT routing (RFC 3327 / TS 24.229 §5.2.7.2) Implemented request.add_pcscf_path(token), registrar.save(flow_token=)/save_proxy(flow_token=), registrar.lookup_by_token(token), request.relay(flow=binding.flow), ipsec.path_host P-CSCF mints opaque token, embeds in Path userpart; binding stores token + captured inbound flow (source addr, listener local addr, accepted-connection id); MT routing bypasses DNS resolution and egresses from the same listener that received the REGISTER. UDP flow survives restart; TCP/TLS/WS/WSS bound to accepting instance lifetime. Via on flow-relay derives from flow.local_addr so IPSec port pairs are preserved (TS 33.203 §7.4).
AS-side contact capture (TS 24.229 §5.4.2.1.2) Implemented registrar.save_as_contact(aor, reply), Contact.params, Contact.kind S-CSCF script caches the AS's Contact: URI and RFC 3840 feature tags (+g.3gpp.smsip, +g.3gpp.icsi-ref, …) from the 3PR 200 OK; tags surface in registrar.reginfo_xml(...) as <unknown-param> children per RFC 3680 §5.3.2 so reg-event NOTIFY watchers see the iFC-matched capability set. AS contacts are excluded from registrar.lookup() (routing-side never picks them as MT targets) and cascade-clear when the last UE binding deregs/expires.
Contact-header parameter passthrough (RFC 3840) Implemented Contact.params Every non-typed Contact-header parameter (anything outside tag/q/expires/+sip.instance/reg-id) round-trips through save → backend persistence → lookup → reg-event NOTIFY. Lowercased at parse time per RFC 3261 §19.1; values preserved verbatim.

Authentication

Feature Readiness Config Notes
Digest auth — 401 (UAS) Production auth.require_digest() REGISTER challenges
Digest auth — 407 (proxy) Production auth.require_proxy_digest() INVITE challenges
HTTP backend (HA1 lookup) Production auth.backend: http REST credential lookup; optional per-username TTL cache (auth.http.cache_ttl_secs) flattens registration storms so repeat REGISTERs skip the blocking fetch
Static users backend Implemented auth.backend: static Inline config credentials
Diameter Cx backend (HSS) Production auth.backend: diameter_cx 3GPP TS 29.228
AKA / AKAv1-MD5 (HSS-backed) Production auth.require_ims_digest() 3GPP TS 33.203 via Cx MAR/MAA
AKA / AKAv1-MD5 (local Milenage) Implemented auth.aka_credentials 3GPP TS 35.206 — local key derivation without HSS
SHA-256 digest (RFC 7616) Implemented
Anti-spoofing (from=auth check) Production Script logic auth_user == from_uri.user (caller-ID/From); the registrar-side AoR/To equivalent is registrar.enforce_auth_aor_match
Digest nonce replay protection (RFC 7616 §3.3) Implemented auth.nonce_secret, auth.nonce_ttl_secs Nonces are timestamp-bound ({unix_secs:016x}.{tag}) and rejected once older than the TTL (default 3600 s), bounding captured-Authorization replay from "forever" to the window. Cross-instance safe with no shared state (correct behind round-robin DNS where a re-REGISTER may land on a different node). Optional shared nonce_secret adds HMAC-SHA256 integrity so a node rejects nonces the cluster never issued — must be identical on every instance behind the domain. Applies to the static + HTTP backends; the IMS/AKA paths use single-use HSS vectors.

STIR/SHAKEN (Caller-ID Attestation)

Feature Readiness Config Notes
Sign — Authentication Service Implemented stir.sign(), stir: signing block ES256 PASSporT + RFC 8224 Identity header (RFC 8225, ATIS-1000074)
Verify — Verification Service Implemented stir.verify(), stir: verification block x5u fetch + full cert-chain validation to STI-CA anchors, sets verstat
Attestation levels A/B/C Implemented stir.sign(attestation=…) ATIS-1000074 §5.2.3; default via default_attestation
Diverted-call PASSporT (div) Implemented stir.sign_div() RFC 8946 — forwarded/retargeted calls
Cert chain + freshness Implemented stir.verification.freshness_secs, trust_anchors EC P-256 chain to STI-CA root; PASSporT iat window
Permissive rollout mode Implemented stir.verification.permissive x5u/infra failures → No-TN-Validation instead of …-Failed
x5u certificate cache Implemented stir.verification.cache_ttl_secs In-memory; honours Cache-Control: max-age
verstat stamping Implemented stir.apply_verstat() ATIS-1000074 §5.3.1 — P-Asserted-Identity / From
RCD (Rich Call Data) Planned Caller name/logo PASSporT — follow-up
OCSP/CRL revocation, RSA STI-CA Planned EC P-256 chains only in v1

Security

Feature Readiness Config Notes
Rate limiting (per source IP) Production security.rate_limit PIKE-style fixed-window per-source-IP limiter. More than max_requests within window_secs → ban the source for ban_duration_secs (default 3600); every further request is dropped silently (no response — no fingerprinting). Enforced in the dispatcher on every inbound request before transaction/dialog/script processing, via the process-global crate::security::SecurityFilter (opt-in, installed only when configured). trusted_cidrs are exempt. 60s prune bounds the maps under scanner churn. Metric: siphon_rate_limited_total. Unit- + integration-tested (security::tests, tests/integration/security_tests.rs)
Scanner UA blocking Production security.scanner_block Drops any inbound request whose User-Agent matches a configured signature (case-insensitive substring — sipvicious, friendly-scanner, VaxSip, sipcli, …). Silent drop in the dispatcher (no response), trusted_cidrs exempt, same SecurityFilter path as rate limiting. When failed_auth_ban is also configured, a match over a connection-oriented transport (TCP/TLS/WS/WSS/SCTP — source validated by the handshake) escalates to a strong-weight auto-ban so the scanner's other probes are dropped at the ACL too; a match over UDP is only dropped (spoofable source → no reflected ban). Metric: siphon_scanner_blocked_total. Unit- + integration-tested
Trusted CIDRs (bypass rate limit + scanner block) Production security.trusted_cidrs Sources matching any CIDR bypass both the rate limiter and the scanner-UA block in SecurityFilter (own infra: AS/trunks/monitoring). Also exempted by failed_auth_ban's auto-ban store. Invalid CIDRs are ignored.
Failed auth ban (auto-ban) Production security.failed_auth_ban Per-source-IP auto-ban for toll-fraud scanners, fed by weighted failure signals so high-confidence abuse bans faster than a bare probe (strong_signal_weight, default 3, vs weight 1). Weight-1 (low-confidence): an auth challenge (401/407) not followed by a success; a non-ACK INVITE server-transaction timeout (RFC 3261 §17.2.1 Timer H); a failed/timed-out TLS/WSS/WS handshake. Strong-weight (high-confidence): present-but-invalid digest credentials or a forged/stale/replayed nonce (kept weight-1 over UDP, where the source is spoofable → reflected-ban-safe); non-SIP/unparseable bytes on a TCP/TLS stream (HTTP probe, TLS record on the plaintext port, binary garbage, over-long header block — never an incomplete-but-plausible frame, empty connection, or CRLF keepalive); a scanner_block User-Agent hit over a connection-oriented transport (UDP scanner UAs are dropped, not banned). A successful auth resets the source's count, so a legit challenge→succeed (or stale-nonce retry) client never accumulates. threshold weighted failures within window_secs → ban for ban_duration_secs (default 10 / 600 / 3600). trusted_cidrs are exempt (own infra: BGCF/trunks/monitoring/health-check LBs) — client-transaction (relay-target) timeouts are also deliberately not counted, so a non-answering trunk is never banned. Enforced at accept/recv on every transport via TransportAcl::is_allowed (dropped before any SIP parsing). Process-global store (crate::security::AutoBanStore, opt-in), lazy ban-expiry + 60s prune. Metrics: siphon_banned_ips, siphon_auth_failures_total, siphon_credential_failures_total, siphon_handshake_failures_total, siphon_malformed_messages_total. Unit-tested (security::tests, transport::tcp::tests)
APIBan integration Production security.apiban Community IP blocklist polling
IP ACLs (allow/deny CIDR lists) Implemented Transport-level ACL
Preloaded Route rejection Production Script logic Anti-abuse for Route header

NAT Traversal

Feature Readiness Config Notes
Force rport (RFC 3581) Production nat.force_rport: true
Fix Contact (observed source) Production nat.fix_contact: true
Fix REGISTER Contact Production nat.fix_register: true
Fix NATed Contact (script) Production request.fix_nated_contact()
NAT keepalive (OPTIONS ping) Implemented nat.keepalive Configurable interval + failure threshold
CRLF keepalive (RFC 5626 §4.4.1) Implemented nat.crlf_keepalive TCP/TLS/pool connection keep-alive; outbound probe + inbound peer-ping/pong responder
Stale contact eviction on restart Production Core Evicts connection-oriented contacts + on_change notify
Outbound flow tokens (RFC 5626) Implemented Via/Route flow tokens

Media

Feature Readiness Config Notes
RTPEngine integration (NG protocol) Production media.rtpengine Single or multi-instance
RTPEngine load balancing Implemented media.rtpengine.instances[] Weighted distribution
Built-in profile: SRTP↔RTP Implemented srtp_to_rtp SRTP UE ↔ RTP core
Built-in profile: WS↔RTP Implemented ws_to_rtp WebSocket UE ↔ RTP core
Built-in profile: WSS↔RTP Implemented wss_to_rtp DTLS-SRTP/AVPF + ICE ↔ RTP
Built-in profile: RTP passthrough Implemented rtp_passthrough IMS-internal
Custom media profiles Implemented media.profiles User-defined NG flags
SDP manipulation (sdp namespace) Implemented None Parse/modify/apply SDP from Python scripts
SDP attribute get/set/remove Implemented None Session and media-level a= attributes
SDP codec filtering Implemented None filter_codecs() / remove_codecs()
SDP media section removal Implemented None remove_media("video")

Gateway Routing & Load Balancing

Feature Readiness Config Notes
Destination groups Production gateway.groups
Round-robin algorithm Production algorithm: round_robin
Weighted algorithm Implemented algorithm: weighted
Hash-based algorithm Implemented algorithm: hash
SIP OPTIONS health probing Production gateway.groups[].probe Configurable interval + failure threshold
Priority-based failover tiers Implemented destinations[].priority
Dynamic group management Implemented Python gateway.add_group() / gateway.remove_group()
Destination up/down marking Implemented Python gateway.mark_up() / gateway.mark_down()

Call Detail Records

Feature Readiness Config Notes
CDR generation Implemented cdr:
File backend (JSON-lines) Implemented cdr.backend: file With rotation
Syslog backend Implemented cdr.backend: syslog UDP syslog
HTTP webhook backend Implemented cdr.backend: http POST with optional auth header
REGISTER event inclusion Implemented cdr.include_register Off by default
Script-injected extra fields Implemented

SIP Tracing

Feature Readiness Config Notes
HEP v3 over UDP Production tracing.hep Homer integration
HEP over TCP Implemented tracing.hep.transport: tcp
HEP over TLS Implemented tracing.hep.transport: tls With CA cert + SNI
Custom agent ID Production tracing.hep.agent_id
Error log suppression Production tracing.hep.error_log_interval Configurable interval

Metrics & Monitoring

Feature Readiness Config Notes
Prometheus endpoint Production metrics.prometheus
Request/response counters Production siphon_requests_total / siphon_responses_total
Active registrations gauge Production siphon_registrations_active
Active transactions gauge Production siphon_transactions_active
Active dialogs gauge Production siphon_dialogs_active
Active connections (by transport) Production siphon_connections_active
Request duration histogram Production siphon_request_duration_seconds
Script execution counters Production siphon_script_executions_total
Uptime gauge Production siphon_uptime_seconds
Admin API — health Implemented GET /admin/health Liveness/readiness probe
Admin API — stats Implemented GET /admin/stats Aggregate counters
Admin API — registrations Implemented GET/DELETE /admin/registrations List, detail, force-unregister

Logging

Feature Readiness Config Notes
JSON structured logging Production log.format: json
Pretty (human-readable) logging Implemented log.format: pretty
File logging Production log.file With logrotate support
Log level control Production log.level debug/info/warn/error

Python Scripting

Feature Readiness Config Notes
Script loading Production script.path
Hot-reload via inotify Production script.reload: auto
Hot-reload via SIGHUP Implemented script.reload: sighup
Proxy handlers (on_request/on_reply/on_failure) Production @proxy.* on_request + on_reply proven. Per-relay request.relay(on_reply=…) / request.relay(on_failure=…) callbacks now fire correctly on the free-threaded build: the dispatcher response path lifted the stored on_reply/on_failure Py<…> callbacks out of the session read-guard with a bare Clone on a Python-executor worker that was not inside a Python::attach scope. Under free-threaded CPython (3.14t, pyo3 0.28) Py::clone panics ("Cannot clone pointer into Python heap without the thread being attached") unless the thread is attached, unwinding the worker mid-relay — which truncated the in-flight relayed request and failed every call that armed a per-relay callback (blocked reply-driven MMTel behaviours: CFNR / busy-on-200 OK marking). Fixed by ProxySession::clone_relay_callbacks, which clones through a Python token (clone_ref) under Python::attach, matching the request path's discipline (script::handle::call_handler). Regression-tested in proxy::session::tests::clone_relay_callbacks_from_unattached_worker_thread (clones the callbacks from a freshly spawned, never-attached OS thread).
B2BUA handlers Production @b2bua.* on_invite, on_early_media, on_answer, on_failure, on_bye, on_refer
Registrar hooks Production @registrar.on_change
Auth API Production auth.require_digest() etc.
Gateway API Production gateway.select() etc.
Cache API Production cache.fetch() Redis-backed
Cache list / TTL / existence ops Implemented cache.list_push/list_pop_all/expire/exists Redis-backed FIFO queue ops (atomic LRANGE+DEL drain), per-key TTL, presence check; degrades silently when Redis is unreachable
Presence API Production presence.* Used for reg-event SUBSCRIBE/NOTIFY
Outbound SUBSCRIBE (RFC 6665 watcher) Implemented proxy.subscribe_state.send/find/refresh Originate SUBSCRIBE, capture dialog state from 200 OK, correlate inbound NOTIFY by tags
Reginfo XML parser (RFC 3680) Implemented presence.parse_reginfo(xml) Watcher-side parser for application/reginfo+xml NOTIFY bodies
Lawful intercept API Implemented li.*
Logging API Production log.*
Async handler support Production Auto-detected by runtime
Custom metrics API Production metrics.counter/gauge/histogram Script-defined Prometheus metrics
Timer routes Implemented @timer.every(), timer.set()/cancel() Periodic callbacks via Tokio; one-shot cancellable timers keyed by string
Mock SDK for testing Implemented siphon-sip (imports as siphon_sdk) Test scripts without Rust binary
Extension API (host namespaces, tasks, custom handler kinds) Implemented extensions:, register_namespace/register_task, _siphon_registry.register("custom.kind", …) Open extension surface for custom transports / sinks; ScriptHandle::handlers_for + call_handler dispatch into script handlers from host extensions
Elastic handler pool (grow + bounded queue + watchdog) Production script.sync_pool_size / sync_pool_max, executor_queue_capacity, handler_stall_abort_secs Pool grows core→max under blocking load and never reaps (no wedge, no heap leak); bounded queue load-sheds at the cap; deadlock-aware liveness watchdog aborts (→ supervisor restart) on zero forward progress while work is pending, at any pool fill (catches low-concurrency deadlocks). Blocking Rust-API calls release the interpreter (py.detach) to avoid the free-threaded GC stop-the-world deadlock. Regression-guarded by pool_grows_under_blocking_load, detached_blocking_does_not_stall_gc, and run-tests.sh --http-auth. Metrics: siphon_pyexec_*. See handler-execution-model.md

Dialog Management

Feature Readiness Config Notes
Memory backend Production Default In-process, ephemeral
Redis backend Implemented dialog.backend: redis Persistent across restarts
PostgreSQL backend Implemented dialog.backend: postgres

Named Cache

Feature Readiness Config Notes
Redis-backed cache Production cache[].url
Local LRU tier Implemented cache[].local_ttl_secs Two-tier: local + Redis

Presence

Feature Readiness Config Notes
SUBSCRIBE/NOTIFY (RFC 6665) Production Python presence API reg-event package; presence.terminate() + auto-GC on terminated NOTIFY drops dialog state per RFC 6665 §4.4.1
PIDF (RFC 3863) Implemented
Resource List Server (RFC 4662) Implemented
Watcher Info (RFC 3857/3858) Implemented

Server Identity

Feature Readiness Config Notes
Custom Server header Production server.server_header
Custom User-Agent header Production server.user_agent_header

Transaction Timers

Feature Readiness Config Notes
Non-INVITE timeout Production transaction.timeout_secs
INVITE timeout Production transaction.invite_timeout_secs

DNS Resolution

Feature Readiness Config Notes
SRV lookup (RFC 3263) Implemented Core With A/AAAA fallback; weighted-random RFC 2782 selection per call
A/AAAA load distribution (RFC 3263 §4.2) Implemented Core Fisher-Yates shuffle on every A-only resolution so callers picking .next() distribute uniformly across equal-cost records
NAPTR support Implemented Core
ENUM (RFC 6116) Implemented Core

3GPP / IMS / Telco

Feature Readiness Config Notes
Diameter Cx (HSS auth) Production auth.backend: diameter_cx MAR/SAA, SAR/SAA, UAR/UAA, LIR/LIA
Diameter Sh (HSS user data) Production diameter sh_udr for repository data; inbound PNR (profile push) handled via @diameter.on_request (req.command_name == "PNR")
Diameter Ro (online charging) Implemented diameter CCR/CCA
Diameter Rf (offline charging) Implemented diameter, rf: ACR/ACA wired through diameter.rf_acr_start/interim/stop/event (TS 32.299 §6.2.2) — kwargs-style Python API, mandatory AVPs (Service-Context-Id, Event-Timestamp, User-Name, Termination-Cause, Acct-Interim-Interval), full IMS-Information sub-AVPs (User-Session-Id, Time-Stamps, Inter-Operator-Identifier, Application-Server, IMS-Visited-Network-Identifier), TS 32.260 IMS Service-Context-Id default. SMS-Information envelope (TS 32.299 §7.2.79) — passing any SMS-specific kwarg (originator_address, recipient_address, sm_message_type, sms_node, sm_user_data_header, reply_path_requested, sm_service_type, sms_result, SCCP/Client/MTC-IWF Address fields, sm_discharge_time, data_coding_scheme, …) switches the wire to Service-Information → SMS-Information so CDR collectors render calling/called party + message type on the SMS tab; can coexist with IMS-Information for hybrid records. rf: config block + RfChargingService runtime emits ACR-EVENT automatically on registrar state change. CDR auto-stamps rf_session_id / rf_result_code from auto-emitted records. B2BUA + proxy ACR-START/INTERIM/STOP auto-emit on call lifecycle is the next layer on the same infrastructure.
Diameter Rx (policy/QoS) Production diameter AAR/AAA, STR/STA; inbound RAR/ASR handled via @diameter.on_request (req.command_name). diameter.rx_aar(media_components=[…]) takes a list of TS 29.214 §5.3.7 MediaComponent dicts with per-flow IPFilterRules + Flow-Usage (RTCP marker) — pair with qos.media_flows_from_sdp(offer, answer, direction) to derive the full 5-tuple from an SDP offer/answer rather than emitting a wildcard permit in 17 from <UE> to any that any non-permissive PCEF would either drop or open globally.
Diameter S6c (SMS-over-Diameter, SMSC↔HSS) Implemented diameter s6c_srr to discover served-node, s6c_rsr for delivery status; inbound ALR (HSS reachability alert) handled via @diameter.on_request (TS 29.336). MSISDN / SC-Address / SGSN-Number / MME-Number-for-MT-SMS encoded as ISDN-AddressString (TS 29.002 §17.7.8 — ToN/NPI 0x91 + TBCD digits); inbound parser is lenient on missing ToN/NPI prefix for non-conformant peers.
Diameter SGd (SMS-over-NAS, SMSC↔MME) Implemented diameter sgd_tfr to deliver SMS-DELIVER TPDU to UE; inbound OFR (MO-SMS) handled via @diameter.on_request (TS 29.338). SC-Address on the wire uses ISDN-AddressString (TS 29.002 §17.7.8), matching S6c.
Diameter S6a (MME↔HSS, LTE attach/auth) Implemented diameter.s6a_air/s6a_ulr/s6a_purge_ue (client); @diameter.on_request + req.answer() (server) TS 29.272 — client: AIR/AIA (E-UTRAN vectors RAND/XRES/AUTN/KASME, SQN resync), ULR/ULA, PUR/PUA. Server (HSS role): siphon transports inbound AIR/ULR/PUR to @diameter.on_request; the script builds the answer with req.answer(code) + grouped-AVP construction. siphon does NOT implement S6a semantics or Milenage — the script owns subscriber data + auth-vector crypto (see examples/hss_s6a.py). Relayable by a server-mode script. Dictionary AVPs 1400–1450/1635 + command codes 316–324.
Diameter generic answer + grouped AVPs (server) Implemented req.answer(result_code), DiameterRequest/DiameterAnswer.{get,set,insert}_avp Application-agnostic inbound serving: build a local answer envelope and construct/read arbitrarily nested Grouped AVPs from Python (list of (code, value[, vendor]) child tuples; values may nest). Lets a script serve any Diameter application (HSS/PCRF/OCS) on the inbound listener — siphon transports, Python decides.
Diameter serve-on-outbound (dial-out + serve) Implemented diameter.connect_to A server NF that initiates the connection (e.g. an HSS dialling an upstream) but answers the requests relayed back over it. siphon sends the CER, then routes inbound requests to @diameter.on_request exactly like the listener path — transport direction is independent of request direction (RFC 6733 §2.1). Works without diameter.listen. TCP + SCTP.
Diameter generic API (spec-name addressing) Implemented diameter.send_request("Send-Routing-Info-for-SM-Request", application="S6c", **avps) (originate); @diameter.on_request + req.command_name (serve) Outbound origination by spec name (AVPs encoded by dictionary type, snake_case ↔ kebab-case kwargs, 3-letter acronym aliases SRR/ALR/TFR/…). Inbound serving is the single unified @on_request hook — the old per-command @on_command was removed.
Diameter peer management Production diameter.peers Failover + round-robin across HSS/PCRF peers
Diameter server mode Implemented diameter.listen, diameter.clients, diameter.servers, @diameter.on_inbound_cer, @diameter.on_request, @diameter.on_reply, @diameter.on_request_completed Accepts inbound Diameter (TCP + SCTP), runs CER/CEA + the DWR/DWA watchdog, and dispatches each inbound request to Python — siphon transports, the script decides (answer locally or relay). Two Rust-only admission gates (source-IP CIDR ACL + Origin-Host validation, both before any Python), lossless AVP tree (DiameterRequest/DiameterAnswer get/set/remove/insert/iter), req.forward_to(peer) relay with Route-Record loop detection (3005) + per-call timeout, @diameter.on_reply for central answer-AVP rewrite (topology hiding, Origin/Result-Code mapping), diameter.peer_pool(target) (round-robin / weighted / sticky over state-as-truth liveness), diameter.config snapshot (no YAML hot-reload), diameter.event_sink (file/none; clickhouse/kafka feature-gated). None→3002. Inbound and outbound TCP+SCTP (peer::connect_with_transport). The ClickHouse/Kafka sinks are follow-ups. See examples/diameter_server.{py,yaml}.
AKA authentication (Milenage, local) Implemented auth.aka_credentials 3GPP TS 35.206 — local key derivation without HSS
AKA authentication (HSS-backed) Production auth.require_ims_digest() 3GPP TS 33.203 via Cx MAR/MAA
IPsec SA management (P-CSCF) Implemented ipsec Shared protected client/server ports; SAs installed via direct XFRM netlink (Phase 3) with ip xfrm shell-out as fallback backend
IPsec sec-agree primitives (script-driven) Implemented siphon.ipsec, request.parse_security_client(), reply.take_av() 3GPP TS 33.203 §6 + RFC 3329; HMAC-SHA-1-96 / HMAC-MD5-96 / HMAC-SHA-256-128 with NULL or AES-CBC-128; Annex H key derivation; registration-tied lifetimes; IPv6; multi-instance SPI partitioning; multi-protocol XFRM selectors (TS 33.203 §7.2 — one SPI pair covers both ESP-over-UDP and ESP-over-TCP, required for iOS UEs mixing REGISTER/TCP with MO MESSAGE/UDP)
IPsec SA hard-lifetime repin on grant Implemented pending.activate(hard_lifetime_secs=…) XFRM_MSG_UPDSA on all four SAs; tightens kernel lifetime from the placeholder (UE's Expires ask, often 600000 s) to the registrar's grant on the 200 OK to auth REGISTER (3GPP TS 33.203 §7.4); kernel preserves add_time so deadline = original install + new value
IPsec SA hard-lifetime repin on REGISTER refresh Implemented automatic in registrar.save_proxy/save 3GPP TS 33.203 §7.4: an IPsec-protected REGISTER refresh extends the bound SA pair's hard lifetime to the granted binding lifetime (granted Expires + 32 s Timer-F grace). IR.92 refreshes carry no AKA challenge (200-without-401 → no PendingSAactivate never fires), so this registrar hook is the only path that moves the SA forward on a refresh; without it an actively-refreshing UE's SA aged out at last-AKA + grace and was reaped + network-de-REGISTERed (live VoLTE/VoNR outage). The re-pin adds elapsed-since-install to the kernel hard_add_expires_seconds because XFRM_MSG_UPDSA preserves add_time (IpsecManager::update_sa_pair_lifetime keyed off SecurityAssociationPair::created_at), so the kernel deadline actually advances rather than staying pinned to the original install. Unit-tested (ipsec::tests elapsed-math + anchor-stability via mock kernel); needs root + live-core validation.
IPsec stale-pair cleanup on re-REGISTER Implemented pending.activate() (automatic) UE picks a fresh random port_uc on every REGISTER (TS 24.229 §5.1.1.2); without this, the manager's (ue_addr, port_uc)-keyed bookkeeping accumulated one entry per refresh and the prior pair's four XFRM policies leaked into the kernel forever. After enough cycles a new port_uc collided with a leaked selector and policy install hit EEXIST, breaking the registration. Activate now fire-and-forgets cleanup_other_pairs_for_ue to tear down every prior pair for the same UE address; the new pair (different port_uc by construction) installs cleanly.
Initial Filter Criteria (iFC) Production isc XML trigger-point matching + per-user profile storage from Cx SAR
IMS P-CSCF role Production Example examples/ims_pcscf.{py,yaml}
IMS I-CSCF role Production Example examples/ims_icscf.{py,yaml}
IMS S-CSCF role Production Example examples/ims_scscf.{py,yaml}
5G SBI — Npcf (policy) Implemented sbi N5 app-session for VoNR QoS. sbi.create_session(media_components=[…]) builds the spec-correct TS 29.514 AppSessionContext: request data nested under ascReqData (a flat body left the PCF reading ueIpv4 as null → session created but never bound), medComponents/medSubComps as maps keyed by medCompN/fNum (not arrays) with the exact wire names medCompN/medType/fStatus/codecs/fDescs/flowUsage and hyphenated ENABLED-UPLINK/ENABLED-DOWNLINK so PCF gating works on real UPFs; same dict shape as diameter.rx_aar. The created appSessionId is taken from the 201 Location header (it is not a body field); modify is an application/merge-patch+json PATCH. Per-call pcf_uri= addresses a session at a discovered PCF instead of the static npcf_url; create returns app_session_uri and update/delete accept it for replica-independent teardown. Wire format corrected after a live open5gs trace exposed the missing envelope; message-level + SDK tested (axum body-capture asserts the ascReqData envelope and medComponents map on the wire), live re-validation against the open5gs PCF pending. Inbound PCF event notifications (@sbi.on_event, TS 29.514 EventsNotification) are now passed to the script verbatim as a dict — previously they were projected through a lossy typed struct that dropped the required evSubsUri correlation key and 422'd (silently lost) any notification carrying flows (the spec shape is {medCompN, fNums}, not {flowId}).
5G SBI — Nbsf (PCF discovery) Implemented sbi.discover_pcf_binding, sbi.bsf_url Nbsf_Management pcfBindings lookup keyed on the UE IP (TS 29.521) — the reliable 5G-vs-4G discriminator a P-CSCF uses to pick N5 vs Rx per session. 200→binding dict (incl. ready-to-use pcf_uri), 404None (4G), 5xx/timeout→sbi.BsfError. Message-level + SDK tested (axum mock); not yet validated against a live open5gs BSF.
5G SBI — SCP indirect communication Implemented sbi.communication: indirect Spec-compliant indirect routing via the SCP (TS 29.500 §6.10). Npcf Model C emits 3gpp-Sbi-Target-apiRoot (the PCF known from the BSF binding); Nbsf Model D (delegated discovery) emits 3gpp-Sbi-Discovery-target-nf-type: BSF / service-names: nbsf-management / requester-nf-type (default AF). direct (default) is byte-identical to today. Header-level tested (axum mock); not yet validated against a live SCP.
5G SBI — Nchf (charging) Implemented sbi

Lawful Intercept / Recording

Feature Readiness Config Notes
LI master switch + audit log Implemented lawful_intercept
ETSI X1 admin interface Implemented lawful_intercept.x1 HTTPS + mTLS + bearer token
ETSI X2 IRI delivery Implemented lawful_intercept.x2 TCP/TLS to mediation device
ETSI X3 CC delivery Implemented lawful_intercept.x3 RTPEngine mirror reception
SIPREC recording (RFC 7866) Implemented lawful_intercept.siprec SIP Recording Server integration

Summary

Category Production Implemented Total
Transports 4 (UDP, TCP, TLS, TLS 1.3) 5 (WS, WSS, SCTP, mTLS, TLS 1.2) 9
Registrar 7 (Redis, expires, max contacts, hooks, TTL slack, Service-Route, registrant) 3 (memory, PG, Python, GRUU) 10
Authentication 6 (HTTP/HA1, digest 401/407, anti-spoof, Diameter Cx, IMS AKA) 3 (static, local Milenage AKA, SHA-256) 9
Security 5 (rate limit, scanner, trusted CIDR, fail ban, APIBan) 1 (IP ACLs) 6
NAT 5 (rport, fix contact, fix register, script fixup, stale eviction) 3 (keepalive, CRLF keepalive, flow tokens) 8
Media 1 (RTPEngine NG) 6 (LB, 4 profiles, custom profiles) 7
Gateway routing 3 (groups, round-robin, probes) 4 (weighted, hash, failover, dynamic) 7
CDR 0 5 (file, syslog, HTTP, register events, extra fields) 5
Tracing 3 (HEP v3 UDP, agent ID, error suppression) 2 (TCP, TLS) 5
Metrics 8 (Prometheus, all gauges/counters/histograms) 3 (admin health, stats, registrations) 11
Scripting 14 (proxy, B2BUA, registrar, auth, gateway, cache, presence, logging, metrics, async, ...) 3 (LI, timer, SDK) 17
3GPP/IMS 10 (Cx, Sh, Rx, peer mgmt, IMS AKA HSS-backed, IPsec, iFC, P/I/S-CSCF, Npcf) 4 (Ro, Rf, local Milenage AKA, Nchf) 14
LI/Recording 0 5 (X1, X2, X3, SIPREC, audit) 5
Totals ~66 ~42 ~109