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

@@ -78,6 +78,83 @@ RUN sed -i \
/etc/freeswitch/sip_profiles/internal.xml \
&& ! grep -q "force-register-domain\|force-subscription-domain\|force-register-db-domain" /etc/freeswitch/sip_profiles/internal.xml
# Hairpin NAT entre dois ramais atrás do MESMO roteador (PHASE 68, achado
# real reportado pelo usuário: "1502 liga pra 1503 e não completa" —
# ambos registrados, mas a chamada falhava em 0ms com 503). Confirmado
# lendo `sofia status profile internal reg`: os dois ramais registram com
# Contact apontando pro MESMO IP público desta rede (a VM nunca tem IP
# público próprio — secao "essa máquina nunca terá IP público" — telefone
# e servidor share o mesmo NAT do escritório). `apply-nat-acl` (nat.auto,
# já testado funcionando) SÓ detecta que o cliente está atrás de NAT; sem
# `NDLB-received-in-nat-reg-contact`, o Contact salvo continua sendo o que
# o telefone auto-relatou (o IP público da rede) em vez do IP realmente
# observado no pacote UDP (a rede interna, ex.: 192.168.40.165) — originar
# uma chamada nova pro Contact salvo então tenta mandar o INVITE de volta
# pro IP público, que o roteador do escritório não sabe refletir de volta
# pra dentro (hairpin NAT clássico). Ligando este parâmetro, o Contact
# salvo passa a ser o IP/porta REALMENTE observados — a chamada entre dois
# ramais no mesmo escritório passa a rotear direto pela rede interna,
# nunca saindo (e voltando) pelo roteador.
RUN sed -i \
-e 's#<!-- *<param name="NDLB-received-in-nat-reg-contact" value="true"/> *-->#<param name="NDLB-received-in-nat-reg-contact" value="true"/>#' \
/etc/freeswitch/sip_profiles/internal.xml \
&& grep -q '<param name="NDLB-received-in-nat-reg-contact" value="true"/>' /etc/freeswitch/sip_profiles/internal.xml
# Continuação do achado acima: registro corrigido, mas a CHAMADA
# completava sem áudio. `local-network-acl="localnet.auto"` (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 (mesmo hairpin de antes, agora na mídia). `b2bcall_lan`
# (acl.conf.xml, cobre todo RFC1918 — nunca alcançável de verdade "de
# fora" nesta VM, que nunca tem IP público próprio) resolve isso.
RUN sed -i \
-e 's#<param name="local-network-acl" value="localnet.auto"/>#<param name="local-network-acl" value="b2bcall_lan"/>#' \
/etc/freeswitch/sip_profiles/internal.xml \
&& grep -q '<param name="local-network-acl" value="b2bcall_lan"/>' /etc/freeswitch/sip_profiles/internal.xml
# Terceira camada do mesmo achado: com áudio corrigido, a chamada ainda
# morria sozinha em exatos 32s (Timer H do RFC 3261) — o 200 OK que o
# FreeSWITCH manda pro ramal que RECEBEU a chamada tinha
# `Contact: sip:...@191.240.175.55:5060` (IP público de novo), então o
# telefone nunca manda o ACK de volta (não tem como voltar por ali) e o
# FreeSWITCH fica retransmitindo o 200 OK sozinho até desistir. Achado
# via captura de pacote real (tcpdump), não suposição.
#
# `b2bcall_external_nat` (acl.conf.xml, sentido INVERTIDO de
# `b2bcall_lan`: só marca como NAT'd quem está de verdade fora de todo
# RFC1918 conhecido) escopa `apply-nat-acl` corretamente pra esta
# implantação — mantido por ser mais correto e não quebrar o fix de
# registro acima (testado: `NDLB-received-in-nat-reg-contact` não
# depende de `apply-nat-acl` bater, continua funcionando mesmo sem o
# `fs_nat=yes` marcado). MAS isto sozinho não resolveu o Contact do
# 200 OK — confirmado com uma segunda captura de pacote depois desta
# mudança, o Contact continuou com o IP público.
RUN sed -i \
-e 's#<param name="apply-nat-acl" value="nat.auto"/>#<param name="apply-nat-acl" value="b2bcall_external_nat"/>#' \
/etc/freeswitch/sip_profiles/internal.xml \
&& grep -q '<param name="apply-nat-acl" value="b2bcall_external_nat"/>' /etc/freeswitch/sip_profiles/internal.xml
# O fix de verdade pro Contact: `ext-rtp-ip`/`ext-sip-ip` (STUN,
# `$${external_sip_ip}` = 191.240.175.55) fazem FreeSWITCH sempre ter
# um endereço "público" pronto pra usar como identidade própria em
# respostas SIP — independente de qualquer ACL de NAT, é só o valor
# que o profile usa quando "se apresentando" externamente. Esta VM
# NUNCA tem IP público próprio de verdade (confirmado pelo usuário) —
# pro profile "internal" (só ramais registrados deste escritório,
# nunca um trunk/SBC de verdade), esse endereço "externo" nunca é
# correto, só existe pra causar exatamente este bug. Removendo os 2
# params, o profile cai automaticamente pra `rtp-ip`/`sip-ip`
# (10.10.32.138) pra tudo, sem exceção — não tem mais endereço "errado"
# pro FreeSWITCH escolher. O profile "external" (reservado pra um
# trunk PSTN real, nunca usado ainda nesta implantação) mantém
# `ext-rtp-ip`/`ext-sip-ip` intactos — só o "internal" precisava disso.
RUN sed -i \
-e '/<param name="ext-rtp-ip" value="\$\${external_rtp_ip}"\/>/d' \
-e '/<param name="ext-sip-ip" value="\$\${external_sip_ip}"\/>/d' \
/etc/freeswitch/sip_profiles/internal.xml \
&& ! grep -q 'ext-rtp-ip\|ext-sip-ip' /etc/freeswitch/sip_profiles/internal.xml
# RTP num range fixo e pequeno (PHASE 54, docs/EXTENSIONS.md) — o range
# vanilla default (16384-32768, ~16k portas) é inviável de publicar uma a
# uma no host; achado ao tentar registrar um softphone de fora da rede