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
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-configagora é alcançado viahttp://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) usamhost.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_internaljá 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.