test(lab): add Asterisk lab — a fake PSTN for media validation
An Asterisk instance that answers calls, plays an IVR, holds with music and connects a "human", so the gateway has something real to dial that is not the PSTN: no charges, no strangers, no E911 exposure. Asterisk rather than Kamailio because the unproven risks are media risks. Kamailio is a proxy — it routes signalling and answers nothing, so it would forward the INVITE and find nobody home. Asterisk is a B2BUA: it answers, plays prompts and collects DTMF, which is the hold-slayer scenario itself. Kamailio remains the better model for trunk registration/digest auth later. No application changes are needed to use it. SIP_TRUNK_HOST is just an address, so the production code path runs unmodified — there is no test-only branch anywhere in the gateway. It also means safety is structural: while the trunk points at the lab there is no route to the PSTN at all, an absence of route rather than a policy that could be misconfigured. Nine scenarios (1001-1008 plus an echo test) cover the baseline call, the IVR/DTMF path, hold-then-human, long hold, busy, no-answer, remote hangup and silence. The image ships no sound files, so sounds/generate.py synthesises three fixtures from fixed seeds — byte-identical on every run, which is what makes a classifier regression distinguishable from noise. Verified against AudioClassifier: music→MUSIC 0.85, speech→LIVE_HUMAN 0.75, silence→SILENCE 1.00. The speech formants deliberately avoid the DTMF bands; the first version landed on a valid pair and classified as a keypress. Anonymous inbound calls are refused, and endpoint matching is by source address — Asterisk's default matches the From-header domain, which Hold Slayer populates from its SIP bind address (0.0.0.0 on a wildcard bind). Generated audio and the rendered per-host configs are gitignored: the former is reproducible from a fixed seed, the latter carry a host-specific IP and the lab password. Known limit, documented in the README: MediaPipeline.create_tap is a stub, so the classifier receives no audio on a live call. RTP flows and Asterisk plays audio, but the tap is never fed — the fixture results above were measured by feeding the classifier directly. This blocks scenarios 1002/1003/1004. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
75
tests/lab/dialplan/pjsip.conf
Normal file
75
tests/lab/dialplan/pjsip.conf
Normal file
@@ -0,0 +1,75 @@
|
||||
; ---------------------------------------------------------------------------
|
||||
; Hold Slayer lab — PJSIP configuration
|
||||
; ---------------------------------------------------------------------------
|
||||
; SECURITY: this endpoint answers calls. Asterisk's stock examples allow
|
||||
; anonymous inbound, which is a well-known toll-fraud target. This config
|
||||
; refuses it: every call must authenticate as the `hold-slayer` endpoint.
|
||||
;
|
||||
; There is no PSTN behind this Asterisk — an unauthorised call reaches only
|
||||
; the lab dialplan and costs nothing. The lock-down is defence in depth and
|
||||
; so this config is never copied somewhere it would matter.
|
||||
; ---------------------------------------------------------------------------
|
||||
|
||||
[global]
|
||||
type = global
|
||||
; Do not fall through to an `anonymous` endpoint for unmatched calls.
|
||||
; This is the single most important line in the file.
|
||||
unidentified_request_count = 5
|
||||
unidentified_request_period = 5
|
||||
unidentified_request_prune_interval = 30
|
||||
|
||||
[transport-udp]
|
||||
type = transport
|
||||
protocol = udp
|
||||
bind = 0.0.0.0:{{ asterisk_sip_port }}
|
||||
; The address Asterisk advertises in SDP. Without this, containers advertise
|
||||
; their internal bridge IP and RTP arrives at an unroutable address — the
|
||||
; classic "call connects but there is no audio" failure.
|
||||
external_media_address = {{ asterisk_external_ip }}
|
||||
external_signaling_address = {{ asterisk_external_ip }}
|
||||
local_net = {{ asterisk_local_net }}
|
||||
|
||||
; ---------------------------------------------------------------------------
|
||||
; Hold Slayer endpoint
|
||||
; ---------------------------------------------------------------------------
|
||||
; Hold Slayer authenticates as this endpoint to place calls into the lab.
|
||||
|
||||
; Identify the endpoint by source address. Asterisk's default matching uses
|
||||
; the From-header domain, which Hold Slayer populates from its SIP bind
|
||||
; address (0.0.0.0 on a wildcard bind) — never a value Asterisk can match.
|
||||
; Matching on where the packet actually came from sidesteps that.
|
||||
[hold-slayer]
|
||||
type = identify
|
||||
endpoint = hold-slayer
|
||||
match = {{ asterisk_match_host }}
|
||||
|
||||
[hold-slayer]
|
||||
type = endpoint
|
||||
context = hold-slayer-lab
|
||||
disallow = all
|
||||
; ulaw first: it is what the PSTN uses, so the lab exercises the same codec
|
||||
; path a real trunk would. alaw as fallback.
|
||||
allow = ulaw
|
||||
allow = alaw
|
||||
auth = hold-slayer-auth
|
||||
aors = hold-slayer
|
||||
; RFC 2833 out-of-band DTMF — what send_dtmf must produce. Setting this
|
||||
; explicitly (rather than `auto`) means a DTMF failure is a real failure and
|
||||
; not a negotiation fallback quietly rescuing it.
|
||||
dtmf_mode = rfc4733
|
||||
direct_media = no
|
||||
force_rport = yes
|
||||
rewrite_contact = yes
|
||||
rtp_symmetric = yes
|
||||
|
||||
[hold-slayer-auth]
|
||||
type = auth
|
||||
auth_type = userpass
|
||||
username = {{ asterisk_sip_username }}
|
||||
password = {{ asterisk_sip_password }}
|
||||
|
||||
[hold-slayer]
|
||||
type = aor
|
||||
max_contacts = 2
|
||||
remove_existing = yes
|
||||
qualify_frequency = 60
|
||||
Reference in New Issue
Block a user