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

68
TODO.md
View File

@@ -2456,6 +2456,74 @@ rota de entrada")
os dois persistiram com o tipo certo, sobrevivendo a um reload
completo da página.
## PHASE 68 — Ramal registrado mas chamada não completava / sem áudio
(reportado pelo usuário testando com 2 telefones IP reais, 1502 e 1503)
- [x] **Achado 1 (chamada não completava, 503 instantâneo)**: os dois
ramais registravam com o Contact apontando pro MESMO IP público
compartilhado desta rede (esta VM nunca tem IP público próprio —
é sempre RFC1918 atrás do NAT do escritório). Ao originar uma
chamada, o FreeSWITCH tentava mandar o INVITE de volta pra esse
IP público — hairpin NAT clássico, o roteador do escritório não
sabe refletir esse tráfego de volta pra dentro. Corrigido com
`NDLB-received-in-nat-reg-contact` (Dockerfile) — o Contact salvo
passa a ser o IP/porta REALMENTE observados no pacote (a rede
interna), não o que o telefone auto-relata.
- [x] `network_mode: host` no serviço `freeswitch` (pedido do usuário,
"sempre é problema NAT do Docker") — tira a camada de NAT/bridge
do Docker do meio. Quebra em cascata corrigida: FreeSWITCH sai da
rede bridge do compose, então (a) não resolve mais os OUTROS
containers pelo nome de serviço — `fs-config` virou
`http://127.0.0.1:8080/` (porta publicada só em loopback,
xml_curl.conf.xml) —, e (b) os workers que chamavam ESL via nome
`freeswitch` (fs-events/fs-config/predictive-dialer) passam a usar
`host.docker.internal` (+ `extra_hosts: host-gateway`, 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.
- [x] **Achado 2 (chamada completava, mas sem áudio)**: com o registro
corrigido, a ligação passou a completar, mas sem RTP (confirmado
pelo usuário com `sngrep`). `local-network-acl="localnet.auto"`
(padrão vanilla) só cobre a subnet da PRÓPRIA interface do
FreeSWITCH — os ramais reais ficam em OUTRAS subnets do mesmo
escritório, então eram tratados como "de fora" e o SDP trocava o
RTP/SIP anunciado pelo IP público compartilhado de novo (mesmo
hairpin, agora na mídia). Corrigido com uma ACL própria
(`b2bcall_lan`, cobre todo RFC1918 — nunca alcançável de verdade
"de fora" nesta VM) referenciada em `local-network-acl`.
- [x] **Achado 3 (chamada completava e tinha áudio, mas morria sozinha
em exatos 32s)**: mesmo com áudio já corrigido, toda chamada
morria sozinha depois de exatos 32 segundos — o Timer H do
RFC 3261 (64×T1, prazo padrão de qualquer UAS esperando o ACK de
um 2xx). Só descoberto de verdade com captura de pacote real
(`tcpdump`, instalado nesta sessão — não estava no host): o
`200 OK` que o FreeSWITCH manda pro ramal que RECEBEU a chamada
ainda tinha `Contact: sip:...@191.240.175.55:5060` (o IP público
de novo, MESMO com o SDP já mostrando o IP certo) — o telefone
nunca manda o ACK de volta (não tem como chegar por ali), o
FreeSWITCH retransmite o 200 OK sozinho (0.5s/1s/2s/4s/4s...) até
desistir e derrubar a chamada. `apply-nat-acl`/`local-network-acl`
(achados 1/2) não bastam aqui — são checagens independentes, e
trocar `apply-nat-acl` pra escopo `b2bcall_lan` (testado) não
mudou nada nesse Contact. O fix de verdade: `ext-rtp-ip`/
`ext-sip-ip` (STUN, resolvem pro IP público) REMOVIDOS do profile
`internal` (Dockerfile) — sem endereço "externo" nenhum
configurado, o FreeSWITCH não tem mais como escolher errado,
cai sempre em `rtp-ip`/`sip-ip` (10.10.32.138). Único profile
afetado; `external` (reservado pra um trunk PSTN real, nunca
usado ainda) mantém os dois params intactos.
- [x] Diagnosticado ao vivo lendo `/var/log/freeswitch/freeswitch.log`
em tempo real durante as tentativas de chamada do usuário (nunca
supondo causa sem ver o log) e, pro achado 3, uma captura de
pacote real via `tcpdump` — cada achado confirmado por evidência
exata (`NORMAL_TEMPORARY_FAILURE`/503 instantâneo pro achado 1;
endereço do `c=`/RTP-IP pro achado 2; cabeçalho `Contact:` do
`200 OK` capturado byte a byte pro achado 3). Confirmado resolvido
pelo usuário com chamadas reais nos dois sentidos (1502→1503 e
1503→1502), áudio bidirecional e chamada sobrevivendo bem além
dos 32s que travavam toda tentativa antes do achado 3.
---
## Riscos conhecidos