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>