Files
B2BCall-dialer/docs/FREESWITCH.md
Matheus 97ef8a6ba8 feat: edição de rotas de entrada, fix de 2 bugs reais no ESL, diagnóstico de NAT/áudio e softphone WebRTC (PHASE 65/66)
Três achados reportados pelo usuário numa mensagem só: (1) Rotas de Entrada
não tinha edição depois de criada — implementada no mesmo padrão de Filas;
(2) telas de Platform > Infraestrutura sempre davam "Timeout no ESL" nesta
VM — não era limitação permanente como o comentário antigo dizia, e sim
ESL_HOST=freeswitch (nome DNS que só existe dentro da rede do Docker) mais
um segundo bug independente (`show gateways as json` não é comando válido
nesta versão do FreeSWITCH); (3) ramal externo registrava mas sem áudio —
diagnosticado com contadores de pacote do iptables: a VM está atrás de um
roteador sem port-forward pra faixa de RTP, achado de infraestrutura de
rede, não bug de código.

Também integra o softphone WebRTC (handphone.js/OpenSIPS, já em produção):
código-fonte encontrado em git.falehandix.com.br/Handix/handphone-2.0,
patch mínimo pra aceitar o endereço do proxy em runtime (era build-time),
nova config global (Platform > Infraestrutura > Softphone WebRTC) e widget
na topbar do tenant que pega usuário/domínio/senha do ramal vinculado ao
agente logado.

Adiciona docs/QA_SETUP.md — runbook completo pra subir o ambiente do zero
numa máquina nova (Docker, migrations, seed, systemd), e completa o
.env.example que estava faltando a maioria das variáveis reais.

Testado ponta a ponta com Playwright: edição de rota (criar/editar/F5),
as 3 telas de Infraestrutura com dado real, e um tenant/ramal/agente de
teste criados na hora confirmando que o script do softphone recebe as
credenciais certas.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
2026-08-30 22:14:39 -03:00

133 lines
6.7 KiB
Markdown

