Trunking SIP
CallAgent termină SIP el însuși, prin FreeSWITCH, în loc să împacheteze un carrier gestionat, așa că aducerea trunk-ului tău e o cale de primă clasă: carrier-ul tău, DID-urile tale, tarifele tale.
Folosește-l când ai deja o relație cu un carrier, când numerele tale nu pot fi mutate, sau când ai nevoie de un trunk pentru care CallAgent nu are adaptor. Dacă nu ești în niciunul dintre cazurile astea, un număr gestionat e mai puțină muncă.
Starea onestă
Citește asta înainte să îți planifici o migrare
Niciun apel pe trunk SIP nu a ajuns vreodată la un asistent.
Autentificarea trunk-ului funcționează și e dovedită pe un peer SIP real: registration, provocare digest, raportarea eșecului de credențiale, admitere și refuz prin ACL, reînregistrare înainte de expirare. Toate astea sunt reale.
Ce nu funcționează e pasul de după admitere — predarea media către voice gateway. Un INVITE de intrare e admis, marcat cu DID-ul și trunk-ul lui, transferat către extensia de predare a media, și moare acolo cu handoff-failed. Cauza e că dialplan-ul FreeSWITCH așteaptă un socket ESL de ieșire pe portul 8084, iar gateway-ul ascultă pe un singur port, 3010, pentru HTTP și WebSocket. Pe 8084 nu ascultă nimic.
Tot ce urmează despre configurarea unui trunk e corect și merită făcut. Tot ce urmează se oprește înainte de audio. Detalii complete în Ce nu este construit.
Modelul
Patru obiecte, în ordinea asta:
| Obiect | Ce este |
|---|---|
| Trunk | Relația cu un carrier: credențiale sau allowlist de IP-uri, codec-uri, mod DTMF, transport, formatarea numerelor, limite. |
| Gateway | Un host către care trimiți, sau de la care primești, sub trunk-ul acela. Un trunk cu mai multe gateways are failover; fiecare e verificat separat. |
| DID | Un număr de intrare, sau un tipar, pe trunk-ul acela, și unde ar trebui să meargă un apel către el. |
| Rută | Ieșire: prin ce trunk pleacă un număr apelat. |
Fiecare gateway devine un fișier de profil Sofia generat, pe partea de FreeSWITCH, numit cu ULID-ul gateway-ului. Numele acela e cel pe care îl apelează dialplan-ul și cu care e marcat un apel de intrare, deci e stabil la redenumiri.
Creează un trunk
API-ul de telefonie e camelCase. (API-ul de asistenți e snake_case; vezi convențiile API.)
curl -X POST "$CALLAGENT_API_URL/telephony/sip-trunks" \
-H "Authorization: Bearer $CALLAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Carrier A",
"direction": "both",
"authMode": "credentials",
"username": "1002001",
"password": "…",
"realm": "sip.carrier-a.example",
"register": true,
"registerExpirySeconds": 3600,
"transport": "udp",
"codecs": ["PCMA", "PCMU", "G722"],
"dtmfMode": "rfc2833",
"inboundRequireAcl": true,
"inboundSourceAcl": ["203.0.113.0/24"],
"outboundCallerIdNumber": "+40312345901",
"maxConcurrentCalls": 30
}'Apoi cel puțin un gateway:
curl -X POST "$CALLAGENT_API_URL/telephony/sip-trunks/$TRUNK_ID/gateways" \
-H "Authorization: Bearer $CALLAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "sbc-1",
"host": "sbc1.carrier-a.example",
"port": 5060,
"priority": 10,
"weight": 100
}'Gateways-urile sunt încercate crescător după priority, apoi descrescător după weight în interiorul unei priorități. Un gateway cu failoverOnly: true se sortează ultimul, indiferent de prioritate.
Fiecare câmp de trunk
| Câmp | Valori | Implicit |
|---|---|---|
direction | inbound · outbound · both | — |
authMode | credentials · ip · both | credentials |
register | boolean | false |
registerExpirySeconds | 60–86400 | 3600 |
retrySeconds | 5–3600 | 30 |
transport | udp · tcp · tls | udp |
srtpPolicy | disabled · optional · required | disabled |
verifyTlsPeer | boolean | |
codecs | listă de nume | ["PCMA","PCMU","G722","OPUS"] |
dtmfMode | rfc2833 · inband · info | rfc2833 |
optionsPingEnabled, optionsPingSeconds, keepaliveSeconds | 5–3600 | |
inboundRequireAcl, inboundSourceAcl | CIDR-uri | |
inboundDestinationType / Id / Value | rezerva când niciun DID nu se potrivește | |
inboundStripDigits, inboundNumberPrefix | normalizarea numerelor | |
outboundCallerIdNumber / Name / Privacy | ||
leadingPlus | keep · strip · add · international_prefix | |
internationalPrefix, outboundPrefix, outboundStripDigits | ||
allowSipUriDialing, customSipHeaders | ||
maxConcurrentCalls, maxCallsPerSecond |
password e criptată de model și nu e întoarsă niciodată de vreun endpoint.
Când authMode e ip, XML-ul Sofia generat omite complet username, password și auth-username, în loc să scrie valori de umplutură, deci un trunk cu autentificare pe IP greșit configurat eșuează vizibil, în loc să se înregistreze ca un trunk cu credențiale stricat.
Codec-uri
codecs e emis ca absolute_codec_string — exact lista aia, în ordinea aia, fără negociere de rezervă. G.729 nu e în lista implicită și nu e în setul de module FreeSWITCH livrat. Gateway-ul normalizează tot la PCM mono 16 kHz intern, deci un trunk de 8 kHz și un browser de 48 kHz întâlnesc același pipeline.
DTMF
Trei moduri, per trunk pentru că nu sunt carrier-ii de acord între ei:
dtmfMode | Valoarea Sofia | Notă |
|---|---|---|
rfc2833 | rfc2833 | Implicit. Evenimente RTP, în afara benzii. |
info | info | Mesaje SIP INFO. |
inband | none | Sofia nu detectează; detectorul Goertzel propriu al CallAgent citește audio-ul. Fragil la transcodare — așteaptă-te la rateuri pe un leg G.729 sau intens transcodat. |
dtmfDurationMs e implicit 2000, iar interDigitDelayMs 120.
O divergență care merită știută
DtmfModeSchema din contractul TypeScript declară cinci valori — rfc2833, rfc4733, sip_info, inband, auto. Enum-ul PHP care generează efectiv configurația Sofia are trei: rfc2833, inband, info. Configurează prin API, cu cele trei.
Rutarea unui apel de intrare
Când sosește un apel, resolverul le încearcă în ordinea asta și o ia pe prima care se potrivește:
- o rută de intrare care se potrivește pe număr, după prioritate;
- hook-ul dinamic al numărului, dacă e configurat;
- destinația statică a numărului;
- destinația de rezervă a trunk-ului (
inboundDestinationType/Id/Value) — doar la intrare pe SIP.
Pentru un trunk anume, pașii 3 și 4 sunt precedați de tabela de DID-uri a trunk-ului: rândurile SipDid se potrivesc primele, apoi un rând din phone_numbers al cărui număr e egal cu DID-ul normalizat, apoi rezerva trunk-ului.
Nicio potrivire înseamnă nimic, iar apelantul e respins. E intenționat: rutarea unui apel pe care nu îl poți identifica spre un asistent implicit e felul în care apelantul unui tenant ajunge la agentul altui tenant.
Tenantul e legat din rândul care s-a potrivit, niciodată din ceva din request.
DID-uri
curl -X POST "$CALLAGENT_API_URL/telephony/sip-trunks/$TRUNK_ID/dids" \
-H "Authorization: Bearer $CALLAGENT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"number": "+40312345901",
"destinationType": "assistant",
"destinationId": "'"$ASSISTANT_ID"'",
"priority": 100
}'Un DID se poate potrivi exact pe number sau pe pattern, cu un matchType. Tipurile de destinație sunt aceleași zece ca peste tot: assistant, squad, queue, ivr, ring_group, extension, voicemail, external, sip_uri, hook.
De unde e citit DID-ul se stabilește per trunk: inboundDidSource e request_uri (implicit), to_header, sau header cu inboundDidHeader care îl numește. Carrier-ii nu sunt de acord între ei pe subiectul ăsta mai mult decât pe oricare altul.
Cele patru endpoint-uri de test
Există pentru că „trunk-ul e configurat" și „trunk-ul funcționează" sunt afirmații diferite. Fii precis cu privire la ce afirmație susține fiecare.
| Endpoint | Ce face de fapt |
|---|---|
POST .../test/options | Trimite un SIP OPTIONS către fiecare gateway și înregistrează răspunsul. Un pachet real pe fir. |
POST .../test/register | Încearcă un REGISTER și înregistrează rezultatul, inclusiv motivul dat de carrier la eșec. Un pachet real pe fir. |
POST .../test/inbound | O verificare de pregătire a configurației, nu un apel. Verifică dacă trunk-ul permite intrarea, dacă ACL-ul nu e gol-și-obligatoriu, și dacă există o mapare de DID sau o destinație de rezervă. Raportează last_inbound_at dacă a sosit vreodată un apel, iar altfel o spune în atâtea cuvinte: „No inbound call has arrived on this trunk yet — place one to confirm end to end." |
POST .../test/outbound | Primește o destination și încearcă un apel real prin trunk. |
GET .../diagnostics întoarce rezultatele înregistrate, cele mai noi primele. GET .../config întoarce XML-ul Sofia generat, per gateway, plus fragmentul de ACL, ca să poți face diff înainte să aplici.
Cum aplici configurația generată
Scrierea XML-ului și reîncărcarea lui în FreeSWITCH e php artisan callagent:sip:apply. php artisan callagent:sip:options-probe trimite OPTIONS către fiecare gateway activ, pe schedule, și înregistrează rezultatul.
Request-uri SIP în dialog
Tools-urile sipRequest trimit un request în dialog în timpul apelului — supervizare specifică unui carrier, sau un transfer cu headere custom. Vezi Tools.
Cum îl dovedești fără un carrier
Repository-ul livrează un peer de test. telephony/sip-peer/ e un container Kamailio care se comportă ca un carrier pe două socketuri — autentificare digest pe 5060, autentificare pe IP pe 5062 — în spatele profilului de compose sip-lab.
./telephony/sip-peer/prove.shDovedește cinci lucruri pe socketuri reale: un REGISTER provocat care reușește; o parolă greșită care eșuează cu 401 și scoate motivul pe rândul trunk-ului; un INVITE admis de la o adresă permisă; un INVITE refuzat cu 403 de la una care nu e; și reînregistrarea înainte de expirare.
Nu poate dovedi ce poate dovedi doar un carrier: digest SHA-256, TLS și SRTP pe un SBC real, ciudățenii de NAT și de keepalive, DID-uri reale, prezentarea caller ID-ului, facturarea sau tratarea 302/Diversion. Și nu poate dovedi audio-ul, din motivul de la începutul paginii.
API
GET/POST /api/v1/telephony/sip-trunks
GET/PATCH/DELETE /api/v1/telephony/sip-trunks/{id}
POST /api/v1/telephony/sip-trunks/{id}/test/options
POST /api/v1/telephony/sip-trunks/{id}/test/register
POST /api/v1/telephony/sip-trunks/{id}/test/inbound
POST /api/v1/telephony/sip-trunks/{id}/test/outbound
GET /api/v1/telephony/sip-trunks/{id}/diagnostics
GET /api/v1/telephony/sip-trunks/{id}/config
GET/POST /api/v1/telephony/sip-trunks/{trunk}/gateways
PATCH/DELETE /api/v1/telephony/sip-trunks/{trunk}/gateways/{id}
POST /api/v1/telephony/sip-trunks/{trunk}/gateways/{id}/test
GET/POST /api/v1/telephony/sip-trunks/{trunk}/dids
PATCH/DELETE /api/v1/telephony/sip-trunks/{trunk}/dids/{id}Parametri și răspunsuri complete: trunk-uri SIP, gateways, DID-uri.