End to end telephony scenarios
This page gives the state of the 31 end to end scenarios of the Navin telephony (ADR-0173, task t-211ee738). The tests run with the real engines, without root or containers: Asterisk 20.6.0, Kamailio 5.7.4 (file also checked by Kamailio 6.0.8), RTPengine, SIPp 3.7.2, PostgreSQL (pgserver), SeaweedFS, and the real Control Plane code: compilation, publication through AMI to Asterisk and through JSONRPC to Kamailio, routes under /v1/telephony.
Statuses: executed (the test passes here), blocked (impossible here, with the reason).
Test bench
The Navin Manager renders the asterisk, kamailio and rtpengine services of its catalog
(tests/telephony/render_stack.py, run with the Manager interpreter). tests/telephony/e2e_stack.py starts the
engines, speaks AMI, ARI and JSONRPC, runs SIPp and provides a Python caller (PCMU RTP and RFC 4733 DTMF, which SIPp
cannot send without privileges) and a SIP client over secure WebSocket. Loopback addresses: Kamailio
127.0.0.21:5060, Asterisk 127.0.0.1, phones of tenant A 127.0.0.6, of tenant B 127.0.0.7, carriers 127.0.0.5
(primary trunk) and 127.0.0.15 (backup trunk).
One time preparation: ln -s en_US_f_Allison /tmp/toolchains/asterisk/usr/share/asterisk/sounds/en (the Debian
sound package creates this link in its postinst, which an extraction skips; without it the IVR, voicemail and
conference PIN prompts fail). The test fails with this command when the link is missing.
Commands
services/control-plane/.venv/bin/python -m pytest services/control-plane/tests/test_telephony_e2e_edge.py -q -p no:cacheprovider
services/control-plane/.venv/bin/python -m pytest services/control-plane/tests/test_telephony_e2e.py -q -p no:cacheprovider
services/control-plane/.venv/bin/python -m pytest services/control-plane/tests/test_telephony_e2e_dedicated.py -q -p no:cacheprovider
services/control-plane/.venv/bin/python -m pytest services/control-plane/tests/test_telephony_ha_real.py -q -p no:cacheprovider
Results of 3 November 2026, after the fix of findings 3, 7, 8 and 9: edge 6 passed; e2e 21 passed; dedicated 2 passed; ha_real 7 passed (tests of tel-ha). 29 scenarios executed, 2 blocked (S30, S31).
Matrix
| No. | Scenario | Status | Proof or reason |
|---|---|---|---|
| S01 | Asterisk single rendered by the Manager, modules loaded | executed | test_telephony_e2e_edge.py::test_s01_*, test_telephony_e2e.py::test_s01_* |
| S02 | Asterisk pool of 2 nodes | executed (tel-ha) | test_telephony_ha_real.py::test_1_both_asterisk_nodes_are_probed_up, test_2_calls_of_one_tenant_land_on_the_same_node |
| S03 | Kamailio checked by the real binary | executed | test_telephony_e2e_edge.py::test_s03_* (5.7.4 and 6.0.8, JSONRPC over HTTPS with the test CA) |
| S04 | RTPengine starts and answers ng ping | executed | test_telephony_e2e_edge.py::test_s04_* |
| S05 | Shared model: two tenants published and read back | executed | test_telephony_e2e.py::test_s05_* (GetConfig, endpoints, Kamailio tables) |
| S06 | Dedicated model | executed | test_telephony_e2e_dedicated.py::test_s06_* (one tenant alone on its pool, call between two extensions) |
| S07 | Registration through Kamailio, Path kept | executed | test_telephony_e2e.py::test_s07_* (Path of the edge read through AMI PJSIPShowContacts, wrong password refused) |
| S08 | Internal call between two extensions | executed | test_telephony_e2e.py::test_s08_* |
| S09 | Tenant isolation | executed | test_telephony_e2e.py::test_s09_* (no context of B names A, call of B to A refused, credentials of B refused on the domain of A) |
| S10 | Inbound DID from an allowed carrier | executed | test_telephony_e2e.py::test_s10_* |
| S11 | Unknown DID (404), host outside trunk_hosts refused | executed | test_telephony_e2e.py::test_s11_*, test_telephony_e2e_edge.py::test_s11_* |
| S12 | IVR: DTMF to a queue, timeout, invalid choice | executed | test_telephony_e2e.py::test_s12_* (option 1 to the queue, invalid choice to 1001); timeout exit proven by test_s14_* |
| S13 | Queue answered by a member | executed | test_telephony_e2e.py::test_s13_* |
| S14 | Voicemail on no answer, AMI count | executed | test_telephony_e2e.py::test_s14_* (MailboxCount, msg0000 file in INBOX) |
| S15 | Conference, PIN | executed | test_telephony_e2e.py::test_s15_* (two callers admitted, wrong PIN refused) |
| S16 | Recording, object storage, signed URL | executed | test_telephony_e2e.py::test_s16_s17_*: MixMonitor file listed by ARI, stored in SeaweedFS (indexed by media.sweep, see finding 8), signed URL of 300 s, forged signature refused, other tenant refused |
| S17 | CDR and history from Cdr and CEL | executed | test_telephony_e2e.py::test_s16_s17_* (AMI stream of tel-calls: inbound call, ANSWERED, extension 1002) |
| S18 | National call through a trunk, failover | executed | test_telephony_e2e.py::test_s18_* (caller id of the trunk, 503 of the primary then backup trunk) |
| S19 | International refused without permission | executed | test_telephony_e2e.py::test_s19_* |
| S20 | Premium prefix blocked by the quota | executed | test_telephony_e2e.py::test_s20_* |
| S21 | Call duration limit | executed | test_telephony_e2e.py::test_s21_* (BYE of Asterisk after 60 s) |
| S22 | CPS limit per tenant (pipelimit) | executed | test_telephony_e2e.py::test_s22_* (a burst above the limit of the tenant refused with 503, after the fix of finding 7) |
| S23 | Concurrent calls per tenant | executed | test_telephony_e2e.py::test_s23_* (486 "Tenant call limit") |
| S24 | Anti-flood (pike) | executed | test_telephony_e2e_edge.py::test_s24_* |
| S25 | Anti-bruteforce (ipban) | executed | test_telephony_e2e_edge.py::test_s25_* |
| S26 | Versions: v2 then back to v1 | executed | test_telephony_e2e_dedicated.py::test_s26_* (files of the node equal to those of v1) |
| S27 | Node failure and drain | executed (tel-ha) | test_telephony_ha_real.py::test_3_4_5_loss_of_a_node_and_its_return, test_6_drain_of_a_node, test_6b_drain_with_the_disabled_state_holds; the drain of Navin Enterprise sends the state d (disabled, neither selected nor probed), because the state i was set active again by the OPTIONS probes after 15 s |
| S28 | WebRTC: ephemeral credentials, REGISTER over WSS | executed | test_telephony_e2e.py::test_s28_* (credentials of the Control Plane accepted over WSS, wrong credentials refused, after the fix of finding 9; media not tested) |
| S29 | call.ended webhook and fraud block | executed | call.ended proven by test_s16_s17_*; test_s29_*: block written into tenant_blocks and the call of the blocked extension refused, after the fix of finding 3 |
| S30 | LiveKit SIP call to a voice agent | blocked | livekit/sip ships no binary (empty GitHub releases, tags up to v1.22.0), only a container image; building it needs libopus, libopusfile and libsoxr (system packages, root); no container runtime here. Proven elsewhere: services/voice-agent/tests/test_registration_real.py, test_telephony_calls.py::test_livekit_webhook_and_voice_worker_contract (fake Twirp server) |
| S31 | Transfer of an AI call to a human | blocked | same reason as S30; TransferSIPParticipant proven against a fake Twirp server in test_telephony_calls.py |
Findings
- AMI publication impossible on a real Asterisk (UpdateConfig adds nothing to a template category). Fixed by tel-sipfix: AOR without template.
- Phones identified as
navin-kamailio: REGISTER refused with 403. Fixed by tel-sipfix:X-Navin-Endpointheader,endpoint_identifier_order = header,ip,username,anonymous. - No
tenant_blockstable in the rendered Kamailio:htable.setsanswers "HTTP 500", a fraud block never reaches the edge. Fixed: table declared (replicated by DMQ in ha) and checked inroute[FROM_PHONE], 403 for a blocked tenant or extension;route[FRAUD_BLOCK]also refuses with 403 the calls of the carriers to a blocked tenant and those of its PBX to a carrier (UUID ofX-Navin-Tenantturned into the tid), before any relay; deleting the key restores the calls (navin-manager/server/tests/test_kamailio_real.py::test_fraud_blocks_refuse_the_calls_of_the_tenant_until_lifted, Kamailio 5.7.4 and 6.0.8). rewrite_contact = yesbehind Kamailio: the contact became the address of the edge. Fixed by tel-sipfix.- Preloaded Route to the edge (Path) refused by
route[FROM_INTERNAL](loose_routeis false for an initial request). Fixed by tel-sipfix. - Duplicate
navin-rejectbetween the Manager and the compiler. Fixed by tel-sipfix. pl_check(...) < 0inroute[LIMITS]is never true: the CPS limit refuses nothing (8 INVITEs in one burst for a limit of 3, all relayed). Fixed:if (!pl_check(...)), a negative code being false.apps = allincel.conf.j2: Asterisk 20 no longer acceptsall, no CEL APP_START event is emitted, the recording is not announced to the ingestion. Fixed: explicit list of applications (mixmonitor, queue, dial, playback, background, voicemail, confbridge, stasis). Test S16 still goes throughmedia.sweep; the announcement through APP_START is not proven by a test yet.- WebRTC password signed with HMAC-SHA1 by the Control Plane while the Manager sets
sha_algorithmto SHA256 by default: REGISTER refused. Fixed: settingEAI_TELEPHONY_EPHEMERAL_ALGORITHM(sha256 by default), to keep equal to theephemeral_algorithmparameter of the kamailio service. - The TLS listeners of Kamailio refuse handshakes for the first seconds: the bench waits for a JSONRPC answer before it starts.