Asterisk is the registrar for devices, not Hold Slayer. A softphone REGISTERs to the lab and the gateway transfers a live call to it by dialling extension 2001. This is deliberate: Hold Slayer's own SIP listener answers 200 OK to any REGISTER with no digest challenge, so anything on the network could register as a device and receive transferred calls. Keeping registration in Asterisk means the lab does not exercise or depend on that path, and the device is authenticated. The pjsua CLI built alongside the Python bindings is the test device — same library stack as the gateway, so no new dependency. Verified end to end: a gateway call to 2001 produces two channels Up under one bridge id. Three things that cost time and are now written down: - `--realm=asterisk`, not `--realm='*'`: the wildcard fails against Asterisk's digest challenge with PJSIP_EFAILEDCREDENTIAL. - pjsua is an interactive console app and exits ~8s after start if stdin is closed or /dev/null. `script -qfc` and `setsid </dev/null` both appear to work — registration succeeds — and then the process dies, leaving a stale contact in Asterisk that routes INVITEs to a port nobody is listening on. Hold a fifo open on stdin instead, and verify the port is actually bound rather than trusting `pjsip show contacts`. - Qualify is off for this AOR: the pjsua console does not answer OPTIONS, so polling marks a working softphone Unavail and the dialplan refuses to ring it. The 2001 guard therefore tests PJSIP_AOR(softphone,contact) rather than DEVICE_STATE. A real hardphone answers OPTIONS and can have it re-enabled. The identify block now matches source address *and port*. A host-only match claims every packet from that address, so a co-located softphone's REGISTER was attributed to the gateway endpoint and checked against the gateway's password — surfacing as "Failed to authenticate" on a correct password. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
162 lines
6.0 KiB
Plaintext
162 lines
6.0 KiB
Plaintext
; ---------------------------------------------------------------------------
|
|
; Hold Slayer lab dialplan — a fake bank phone tree
|
|
; ---------------------------------------------------------------------------
|
|
; Each extension is one scenario Hold Slayer must handle. Everything here is
|
|
; deterministic on purpose: real hold music varies per call, so a classifier
|
|
; regression on the PSTN is indistinguishable from noise. Against a fixed
|
|
; prompt the answer is binary.
|
|
;
|
|
; Hold Slayer dials these as `number` with SIP_TRUNK_HOST pointing here.
|
|
; ---------------------------------------------------------------------------
|
|
|
|
[globals]
|
|
; Lab-generated audio (tests/lab/sounds/, built by generate.py). The Asterisk
|
|
; image ships with no sounds at all, and these are synthesised from a fixed
|
|
; seed so the classifier sees byte-identical input on every run.
|
|
; lab-speech -> must classify LIVE_HUMAN
|
|
; lab-music -> must classify MUSIC
|
|
; lab-silence -> must classify SILENCE
|
|
GREETING=lab-speech
|
|
INVALID=lab-speech
|
|
|
|
[hold-slayer-lab]
|
|
|
|
; --- 1001: immediate answer, speech, hangup -------------------------------
|
|
; Baseline. Proves INVITE→200→ACK→RTP→BYE and that audio flows both ways.
|
|
; The classifier should report LIVE_HUMAN throughout.
|
|
exten => 1001,1,NoOp(LAB 1001: immediate answer)
|
|
same => n,Answer()
|
|
same => n,Wait(1)
|
|
same => n,Playback(${GREETING})
|
|
same => n,Playback(lab-speech)
|
|
same => n,Wait(20)
|
|
same => n,Playback(lab-speech)
|
|
same => n,Hangup()
|
|
|
|
; --- 1002: IVR menu, branches on DTMF -------------------------------------
|
|
; THE important one. Proves send_dtmf genuinely emits RFC 2833 and that
|
|
; Asterisk receives the digits — currently a no-op in MockSIPEngine.
|
|
; Press 1 → accounts (answers as human). Press 2 → cards (hold, then human).
|
|
exten => 1002,1,NoOp(LAB 1002: IVR menu)
|
|
same => n,Answer()
|
|
same => n,Wait(1)
|
|
same => n,Set(TRIES=0)
|
|
same => n(menu),Background(lab-speech)
|
|
same => n,WaitExten(8)
|
|
same => n,Set(TRIES=$[${TRIES} + 1])
|
|
same => n,GotoIf($[${TRIES} < 3]?menu)
|
|
same => n,Playback(lab-speech)
|
|
same => n,Hangup()
|
|
|
|
; Option 1 — straight to a "human"
|
|
exten => 1,1,NoOp(LAB 1002: caller pressed 1 -> accounts)
|
|
same => n,Playback(lab-speech)
|
|
same => n,Playback(lab-speech)
|
|
same => n,Wait(15)
|
|
same => n,Hangup()
|
|
|
|
; Option 2 — hold queue, then a "human"
|
|
exten => 2,1,NoOp(LAB 1002: caller pressed 2 -> cards, hold)
|
|
same => n,Playback(lab-speech)
|
|
same => n,Playback(lab-music)
|
|
same => n,Playback(lab-speech)
|
|
same => n,Wait(15)
|
|
same => n,Hangup()
|
|
|
|
exten => i,1,NoOp(LAB 1002: invalid entry)
|
|
same => n,Playback(${INVALID})
|
|
same => n,Goto(1002,menu)
|
|
|
|
exten => t,1,NoOp(LAB 1002: entry timeout)
|
|
same => n,Goto(1002,menu)
|
|
|
|
; --- 1003: hold music, then a human ---------------------------------------
|
|
; The whole hold-slayer loop in one call: classify music → stay on hold →
|
|
; detect the human → ring the owner. 60s of MoH is long enough for several
|
|
; classifier windows (CLASSIFIER_WINDOW_SECONDS defaults to 3.0).
|
|
exten => 1003,1,NoOp(LAB 1003: hold then human)
|
|
same => n,Answer()
|
|
same => n,Wait(1)
|
|
same => n,Playback(lab-speech)
|
|
same => n,Playback(lab-music)
|
|
same => n,Playback(lab-music)
|
|
same => n,Playback(lab-speech)
|
|
same => n,Playback(lab-speech)
|
|
same => n,Wait(30)
|
|
same => n,Hangup()
|
|
|
|
; --- 1004: long hold ------------------------------------------------------
|
|
; Exercises MAX_HOLD_TIME and HOLD_CHECK_INTERVAL. 10 minutes.
|
|
exten => 1004,1,NoOp(LAB 1004: long hold)
|
|
same => n,Answer()
|
|
same => n,Wait(1)
|
|
same => n,Playback(lab-speech)
|
|
same => n,Playback(lab-music)
|
|
same => n,Playback(lab-music)
|
|
same => n,Playback(lab-music)
|
|
same => n,Playback(lab-music)
|
|
same => n,Playback(lab-speech)
|
|
same => n,Wait(15)
|
|
same => n,Hangup()
|
|
|
|
; --- 1005: busy -----------------------------------------------------------
|
|
; Failure path: the call must be marked FAILED with no stuck leg.
|
|
exten => 1005,1,NoOp(LAB 1005: busy)
|
|
same => n,Busy(20)
|
|
same => n,Hangup()
|
|
|
|
; --- 1006: ring, never answer ---------------------------------------------
|
|
; Timeout path. Rings for 120s without answering.
|
|
exten => 1006,1,NoOp(LAB 1006: ring no answer)
|
|
same => n,Progress()
|
|
same => n,Wait(120)
|
|
same => n,Hangup()
|
|
|
|
; --- 1007: answer, then remote hangup after 5s ----------------------------
|
|
; Proves remote-BYE handling and that the call persists to the DB on hangup.
|
|
exten => 1007,1,NoOp(LAB 1007: quick remote hangup)
|
|
same => n,Answer()
|
|
same => n,Playback(${GREETING})
|
|
same => n,Wait(5)
|
|
same => n,Hangup()
|
|
|
|
; --- 1008: answer, then silence -------------------------------------------
|
|
; Classifier SILENCE vs the no-audio case. 45s of nothing.
|
|
exten => 1008,1,NoOp(LAB 1008: silence)
|
|
same => n,Answer()
|
|
same => n,Playback(lab-silence)
|
|
same => n,Wait(40)
|
|
same => n,Hangup()
|
|
|
|
; --- 2001: ring the registered softphone ----------------------------------
|
|
; The transfer target. Asterisk is the registrar for devices, so the gateway
|
|
; reaches a desk phone by dialling this rather than by registering it itself.
|
|
; Fails fast when nothing is registered — a silent 30s ring would look like a
|
|
; gateway bug rather than an absent softphone.
|
|
exten => 2001,1,NoOp(LAB 2001: ring softphone)
|
|
; Count registered contacts rather than DEVICE_STATE: device state follows
|
|
; the OPTIONS qualify, which is off for this AOR (the pjsua CLI does not
|
|
; answer OPTIONS), so a registered softphone would still read UNAVAILABLE.
|
|
same => n,GotoIf($[${PJSIP_AOR(softphone,contact)} = ""]?nodevice)
|
|
same => n,Dial(PJSIP/softphone,30)
|
|
same => n,Hangup()
|
|
same => n(nodevice),NoOp(LAB 2001: no softphone registered)
|
|
same => n,Answer()
|
|
same => n,Playback(lab-speech)
|
|
same => n,Hangup()
|
|
|
|
; --- echo test ------------------------------------------------------------
|
|
; Not a scenario — a debugging aid. Echoes audio back so you can confirm
|
|
; bidirectional RTP by ear when something looks wrong.
|
|
exten => 1099,1,NoOp(LAB 1099: echo test)
|
|
same => n,Answer()
|
|
same => n,Playback(lab-speech)
|
|
same => n,Echo()
|
|
same => n,Hangup()
|
|
|
|
; Anything else: reject explicitly rather than failing obscurely.
|
|
exten => _X.,1,NoOp(LAB: unknown extension ${EXTEN})
|
|
same => n,Answer()
|
|
same => n,Playback(${INVALID})
|
|
same => n,Hangup()
|