# FreeSWITCH
Imagem própria em `infrastructure/freeswitch/` (agente.md secao 19), rodando como
serviço Docker `freeswitch` (container `b2bcall-freeswitch`).
## Por que pacotes prontos, e não compilar da fonte
O servidor de desenvolvimento tem só ~1.9GB de RAM. Compilar FreeSWITCH (C/C++,
dezenas de módulos) arriscaria OOM e levaria muito tempo. Em vez disso, a imagem
usa os pacotes `.deb` pré-compilados do repositório oficial do SignalWire
(`freeswitch.signalwire.com`), autenticado com `FREESWITCH_PAT` — exatamente o
uso previsto para essa credencial (agente.md secao 11). O repositório do
SignalWire tem pacotes para o codename `trixie`, então a imagem usa
`debian:trixie-slim` como base (mesma versão do host, mas isso é coincidência —
o container é isolado e poderia usar qualquer Debian suportado pelo repo).
## Manuseio do PAT (agente.md secao 11)
`FREESWITCH_PAT` entra no build via **Docker BuildKit secret**
(`--mount=type=secret,id=freeswitch_pat`), nunca como `ARG`/`ENV`. O arquivo de
credenciais do apt (`/etc/apt/auth.conf.d/freeswitch.conf`, que contém o PAT em
texto puro) é criado e apagado dentro do **mesmo** `RUN`, então nunca aparece em
nenhuma camada da imagem final — verificado com `docker history --no-trunc`.
`docker-compose.yml` declara o secret assim:
```yaml
secrets:
freeswitch_pat:
environment: FREESWITCH_PAT
```
e o serviço `freeswitch` referencia `secrets: [freeswitch_pat]` em `build:`.
## Pacotes instalados
```
freeswitch-meta-vanilla # core + config de referência (a mesma usada em
# praticamente todo tutorial/livro de FreeSWITCH)
freeswitch-conf-vanilla # ⚠ Recommends de meta-vanilla, não Depends —
# precisa ser listado explicitamente com
# --no-install-recommends (foi um bug real
# durante o setup: sem isso /etc/freeswitch
# fica vazio e o container entra em crash loop)
freeswitch-mod-callcenter # ACD (agente.md secao 37)
freeswitch-mod-avmd # detecção de caixa postal/beep (secao 87)
freeswitch-mod-curl # chamadas HTTP a partir do dialplan
```
`mod_xml_curl` está **instalado mas desativado** em
`overrides/autoload_configs/modules.conf.xml` — o módulo se recusa a carregar
sem pelo menos um binding com `gateway-url` configurada ("Binding has no
url!"), e essa URL só existirá quando o `b2bcall-fs-config` for criado (fase
"XML Curl", logo em seguida). Reativar lá.
`mod_signalwire` foi removido da lista de módulos: é específico da nuvem do
SignalWire, que não usamos (só o repositório de pacotes).
## Event Socket (agente.md secao 22)
Senha alterada da padrão (`ClueCon`) para `${ESL_PASSWORD}` (gerado com
`openssl rand`, vive só em `.env`) via `entrypoint.sh`, que faz um `sed` no
`event_socket.conf.xml` **em runtime** — a senha real nunca é copiada para a
imagem, só injetada via variável de ambiente do container. O entrypoint falha
alto (`set -eu` + `${ESL_PASSWORD:?...}`) se a variável não estiver definida —
nunca sobe com a senha padrão por engano.
Porta 8021 **nunca publicada pra rede** — só em loopback do próprio host
(`"127.0.0.1:8021:8021"` no `docker-compose.yml`, desde a PHASE 65, ver seção
abaixo). Isso satisfaz "NÃO pública... acesso somente pela aplicação
autorizada" (agente.md secao 22) sem abrir a porta pra ninguém além do próprio
host.
## Achados na sessão de PHASE 65 (Platform > Infraestrutura)
Duas telas de `platform/infraestrutura` (FreeSWITCH, SIP Profiles, Nodes)
sempre mostravam "Timeout conectando no ESL do FreeSWITCH" nesta VM. A
suposição registrada até então (agora corrigida) era que isso seria uma
limitação permanente: "`apps/api` roda fora do Docker, a porta nunca é
publicada, então este endpoint sempre falha aqui". Isso estava incompleto.
**Causa real**: `ESL_HOST=freeswitch` no `.env` — um nome DNS que só resolve
dentro da rede interna do Docker Compose (embedded DNS), nunca a partir do
resolver do próprio host (confirmado: `getent hosts freeswitch` falha no host
com exit code 2). Não era uma questão de porta publicada — testado e
confirmado que o **host sempre alcança o IP de qualquer container na rede
bridge do Docker diretamente** (`docker inspect` + `/dev/tcp` bateram), mesmo
sem nenhuma porta publicada. Só outras *máquinas* são bloqueadas sem
`ports:` — o host nunca foi.
**Fix**: publicar 8021 só em loopback (`127.0.0.1:8021:8021`) e trocar
`ESL_HOST` pra `127.0.0.1` no `.env` — resolve o DNS sem abrir a porta pra
rede nenhuma (confirmado via `ss -tlnp`: `docker-proxy` escutando em
`127.0.0.1`, não `0.0.0.0`). Os serviços que rodam DENTRO do Docker
(`fs-events`, `fs-config`, `predictive-dialer`) continuam com
`ESL_HOST=freeswitch` fixo no próprio `docker-compose.yml` — não dependem do
`.env` pra isso, então a troca não afeta eles.
**Segundo bug, achado na mesma investigação**: a tela "Nodes" continuava
mostrando `error: ""` (string vazia) mesmo depois do fix acima.
`getGateways()` (`packages/telephony/src/freeswitch-provider.ts`) rodava
`show gateways as json`**não é um sub-comando válido** de `show` nesta
versão do FreeSWITCH (`fs_cli -x "show gateways as json"` devolve
`-USAGE: codec|endpoint|application|...`). O pacote `esl` trata qualquer
reply começando com `-` como erro de protocolo e rejeita a promise com um
`FreeSwitchError` cujo `.message` pode vir vazio — daí o `""` na tela em vez
de uma mensagem útil. Fix: `getGateways()` agora roda `sofia status` (mesmo
comando já usado pela tela de SIP Profiles) — gateways aparecem como linhas
`type=gateway` no texto puro, sem variante JSON dedicada nesta versão.
## O que NÃO foi feito nesta fase (fica para as próximas, por design)
Seguindo a ordem do próprio `agente.md` (secao 232): esta fase só cobre "ter o
FreeSWITCH rodando e alcançável". As próximas fases constroem em cima:
- **Event Socket**: `b2bcall-fs-events`, conexão ESL permanente (secao 21).
- **XML Curl**: `b2bcall-fs-config`, reativa `mod_xml_curl` apontando pra esse
serviço (secao 26).
- **Extensions/Trunks/Dialplan**: hoje o directory/dialplan estático da config
vanilla continua com os 20 ramais de teste (1000-1019, senhas fracas — README
do próprio pacote avisa isso). Como as portas SIP não estão publicadas no
host, isso fica contido, mas precisa ser substituído por `mod_xml_curl`
dinâmico antes de qualquer tronco/ramal real existir.
- **mod_odbc_cdr**: não instalado ainda — só faz sentido junto da fase de CDR.
## Verificação manual
```bash
source <(grep '^ESL_PASSWORD=' .env)
docker exec b2bcall-freeswitch fs_cli -p "$ESL_PASSWORD" -x "status"
docker exec b2bcall-freeswitch fs_cli -p "$ESL_PASSWORD" -x "module_exists mod_sofia"
```