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:
@@ -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
|
||||
|
||||
@@ -10,5 +10,58 @@
|
||||
<node type="allow" cidr="::1/128"/>
|
||||
<node type="allow" cidr="172.16.0.0/12"/>
|
||||
</list>
|
||||
|
||||
<!-- PHASE 68 — achado real: depois de corrigir o registro (NDLB-
|
||||
received-in-nat-reg-contact, Dockerfile), a chamada completava
|
||||
mas sem áudio. Causa: `local-network-acl="localnet.auto"`
|
||||
(padrão vanilla) só cobre a subnet da PRÓPRIA interface do
|
||||
FreeSWITCH (10.10.32.0/24, com network_mode: host) — os ramais
|
||||
de verdade estão em OUTRAS subnets do mesmo escritório
|
||||
(192.168.40.0/24, 192.168.44.0/24), então o FreeSWITCH os
|
||||
tratava como "fora da rede local" e trocava o RTP/SIP anunciado
|
||||
no SDP pelo IP público compartilhado (Ext-RTP-IP/Ext-SIP-IP,
|
||||
191.240.175.55) — o mesmo hairpin NAT de antes, só que na
|
||||
mídia em vez do registro. Esta VM nunca tem IP público próprio
|
||||
(é sempre RFC1918 atrás do NAT do escritório), então qualquer
|
||||
cliente que realmente CONSEGUE alcançar este servidor já é,
|
||||
por definição, parte dessa rede privada — cobrir todo o RFC1918
|
||||
aqui é seguro pra esta implantação (nunca é "de fora" de
|
||||
verdade) e evita ter que listar cada subnet/VLAN do escritório
|
||||
uma por uma. -->
|
||||
<list name="b2bcall_lan" default="deny">
|
||||
<node type="allow" cidr="10.0.0.0/8"/>
|
||||
<node type="allow" cidr="172.16.0.0/12"/>
|
||||
<node type="allow" cidr="192.168.0.0/16"/>
|
||||
</list>
|
||||
|
||||
<!-- PHASE 68 (continuação) — achado real: com áudio já corrigido, o
|
||||
200 OK que o FreeSWITCH manda pra um ramal como 1503 ainda tinha
|
||||
`Contact: sip:1502@191.240.175.55:5060` (o IP público de novo) —
|
||||
o telefone nunca manda o ACK de volta (não é dele, não sabe
|
||||
voltar), o FreeSWITCH fica retransmitindo o 200 OK sozinho
|
||||
(0.5s/1s/2s/4s/4s...) até desistir em exatos 32s (Timer H do
|
||||
RFC 3261) e derrubar a chamada. Causa: o Contact que o
|
||||
FreeSWITCH usa pra SE IDENTIFICAR numa resposta SIP é decidido
|
||||
por `apply-nat-acl` (nat.auto), não por `local-network-acl` —
|
||||
são duas checagens INDEPENDENTES (uma pro SDP/mídia, outra pro
|
||||
cabeçalho SIP). `nat.auto` (RFC1918 exceto a própria subnet do
|
||||
FreeSWITCH) continua marcando ramais de OUTRAS subnets do
|
||||
escritório como "precisa de IP externo" nesta camada, mesmo já
|
||||
não afetando mais o SDP. Não dá pra simplesmente trocar
|
||||
`apply-nat-acl` pra `b2bcall_lan` (mesmo sentido de match de
|
||||
`local-network-acl`) — isso pararia de marcar esses ramais como
|
||||
NAT'd, e SEM essa marcação o `NDLB-received-in-nat-reg-contact`
|
||||
(Dockerfile, o fix do REGISTRO) para de disparar, voltando o
|
||||
bug original. Por isso esta é uma ACL NOVA e SEPARADA, com o
|
||||
sentido INVERTIDO de `b2bcall_lan`: "NAT'd de verdade" só quem
|
||||
está FORA de todo RFC1918 conhecido desta implantação — nunca
|
||||
alcançável de verdade nesta VM (nunca tem IP público próprio),
|
||||
mas mantém o comportamento correto se um dia existir uma
|
||||
conexão genuinamente externa (trunk real via IP público). -->
|
||||
<list name="b2bcall_external_nat" default="allow">
|
||||
<node type="deny" cidr="10.0.0.0/8"/>
|
||||
<node type="deny" cidr="172.16.0.0/12"/>
|
||||
<node type="deny" cidr="192.168.0.0/16"/>
|
||||
</list>
|
||||
</network-lists>
|
||||
</configuration>
|
||||
|
||||
@@ -6,7 +6,13 @@
|
||||
vanilla continua valendo como fallback quando "not found".
|
||||
Credenciais substituidas em runtime pelo entrypoint.sh — nunca
|
||||
ficam de verdade na imagem (mesmo padrao do ESL_PASSWORD). -->
|
||||
<param name="gateway-url" value="http://fs-config:8080/" bindings="directory|dialplan"/>
|
||||
<!-- 127.0.0.1, nao "fs-config" (PHASE 68): com o FreeSWITCH em
|
||||
network_mode: host, ele nao esta mais na rede bridge do
|
||||
compose, entao o DNS embutido do Docker (nomes de servico) nao
|
||||
resolve mais pra ele. fs-config publica 8080 so em loopback do
|
||||
host (docker-compose.yml) — o FreeSWITCH, agora usando a
|
||||
propria stack de rede do host, alcanca por ali. -->
|
||||
<param name="gateway-url" value="http://127.0.0.1:8080/" bindings="directory|dialplan"/>
|
||||
<param name="gateway-credentials" value="__FS_CONFIG_USER__:__FS_CONFIG_PASSWORD__"/>
|
||||
<param name="auth-scheme" value="basic"/>
|
||||
<param name="timeout" value="5"/>
|
||||
|
||||
Reference in New Issue
Block a user