STIR/SHAKEN — caller-ID attestation
Anti-spoofing for North American calls. The originating carrier signs a JWT asserting "yes, this caller-ID belongs to us". Verifying carriers check the signature; if it fails, the call is marked as suspected spam at the terminating endpoint.
In our config, the warning Endpoint dyn-…: Option 'stir_shaken' is no longer supported. Use 'stir_shaken_profile' instead. is cosmetic — Asterisk 22.8.2 deprecated the old syntax. Replace with stir_shaken_profile in the endpoint block to silence it.
We do not currently sign or verify STIR/SHAKEN on outbound TR calls. If we expand to US calls, this becomes mandatory: full-attestation A means we asserted both the call origin and the right to use that caller-ID; B means call origin only; C means we just relayed it.
The 60-second drop
Whenever a call drops at exactly ~60 seconds, the cause is missing RTP, not the timer. The timer (rtptimeout=60) is doing its job. Find the missing ACK, the missing route, or the missing firewall rule.
The Garanti M08 6-hypothesis trail is the canonical example of this symptom — six different ways to cause one drop pattern. Walk through it below.
Every outbound call to Garanti's VVB dropped at exactly 60 s. Six hypotheses on the table. Click a card to expand its evidence; one of them is right.
c= line and packet source IPs at both ends.identify match missmatch=10.231.16.153/32 in [providers-identify] resolved to KASP.pjsip show endpoints classified inbound INVITE under the providers endpoint. Auth passed.SIP "light mode" proxy
A stateless SIP proxy passes signaling but does not insert itself into the dialog (no Record-Route). Consequence: ACK and BYE bypass the proxy and go end-to-end UA-to-UA.
If your firewall opens 5060 only between the proxy and your Asterisk, ACK is lost. This is exactly the Garanti M08 root cause. Fix: open 5060 directly UA-to-UA, in addition to the proxy path.
Stateful proxy (Record-Route inserted)
record_route() in the route block.Light-mode proxy (no Record-Route)
$ ssh freya@192.168.35.197 "docker exec freya-asterisk asterisk -rx 'pjsip show contacts'" $ ssh freya@192.168.35.197 "docker exec freya-asterisk asterisk -rx 'pjsip set logger on'" $ # place a call, watch for Record-Route headers in the logs
SIP timer B
Default 32 s. After Timer B fires on an INVITE without a final response, the UA gives up and emits 408 Request Timeout. KKB campaign 879c7bb0 had one such timeout on +905537721287 at 20:38 UTC — the carrier never answered the INVITE.
You can shorten Timer B (timer_b in PJSIP) but you lose tolerance to slow PSTN gateways. The default 32 s comes from RFC 3261's worst-case retransmit ladder: T1 = 500 ms, then doubling — 0.5, 1, 2, 4, 8, 16 s — for a total of seven INVITE attempts before giving up.
Symmetric NAT and direct_media
Symmetric NAT maps every (src-IP, src-port, dst-IP, dst-port) tuple to a unique external port. Two peers behind symmetric NAT cannot direct-talk even with STUN — they need TURN.
direct_media = no sidesteps this entirely (Asterisk relays). It is also why our agent can record + transcribe + run the LLM. Two reasons one knob stays off.
| NAT type | Mapping rule | STUN works? | Direct media works? |
|---|---|---|---|
| Full cone | Same external port for any peer | Yes | Yes |
| Restricted cone | External port reused; remote IP gated | Yes | Yes if STUN-warmed |
| Port-restricted | Remote IP+port both gated | Yes | Sometimes |
| Symmetric | New external port per (src,dst) tuple | No | No — needs TURN |
TLS vs SRTP
TLS — signaling
Encrypts SIP messages: INVITE, 200 OK, REGISTER, headers, request URIs. Not the audio. Cheap. Recommended on any inter-network leg.
SRTP — audio
Encrypts RTP payloads with AES. Needs a key exchange (SDES in SDP, or DTLS-SRTP). More CPU, slightly higher per-packet overhead.
You can have one without the other. Most enterprise deployments use TLS without SRTP — signaling is small and contains identifiers; audio is high-bandwidth and stays inside trusted MPLS. KKB and Garanti both run unencrypted UDP today because the traffic never leaves the customer's controlled network.
Concurrent channel limits
Every trunk provider has a per-account ceiling. Exceeding triggers 503 Service Unavailable from the provider, or 603 Decline per destination. Verimor caps and per-destination blocks both showed up in the 24 April 2026 KKB outbound test.
879c7bb0 (20:38 UTC, 24 April 2026): +905374705251, +905510154221, +905532890209, +905053864612, +905535918355 all returned SIP 603 with Q.850 cause 41 across multiple runs — stable carrier-side block, not transient congestion.You can rate-limit on our side from the campaign-worker: cap concurrency, add inter-INVITE gap. See step 95.
Codec transcoding cost
Each codec conversion uses CPU. Asterisk on modern x86 can handle hundreds of ulaw ↔ alaw transcodings without breaking a sweat (8 kHz mono, simple lookup tables). Opus or G.722 transcoding is more expensive — watch CPU when adding HD codecs.
| Codec pair | CPU per channel | Notes |
|---|---|---|
| ulaw ↔ alaw | ~0.1% | Lookup table, no DSP work |
| ulaw ↔ G.722 | ~0.5% | 16 kHz upsampling + ADPCM |
| ulaw ↔ Opus | ~1.5% | Resampling, frame boundary alignment |
| G.711 passthrough | 0% | No transcoding — the goal |
Rule of thumb: pick a codec the carrier already speaks. We standardise on ulaw + alaw at every TR customer and let the agent's STT/TTS handle the higher-fidelity end.
CDR — Call Detail Records
Asterisk writes a CDR row per call: caller-ID, called-number, start/answer/end times, duration, hangup cause, recording filename.
CSV backend writes to /var/log/asterisk/cdr-csv/Master.csv. ODBC/MySQL backends write to a database. We rely primarily on the database CDR + dashboard's own call records; the CSV file is a backup. The current Unable to open … cdr-csv/Master.csv warning at KKB is because the directory does not exist in the container — cosmetic.
When the dashboard team asks "what does our column X mean?", this row is your answer. Open work item from KKB step 99: surface SIP response code + Q.850 cause directly in the dashboard call detail page, so triage doesn't need shell access.
Recording compliance
Türkiye PDPL (KVKK) requires:
- Consent disclosure at call start.
- Audit logs of access to recordings.
- Time-bounded retention (typically 1–3 years).
Our agent plays a disclosure (configurable per workflow). Recordings live in S3/MinIO with bucket policies; access goes through the dashboard, which logs every fetch.
Production checklist — 10 things to verify before a customer goes live
allow=ulaw,alaw only — no surprise Opus / G.722 transcoding.tcpdump, not just docs. UDP vs TCP misalignment is silent.$ ssh freya@192.168.35.197 \ "docker exec freya-asterisk asterisk -rx 'cdr show status'" $ ssh freya@192.168.35.197 \ "docker exec freya-asterisk asterisk -rx 'pjsip show endpoint providers'" \ | grep -E 'allow|direct_media|rtptimeout|force_rport|rewrite_contact'
Why is our direct_media = no non-negotiable today?
Show answer
Two reasons that compound: (1) topology — symmetric NAT plus the typical TR enterprise firewall layout means peers cannot reliably negotiate end-to-end RTP, so Asterisk relaying audio is the only path that works through every customer's network; (2) product — our voice agent must record, transcribe, and inject TTS into the audio stream. Direct media would route RTP UA-to-UA and bypass our pipeline entirely. As long as we are an AI agent — not a pure SIP proxy — direct media stays off.