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>
133 lines
5.0 KiB
Plaintext
133 lines
5.0 KiB
Plaintext
; ---------------------------------------------------------------------------
|
|
; 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 *and port*. 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 came from sidesteps that.
|
|
;
|
|
; The port is essential when the softphone runs on the same host: a
|
|
; host-only match claims *every* packet from that address, so the
|
|
; softphone's REGISTER would be attributed to this endpoint and checked
|
|
; against the gateway's password ("Failed to authenticate", confusingly).
|
|
; Endpoints that authenticate by username (the softphone) must not be
|
|
; covered by an identify block.
|
|
[hold-slayer]
|
|
type = identify
|
|
endpoint = hold-slayer
|
|
match = {{ asterisk_match_host }}:{{ asterisk_gateway_port }}
|
|
|
|
[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 }}
|
|
|
|
; ---------------------------------------------------------------------------
|
|
; Softphone endpoint — the transfer target
|
|
; ---------------------------------------------------------------------------
|
|
; Asterisk is the registrar for devices, not Hold Slayer. A softphone REGISTERs
|
|
; here and the gateway transfers a live call to it by dialling extension 2001.
|
|
;
|
|
; 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 depend on that path, and the softphone is authenticated.
|
|
;
|
|
; Test with the pjsua CLI built alongside the Python bindings:
|
|
; pjsua --null-audio --auto-answer=200 \
|
|
; --id=sip:softphone@<asterisk-host> \
|
|
; --registrar=sip:<asterisk-host>:21061 \
|
|
; --realm='*' --username=softphone --password=<pw> \
|
|
; --local-port=<free port>
|
|
|
|
[softphone]
|
|
type = endpoint
|
|
context = hold-slayer-lab
|
|
disallow = all
|
|
allow = ulaw
|
|
allow = alaw
|
|
auth = softphone-auth
|
|
aors = softphone
|
|
dtmf_mode = rfc4733
|
|
direct_media = no
|
|
force_rport = yes
|
|
rewrite_contact = yes
|
|
rtp_symmetric = yes
|
|
|
|
[softphone-auth]
|
|
type = auth
|
|
auth_type = userpass
|
|
username = {{ asterisk_softphone_username }}
|
|
password = {{ asterisk_softphone_password }}
|
|
|
|
[softphone]
|
|
type = aor
|
|
; The device's contact is learned from its REGISTER rather than configured —
|
|
; a softphone's port is not known in advance.
|
|
max_contacts = 1
|
|
remove_existing = yes
|
|
; No qualify: the pjsua CLI does not answer OPTIONS while sitting at its
|
|
; console prompt, so polling marks a perfectly working softphone Unavail and
|
|
; the dialplan refuses to ring it. Registration itself is the liveness signal
|
|
; here. A real hardphone answers OPTIONS and can have qualify re-enabled.
|
|
qualify_frequency = 0
|
|
|
|
[hold-slayer]
|
|
type = aor
|
|
max_contacts = 2
|
|
remove_existing = yes
|
|
qualify_frequency = 60
|