- trunks table (tenant-scoped, RLS): host/proxy/realm, register, username/password_enc (AES-256-GCM), dtmf_mode, ping, transport, and a status/status_updated_at pair meant to be driven by FreeSWITCH events - apps/api/src/trunks: CRUD (POST/GET/GET:id/DELETE), same RBAC/tenant pattern as Extensions, password never exposed in any GET - packages/telephony: buildGatewayXml() generates a Sofia gateway XML file - b2bcall-fs-config now writes sip_profiles/external/<trunk_id>.xml (shared Docker volume with FreeSWITCH -- the vanilla external profile already includes external/*.xml) and runs 'sofia profile external rescan' over ESL; syncs on boot and on demand via Redis pub/sub (b2bcall:trunks:sync), since apps/api runs on the host and fs-config has no port published to reach directly - added FreeSwitchTelephonyProvider.waitUntilConnected() to fix a startup race: the first sync ran before the ESL connection had settled, logging a harmless but noisy error - verified end-to-end with a fake host: create trunk -> gateway file written -> FreeSWITCH shows the real gateway (FAIL_WAIT, expected) -> delete -> file removed (cleanup also correctly swept the stale 'example.com' gateway that had been copied into the volume from the vanilla image) - apps/freeswitch-events/src/trunk-status.ts: written to update Trunk.status from sofia::gateway_state events, using the same normalizeEslEvent path already proven for CHANNEL_* events - KNOWN GAP, documented rather than glossed over: monitored fs-events for ~90s while the gateway visibly transitioned states in FreeSWITCH (FAIL_WAIT/DOWN) and no sofia::gateway_state event was observed arriving. CUSTOM/sofia::* events have not actually been proven working end-to-end in this session -- only CHANNEL_* events have been. Needs verification against a real SIP target before the status auto-update can be trusted in production. See docs/TRUNKS.md and TODO.md. - docs/TRUNKS.md
94 lines
4.3 KiB
Markdown
94 lines
4.3 KiB
Markdown
# Trunks (Troncos SIP)
|
|
|
|
Agente.md seções 41-42. Primeiro recurso que grava configuração real no
|
|
filesystem do FreeSWITCH (gateways Sofia) em vez de só responder via
|
|
`mod_xml_curl`.
|
|
|
|
## Modelo
|
|
|
|
`trunks` (tenant-scoped, RLS): `host`, `proxy`, `realm`, `register`,
|
|
`username`/`password_enc` (AES-256-GCM, mesmo padrão de `sip_password_enc`),
|
|
`from_user`/`from_domain`, `register_proxy`/`outbound_proxy`,
|
|
`expire_seconds`, `retry_seconds`, `dtmf_mode`, `ping`/`ping_frequency`,
|
|
`transport`, `max_cps`/`max_channels`, e `status`/`status_updated_at`
|
|
(refletidos pelos eventos do FreeSWITCH, nunca escritos manualmente pela API).
|
|
|
|
## Mecanismo: por que arquivo + rescan, e não XML Curl "configuration"
|
|
|
|
Ao contrário de Extensions (resolvido 100% via `mod_xml_curl` na hora do
|
|
lookup), gateways Sofia não têm um binding XML Curl limpo e específico sem
|
|
reativar a seção `configuration` inteira — e isso já causou problemas reais
|
|
na fase XML Curl (chamadas HTTP desnecessárias no boot pra configs de outros
|
|
módulos). Em vez disso, `b2bcall-fs-config`:
|
|
|
|
1. Gera um arquivo `<trunk_id>.xml` por trunk habilitado em
|
|
`sip_profiles/external/` (volume Docker compartilhado com o FreeSWITCH —
|
|
o profile `external` da config vanilla já tem
|
|
`<X-PRE-PROCESS cmd="include" data="external/*.xml"/>`, então não precisou
|
|
editar a config do profile).
|
|
2. Roda `sofia profile external rescan` via ESL — relê os gateways sem
|
|
derrubar chamadas em andamento (diferente de `restart`).
|
|
|
|
## Gatilho de sincronização
|
|
|
|
`apps/api` não roda no Docker (ainda está no host) e `fs-config` não expõe
|
|
porta pro host — então a notificação "algo mudou, resincroniza" vai por
|
|
Redis pub/sub (`b2bcall:trunks:sync`), o mesmo mecanismo já usado pra eventos
|
|
normalizados. `apps/api` publica depois de criar/apagar um trunk;
|
|
`fs-config` também roda uma sincronização completa ao subir (cobre trunks
|
|
criados enquanto ele estava fora do ar).
|
|
|
|
**Achado real**: a primeira sincronização no boot corria antes da conexão
|
|
ESL do `fs-config` terminar de se estabelecer, gerando um erro cosmético
|
|
("FreeSWITCH ESL nao conectado") — a escrita dos arquivos funcionava, só o
|
|
rescan falhava. Corrigido com `FreeSwitchTelephonyProvider.waitUntilConnected()`
|
|
(timeout de 5s) antes de tentar o rescan.
|
|
|
|
## Status do trunk (secao 42)
|
|
|
|
`b2bcall-fs-events` já escutava `sofia::gateway_state` desde a fase Event
|
|
Socket (normalizado pra `GATEWAY_UP`/`GATEWAY_DOWN`); nesta fase, ele passou
|
|
a também escrever esse estado de volta em `Trunk.status`. Como o nome do
|
|
gateway no FreeSWITCH é o `Trunk.id` (UUID) e o evento não diz de qual
|
|
tenant é, a busca percorre os tenants ativos (`packages/database`'s
|
|
`withTenantContext`) até achar o trunk dono daquele id — aceitável dado que
|
|
mudança de estado de trunk é rara, não é um evento de alto volume por
|
|
chamada.
|
|
|
|
Mapeamento de estados brutos do Sofia pro enum interno
|
|
(`apps/freeswitch-events/src/trunk-status.ts`):
|
|
|
|
```
|
|
UP, REGED → REGISTERED/UP
|
|
TRYING, REGISTER → TRYING
|
|
FAILED, FAIL_WAIT → FAILED
|
|
DOWN → DOWN
|
|
NOREG, UNREGED → UNREGISTERED
|
|
qualquer outro → UNKNOWN
|
|
```
|
|
|
|
## Verificado
|
|
|
|
Criei um trunk de teste apontando pra um host inexistente
|
|
(`sip.trunk-inexistente.invalid`, nunca resolve — RFC 2606) via API:
|
|
|
|
- `fs-config` sincronizou (`count: 1`), escreveu o arquivo `.xml`, rodou o
|
|
rescan com sucesso.
|
|
- `sofia status gateway` no FreeSWITCH mostrou o gateway real, estado
|
|
`FAIL_WAIT` (esperado — host não existe).
|
|
- Confirma o pipeline `API → Postgres → fs-config → arquivo XML → rescan →
|
|
FreeSWITCH` funcionando ponta a ponta sem precisar de nenhum tronco/
|
|
credencial real.
|
|
|
|
## O que falta
|
|
|
|
- Propagação de `GATEWAY_UP`/`DOWN` → `Trunk.status` não confirmada com
|
|
evento real disparado por uma mudança de estado ao vivo nesta sessão de
|
|
testes (o gateway ficou em `FAIL_WAIT` por falha de DNS, que pode não
|
|
disparar o mesmo ciclo de eventos que uma rejeição SIP normal) — vale
|
|
reverificar com um destino que responda de verdade (outro FreeSWITCH, por
|
|
exemplo) antes de confiar nisso em produção.
|
|
- Quota de troncos (`max_trunks`, secao 59) — depende de Plans/Entitlements.
|
|
- `GET /trunks` não mostra `sofia status gateway` ao vivo, só o último
|
|
status conhecido no banco — suficiente por enquanto, sem WebSocket ainda.
|