Files
B2BCall-dialer/docs/NETWORK_ARCHITECTURE.md
Matheus 2134fa0aa4 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
2026-08-31 11:50:51 -03:00

4.5 KiB

Arquitetura de Rede

Estado atual (PHASE 68 — network_mode: host)

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.

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.

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.

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).

Decisão tomada: network_mode do FreeSWITCH (agente.md secao 19)

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

Esta VM de laboratório está atrás de um roteador (NAT) — o IP da interface de rede (ens18) é privado (10.10.32.x), o roteador é quem faz NAT pro IP público real. Reportado pelo usuário: um softphone externo registrava com sucesso, mas sem áudio.

Diagnóstico (contadores de pacotes do iptables, não suposição): 10 pacotes chegaram em 5060/udp (SIP), zero pacotes chegaram em qualquer porta da faixa de RTP (16384-16584/udp). O REGISTER funciona porque a resposta trafega de volta pelo mesmo "buraco" NAT que o próprio pacote do cliente abriu — mas RTP usa portas completamente diferentes, como um fluxo NOVO. Sem uma regra de port forward no roteador pra essa faixa (apontando pro IP privado desta VM), esses pacotes nunca chegam até o Docker/FreeSWITCH.

Do lado desta VM estava tudo certo: docker-compose.yml publica a faixa de RTP pra rede (não só loopback), Ext-RTP-IP/Ext-SIP-IP resolvem certo via STUN (sofia status profile internal mostra o IP público real), e a detecção de NAT do próprio FreeSWITCH (apply-nat-acl value="nat.auto", nat.auto é uma ACL auto-gerada pelo core do FreeSWITCH — RFC1918 exceto a própria rede local do container, não precisa existir em acl.conf.xml) funciona corretamente (testado direto via fs_cli -x "acl <ip> nat.auto").

Não tem fix de código pra isso — é infraestrutura de rede fora do controle desta aplicação. Checklist pra quem for expor telefonia real atrás de um roteador: encaminhar UDP 5060 e toda a faixa RTP (16384-16584, Dockerfile do FreeSWITCH) pro IP privado da VM, sem tradução de porta.

Quando apps/api virar container

Hoje ela roda no host por conveniência de desenvolvimento. Quando virar o serviço Docker b2bcall-api (agente.md secao 14), as connection strings precisam trocar de localhost pros hostnames internos do compose (postgres, redis, freeswitch) — ver nota em docs/AUTHENTICATION.md sobre essa pegadinha.