fix(freeswitch): 3 achados reais de NAT entre ramais no mesmo escritório — registro, áudio e chamada morrendo em 32s (PHASE 68)

Usuário reportou que 1502 ligando pra 1503 (dois telefones IP reais,
ambos registrados) não completava a chamada. Três bugs em camadas
diferentes, cada um só confirmado lendo o log/capturando pacote real —
nunca por suposição:

1. Registro: os dois ramais registravam com Contact apontando pro MESMO
   IP público compartilhado desta rede (a VM nunca tem IP público
   próprio, é sempre RFC1918 atrás do NAT do escritório) — originar uma
   chamada tentava mandar o INVITE de volta pra esse IP público, hairpin
   NAT clássico, falha instantânea (503). Fix: NDLB-received-in-nat-reg-
   contact (Contact salvo vira o IP realmente observado no pacote).

   De quebra, aplicado network_mode: host no serviço freeswitch (pedido
   explícito do usuário) — tira o Docker NAT/bridge do meio. Quebra em
   cascata corrigida: fs-config vira alcançável só via 127.0.0.1:8080
   (não mais nome de serviço), workers ESL (fs-events/predictive-dialer)
   passam a usar host.docker.internal.

2. Áudio: com o registro corrigido, a chamada completava mas sem RTP —
   local-network-acl="localnet.auto" só cobria a subnet da própria
   interface do FreeSWITCH, tratando ramais de OUTRAS subnets do mesmo
   escritório como "de fora" e trocando o SDP pelo IP público de novo.
   Fix: ACL própria (b2bcall_lan, cobre todo RFC1918) referenciada em
   local-network-acl.

3. Chamada morrendo sozinha em exatos 32s (Timer H do RFC 3261): mesmo
   com áudio ok, o 200 OK que o FreeSWITCH manda pro ramal que recebeu a
   chamada ainda tinha Contact com o IP público — o telefone nunca manda
   o ACK de volta, FreeSWITCH retransmite sozinho até desistir. Só
   confirmado com tcpdump (instalado nesta sessão) capturando o pacote
   byte a byte. Fix: ext-rtp-ip/ext-sip-ip (STUN, sempre resolvem pro IP
   público) removidos do profile "internal" — sem endereço "externo"
   configurado, o FreeSWITCH nunca mais tem como escolher errado.

Confirmado resolvido pelo usuário com chamadas reais nos dois sentidos,
áudio bidirecional, sobrevivendo bem além dos 32s que travavam antes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
This commit is contained in:
2026-08-31 11:50:51 -03:00
parent 432cd55adb
commit 2134fa0aa4
6 changed files with 290 additions and 47 deletions

View File

@@ -1,31 +1,47 @@
# Arquitetura de Rede
## Estado atual (fase FreeSWITCH inicial)
## Estado atual (PHASE 68 — `network_mode: host`)
`freeswitch` roda na rede padrão do Docker Compose (bridge, `b2bcall_default`),
igual a `postgres` e `redis`. Nenhuma porta é publicada no host — nem 8021
(ESL), nem SIP (5060/5080), nem RTP. Isso é intencional: ainda não existe
nenhum tronco SIP real nem ramal externo, então não há motivo pra expor nada.
`freeswitch` roda em `network_mode: host` — decisão tomada na PHASE 68 (era
"pendente" antes, ver seção abaixo), pedida pelo usuário depois de dois
ramais reais no mesmo escritório não conseguirem se ligar. `postgres`/
`redis`/`fs-config` continuam na rede bridge padrão do compose
(`b2bcall_default`); só o FreeSWITCH saiu dela.
`apps/api` roda hoje **direto no host** (fora do Docker), então usa
`APP_DATABASE_URL`/`REDIS_URL` apontando pra `localhost` nas portas publicadas
pelo Postgres/Redis. Ela não consegue (nem precisa, ainda) alcançar o
FreeSWITCH.
Com host networking, o FreeSWITCH usa a stack de rede do PRÓPRIO host
diretamente — sem NAT/bridge do Docker no meio, `local_ip_v4` resolve pro IP
real da interface (`10.10.32.138` nesta VM), SIP/ESL escutam direto nas
interfaces do host. Isso elimina uma camada de tradução de endereço que
existia antes de qualquer roteador físico entrar em cena — ver PHASE 68 no
TODO.md pro relato completo dos 3 achados (registro, áudio, e a chamada
morrendo sozinha em 32s) que motivaram e validaram essa mudança.
## Decisão pendente: `network_mode` do FreeSWITCH (agente.md secao 19)
Efeitos em cascata (o FreeSWITCH sai da rede bridge do compose):
- Não resolve mais os outros containers pelo nome de serviço — `fs-config`
agora é alcançado via `http://127.0.0.1:8080/` (porta publicada só em
loopback, `xml_curl.conf.xml`).
- Os workers que chamavam ESL via nome `freeswitch`
(fs-events/fs-config/predictive-dialer) usam `host.docker.internal` (+
`extra_hosts: host-gateway`, exige Docker 20.10+).
- Segurança do Event Socket deixa de depender de "publicar só em loopback"
(não existe mais essa camada) e passa a depender inteiramente da ACL
`b2bcall_internal` já configurada (`apply-inbound-acl`) — confirmado que
continua rejeitando qualquer origem fora de loopback/rede do Docker.
Quando existir um tronco SIP real (fase Trunks), será preciso decidir entre:
`apps/api` continua rodando **direto no host** (fora do Docker) — nada muda
pra ela, `ESL_HOST=127.0.0.1` já funcionava e continua funcionando (agora de
forma ainda mais direta, sem a camada de publish-port do Docker no meio).
- **`network_mode: host`**: mais simples pra SIP/RTP (sem NAT entre o
container e a rede), mas perde isolamento de rede do Docker.
- **macvlan/ipvlan**: dá ao FreeSWITCH um IP próprio na rede física, sem expor
outros serviços do host: mais trabalho de configurar, melhor isolamento.
## Decisão tomada: `network_mode` do FreeSWITCH (agente.md secao 19)
Não decidido ainda — só vira relevante quando houver um carrier/SBC real pra
conectar (secao 17: "FreeSWITCH não deverá depender de IP SIP público" — a
topologia esperada é `Internet → OpenSIPS → rede SIP privada → FreeSWITCH`,
então o FreeSWITCH em si tende a ficar em rede privada mesmo, o que favorece
manter bridge/macvlan em vez de host).
Historicamente listada como pendente (macvlan/ipvlan vs. host), resolvida na
PHASE 68 a favor de **`network_mode: host`** — não por causa de um trunk
PSTN real (a topologia `Internet → OpenSIPS → rede SIP privada →
FreeSWITCH` da secao 17 continua valendo pra isso), mas por um problema
concreto de NAT entre dois ramais registrados no mesmo escritório (ver
achado abaixo). `macvlan`/`ipvlan` continuam como alternativas válidas se um
dia o isolamento de rede do host virar prioridade maior que a simplicidade
atual — não implementadas, não descartadas.
## Achado real (PHASE 65): VM atrás de roteador, ramal externo sem áudio