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:
68
TODO.md
68
TODO.md
@@ -2456,6 +2456,74 @@ rota de entrada")
|
|||||||
os dois persistiram com o tipo certo, sobrevivendo a um reload
|
os dois persistiram com o tipo certo, sobrevivendo a um reload
|
||||||
completo da página.
|
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
|
## Riscos conhecidos
|
||||||
|
|||||||
@@ -49,11 +49,18 @@ services:
|
|||||||
FS_CONFIG_USER: ${FS_CONFIG_USER}
|
FS_CONFIG_USER: ${FS_CONFIG_USER}
|
||||||
FS_CONFIG_PASSWORD: ${FS_CONFIG_PASSWORD}
|
FS_CONFIG_PASSWORD: ${FS_CONFIG_PASSWORD}
|
||||||
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
|
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
|
||||||
ESL_HOST: freeswitch
|
# host.docker.internal (PHASE 68): freeswitch virou network_mode:
|
||||||
|
# host, saiu da rede bridge do compose — o nome de serviço
|
||||||
|
# "freeswitch" não resolve mais pra ele. host.docker.internal
|
||||||
|
# aponta pro HOST (onde o ESL agora escuta direto), exige o
|
||||||
|
# extra_hosts abaixo em Linux (Docker 20.10+).
|
||||||
|
ESL_HOST: host.docker.internal
|
||||||
ESL_PORT: "8021"
|
ESL_PORT: "8021"
|
||||||
ESL_PASSWORD: ${ESL_PASSWORD}
|
ESL_PASSWORD: ${ESL_PASSWORD}
|
||||||
SOFIA_EXTERNAL_GATEWAYS_DIR: /gateways
|
SOFIA_EXTERNAL_GATEWAYS_DIR: /gateways
|
||||||
CALLCENTER_QUEUES_DIR: /callcenter-queues
|
CALLCENTER_QUEUES_DIR: /callcenter-queues
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
volumes:
|
volumes:
|
||||||
# Compartilhado com o FreeSWITCH (agente.md secao 41-42): fs-config
|
# Compartilhado com o FreeSWITCH (agente.md secao 41-42): fs-config
|
||||||
# escreve os XML de gateway aqui, o profile "external" ja inclui
|
# escreve os XML de gateway aqui, o profile "external" ja inclui
|
||||||
@@ -63,7 +70,12 @@ services:
|
|||||||
# escreve um XML de fila por arquivo aqui, incluido via
|
# escreve um XML de fila por arquivo aqui, incluido via
|
||||||
# X-PRE-PROCESS no nosso callcenter.conf.xml (mesmo padrao acima).
|
# X-PRE-PROCESS no nosso callcenter.conf.xml (mesmo padrao acima).
|
||||||
- freeswitch_callcenter_queues:/callcenter-queues
|
- freeswitch_callcenter_queues:/callcenter-queues
|
||||||
# Sem porta publicada: so o FreeSWITCH (mesma rede do compose) chama isto.
|
# Publicado só em loopback (PHASE 68): o FreeSWITCH virou
|
||||||
|
# network_mode: host e chama isto via 127.0.0.1 agora (ver
|
||||||
|
# xml_curl.conf.xml) — antes não precisava de porta nenhuma porque os
|
||||||
|
# dois estavam na mesma rede bridge do compose.
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8080:8080"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "node", "-e", "fetch('http://localhost:8080/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"]
|
test: ["CMD", "node", "-e", "fetch('http://localhost:8080/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
@@ -99,28 +111,31 @@ services:
|
|||||||
# uma chamada ativa seria latência/confiabilidade desnecessárias
|
# uma chamada ativa seria latência/confiabilidade desnecessárias
|
||||||
# pra um prompt de poucos segundos.
|
# pra um prompt de poucos segundos.
|
||||||
- ./data/ivr-prompts:/ivr-prompts
|
- ./data/ivr-prompts:/ivr-prompts
|
||||||
# Event Socket (8021) NUNCA publicado pra REDE — só em loopback do
|
# network_mode: host (PHASE 68) — achado real reportado pelo usuário:
|
||||||
# próprio host (agente.md secao 22: "NÃO pública... acesso somente
|
# dois ramais reais (telefones IP), atrás do MESMO roteador/NAT desta
|
||||||
# pela aplicação autorizada"). Achado real testando as telas de
|
# rede (esta VM nunca tem IP público próprio — é sempre RFC1918 por
|
||||||
# Infraestrutura (platform): o comentário original aqui dizia que
|
# trás do NAT do escritório), ligando um pro outro. Com bridge+NAT do
|
||||||
# elas SEMPRE falhariam nesta VM porque "apps/api roda fora do
|
# Docker no meio, o SIP/RTP entre eles precisava atravessar uma camada
|
||||||
# Docker" — isso estava incompleto. `apps/api` (systemd, no host)
|
# de tradução de endereço A MAIS antes mesmo de chegar no roteador do
|
||||||
# JÁ conseguia alcançar o IP do container na rede bridge sem nenhuma
|
# escritório — combinado com o hairpin NAT do roteador (dois clientes
|
||||||
# porta publicada (`docker inspect` + `/dev/tcp` confirmam: o HOST
|
# atrás do mesmo NAT se enxergando só pelo IP público), a chamada
|
||||||
# sempre alcança a rede bridge do Docker, só outras MÁQUINAS é que
|
# falhava em 0ms (503, nunca chegava a sair pela rede de verdade).
|
||||||
# não). O bloqueio real era `ESL_HOST=freeswitch` — um nome DNS que
|
# Tirar o Docker NAT do meio (host networking) elimina essa camada
|
||||||
# só existe dentro da rede interna do Docker, nunca no resolver do
|
# extra; o fix real de hairpin em si é separado — ver comentário sobre
|
||||||
# host. `127.0.0.1:8021:8021` resolve isso sem abrir a porta pra
|
# `NDLB-received-in-nat-reg-contact` no Dockerfile.
|
||||||
# ninguém além do próprio host — mesma garantia de segurança de
|
#
|
||||||
# antes, só que a aplicação autorizada agora alcança de verdade.
|
# Sem `ports:` aqui (inválido com network_mode: host — o container usa
|
||||||
# SIP (5060) e RTP (16384-16584, range fixo no Dockerfile) publicados
|
# as portas do HOST diretamente). Segurança do Event Socket (8021,
|
||||||
# pra rede a partir da PHASE 54 — necessário pra registrar um
|
# agente.md secao 22) deixa de depender de publicar só em loopback
|
||||||
# softphone/telefone de fora da rede Docker.
|
# (não existe mais essa camada) e passa a depender inteiramente da
|
||||||
ports:
|
# ACL `apply-inbound-acl="b2bcall_internal"` já configurada em
|
||||||
- "127.0.0.1:8021:8021"
|
# event_socket.conf.xml (127.0.0.0/8 + 172.16.0.0/12, o range do
|
||||||
- "5060:5060/udp"
|
# bridge do Docker) — qualquer outra origem continua rejeitada mesmo
|
||||||
- "5060:5060/tcp"
|
# com a porta agora "aberta" nas interfaces do host. Os workers que
|
||||||
- "16384-16584:16384-16584/udp"
|
# antes alcançavam via nome de serviço `freeswitch` na rede do compose
|
||||||
|
# (fs-events/fs-config/predictive-dialer) precisam agora de
|
||||||
|
# `host.docker.internal` — ver `extra_hosts` em cada um deles abaixo.
|
||||||
|
network_mode: host
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "fs_cli -p \"$$ESL_PASSWORD\" -x status | grep -q 'is ready'"]
|
test: ["CMD-SHELL", "fs_cli -p \"$$ESL_PASSWORD\" -x status | grep -q 'is ready'"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
@@ -139,7 +154,9 @@ services:
|
|||||||
- redis
|
- redis
|
||||||
- postgres
|
- postgres
|
||||||
environment:
|
environment:
|
||||||
ESL_HOST: freeswitch
|
# host.docker.internal, não "freeswitch" (PHASE 68) — ver comentário
|
||||||
|
# equivalente em fs-config acima.
|
||||||
|
ESL_HOST: host.docker.internal
|
||||||
ESL_PORT: "8021"
|
ESL_PORT: "8021"
|
||||||
ESL_PASSWORD: ${ESL_PASSWORD}
|
ESL_PASSWORD: ${ESL_PASSWORD}
|
||||||
APP_DATABASE_URL: postgresql://${POSTGRES_APP_USER}:${POSTGRES_APP_PASSWORD}@postgres:5432/${POSTGRES_DB}?schema=public
|
APP_DATABASE_URL: postgresql://${POSTGRES_APP_USER}:${POSTGRES_APP_PASSWORD}@postgres:5432/${POSTGRES_DB}?schema=public
|
||||||
@@ -159,6 +176,8 @@ services:
|
|||||||
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID:-}
|
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID:-}
|
||||||
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:-}
|
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:-}
|
||||||
S3_FORCE_PATH_STYLE: ${S3_FORCE_PATH_STYLE:-}
|
S3_FORCE_PATH_STYLE: ${S3_FORCE_PATH_STYLE:-}
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/recordings-spool:/recordings
|
- ./data/recordings-spool:/recordings
|
||||||
- ./data/object-storage-local:/data/object-storage
|
- ./data/object-storage-local:/data/object-storage
|
||||||
@@ -174,7 +193,9 @@ services:
|
|||||||
- redis
|
- redis
|
||||||
- postgres
|
- postgres
|
||||||
environment:
|
environment:
|
||||||
ESL_HOST: freeswitch
|
# host.docker.internal, não "freeswitch" (PHASE 68) — ver comentário
|
||||||
|
# equivalente em fs-config acima.
|
||||||
|
ESL_HOST: host.docker.internal
|
||||||
ESL_PORT: "8021"
|
ESL_PORT: "8021"
|
||||||
ESL_PASSWORD: ${ESL_PASSWORD}
|
ESL_PASSWORD: ${ESL_PASSWORD}
|
||||||
APP_DATABASE_URL: postgresql://${POSTGRES_APP_USER}:${POSTGRES_APP_PASSWORD}@postgres:5432/${POSTGRES_DB}?schema=public
|
APP_DATABASE_URL: postgresql://${POSTGRES_APP_USER}:${POSTGRES_APP_PASSWORD}@postgres:5432/${POSTGRES_DB}?schema=public
|
||||||
@@ -188,6 +209,8 @@ services:
|
|||||||
# — quem de fato grava e' o processo FreeSWITCH, não este worker;
|
# — quem de fato grava e' o processo FreeSWITCH, não este worker;
|
||||||
# não precisa do volume montado aqui, só saber o path Docker-interno.
|
# não precisa do volume montado aqui, só saber o path Docker-interno.
|
||||||
RECORDINGS_SPOOL_DIR: /recordings
|
RECORDINGS_SPOOL_DIR: /recordings
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
|
||||||
ai-worker:
|
ai-worker:
|
||||||
build:
|
build:
|
||||||
|
|||||||
@@ -1,31 +1,47 @@
|
|||||||
# Arquitetura de Rede
|
# Arquitetura de Rede
|
||||||
|
|
||||||
## Estado atual (fase FreeSWITCH inicial)
|
## Estado atual (PHASE 68 — `network_mode: host`)
|
||||||
|
|
||||||
`freeswitch` roda na rede padrão do Docker Compose (bridge, `b2bcall_default`),
|
`freeswitch` roda em `network_mode: host` — decisão tomada na PHASE 68 (era
|
||||||
igual a `postgres` e `redis`. Nenhuma porta é publicada no host — nem 8021
|
"pendente" antes, ver seção abaixo), pedida pelo usuário depois de dois
|
||||||
(ESL), nem SIP (5060/5080), nem RTP. Isso é intencional: ainda não existe
|
ramais reais no mesmo escritório não conseguirem se ligar. `postgres`/
|
||||||
nenhum tronco SIP real nem ramal externo, então não há motivo pra expor nada.
|
`redis`/`fs-config` continuam na rede bridge padrão do compose
|
||||||
|
(`b2bcall_default`); só o FreeSWITCH saiu dela.
|
||||||
|
|
||||||
`apps/api` roda hoje **direto no host** (fora do Docker), então usa
|
Com host networking, o FreeSWITCH usa a stack de rede do PRÓPRIO host
|
||||||
`APP_DATABASE_URL`/`REDIS_URL` apontando pra `localhost` nas portas publicadas
|
diretamente — sem NAT/bridge do Docker no meio, `local_ip_v4` resolve pro IP
|
||||||
pelo Postgres/Redis. Ela não consegue (nem precisa, ainda) alcançar o
|
real da interface (`10.10.32.138` nesta VM), SIP/ESL escutam direto nas
|
||||||
FreeSWITCH.
|
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.
|
||||||
|
|
||||||
## Decisão pendente: `network_mode` do FreeSWITCH (agente.md secao 19)
|
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.
|
||||||
|
|
||||||
Quando existir um tronco SIP real (fase Trunks), será preciso decidir entre:
|
`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).
|
||||||
|
|
||||||
- **`network_mode: host`**: mais simples pra SIP/RTP (sem NAT entre o
|
## Decisão tomada: `network_mode` do FreeSWITCH (agente.md secao 19)
|
||||||
container e a rede), mas perde isolamento de rede do Docker.
|
|
||||||
- **macvlan/ipvlan**: dá ao FreeSWITCH um IP próprio na rede física, sem expor
|
|
||||||
outros serviços do host: mais trabalho de configurar, melhor isolamento.
|
|
||||||
|
|
||||||
Não decidido ainda — só vira relevante quando houver um carrier/SBC real pra
|
Historicamente listada como pendente (macvlan/ipvlan vs. host), resolvida na
|
||||||
conectar (secao 17: "FreeSWITCH não deverá depender de IP SIP público" — a
|
PHASE 68 a favor de **`network_mode: host`** — não por causa de um trunk
|
||||||
topologia esperada é `Internet → OpenSIPS → rede SIP privada → FreeSWITCH`,
|
PSTN real (a topologia `Internet → OpenSIPS → rede SIP privada →
|
||||||
então o FreeSWITCH em si tende a ficar em rede privada mesmo, o que favorece
|
FreeSWITCH` da secao 17 continua valendo pra isso), mas por um problema
|
||||||
manter bridge/macvlan em vez de host).
|
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
|
## Achado real (PHASE 65): VM atrás de roteador, ramal externo sem áudio
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,83 @@ RUN sed -i \
|
|||||||
/etc/freeswitch/sip_profiles/internal.xml \
|
/etc/freeswitch/sip_profiles/internal.xml \
|
||||||
&& ! grep -q "force-register-domain\|force-subscription-domain\|force-register-db-domain" /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
|
# 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
|
# 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
|
# 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="::1/128"/>
|
||||||
<node type="allow" cidr="172.16.0.0/12"/>
|
<node type="allow" cidr="172.16.0.0/12"/>
|
||||||
</list>
|
</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>
|
</network-lists>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|||||||
@@ -6,7 +6,13 @@
|
|||||||
vanilla continua valendo como fallback quando "not found".
|
vanilla continua valendo como fallback quando "not found".
|
||||||
Credenciais substituidas em runtime pelo entrypoint.sh — nunca
|
Credenciais substituidas em runtime pelo entrypoint.sh — nunca
|
||||||
ficam de verdade na imagem (mesmo padrao do ESL_PASSWORD). -->
|
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="gateway-credentials" value="__FS_CONFIG_USER__:__FS_CONFIG_PASSWORD__"/>
|
||||||
<param name="auth-scheme" value="basic"/>
|
<param name="auth-scheme" value="basic"/>
|
||||||
<param name="timeout" value="5"/>
|
<param name="timeout" value="5"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user