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
- packages/telephony: TelephonyProvider interface (agente.md secao 25) and
FreeSwitchTelephonyProvider implementation over the 'esl' library
(actively maintained, TypeScript-native, built-in reconnect-with-backoff
satisfying secao 195); normalizeEslEvent() translates raw ESL events into
the internal vocabulary (secao 24)
- apps/freeswitch-events (b2bcall-fs-events): permanent ESL connection,
resubscribes on every reconnect, publishes normalized events to the
'b2bcall:events' Redis pub/sub channel; containerized (Dockerfile +
docker-compose service) since its whole job is reaching the freeswitch
container by internal hostname
- packages/shared: reusable createLogger() (structured JSON per secao 189),
fixed a BigInt serialization crash surfaced by the esl library's error
stats
- found and fixed a real FreeSWITCH 1.11 default: without an explicit
apply-inbound-acl, mod_event_socket silently rejects any non-loopback
connection ('Access Denied, go away.') even with the correct password —
added a dedicated ACL (loopback + the Docker Compose network range, never
0.0.0.0/0) in infrastructure/freeswitch/overrides/autoload_configs/
- verified end-to-end with a local loopback test call: CALL_CREATED ->
CALL_ANSWERED -> CALL_ENDED observed on the Redis channel with the
correct callUuid and hangup cause
- docs/EVENT_SOCKET.md