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

6.7 KiB

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:

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 jsonnã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

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"