feat: implement Trunks with real FreeSWITCH gateway sync
- 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
This commit is contained in:
93
docs/TRUNKS.md
Normal file
93
docs/TRUNKS.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user