Files
B2BCall-dialer/infrastructure/freeswitch/Dockerfile
Matheus af46739af2 fix(telefonia): pickup de grupo funcionando de verdade + RCE no dialplan + domínio real no REGISTER
Pedido explícito do usuário: "testa criando um ramal com callgroup e captura
chamada de outro ramal". O teste com softphones reais (não só leitura de
código) achou 3 problemas que a fase anterior tinha dado como resolvidos:

1. A variable de call group estava com o nome errado (`call-group`, convenção
   do Asterisk) e a suposição de que o FreeSWITCH fazia pickup automático só
   com ela era falsa. Corrigido pro nome certo (`callgroup`) e pro mecanismo
   real (fork de leg `pickup/<grupo>` no bridge + extension `*8` chamando a
   application `pickup`, lendo o grupo via `${user_data(...)}`).

2. Implementando o mecanismo acima, achada uma vulnerabilidade real de RCE: o
   allowlist de `application` no dialplan nunca bloqueava `${system(...)}`
   embutido dentro do `data` de qualquer application já permitida —
   `mod_commands` está carregado, então isso era execução de comando
   arbitrário no host do FreeSWITCH pra qualquer Tenant Admin. Corrigido com
   um segundo allowlist (`ALLOWED_INLINE_API_FUNCTIONS` +
   `IsSafeDialplanData`) que só libera funções de leitura seguras
   (`user_data`, `escape`, `url_encode`, `url_decode`, `regex`, `strftime`).

3. Registrar um softphone de verdade contra o domínio do tenant (não só curl
   no mod_xml_curl) revelava 403 Forbidden: o sofia profile `internal` tinha
   `force-register-domain`/`force-subscription-domain`/
   `force-register-db-domain` fixados no domínio antigo, ignorando o domínio
   de cada tenant. Corrigido no Dockerfile do FreeSWITCH (imagem
   reconstruída, não só patch ao vivo).

Testado ponta a ponta com 3 softphones reais (linphone-cli) em containers na
mesma rede Docker: ramal do mesmo grupo captura de verdade uma ligação
tocando em outro ramal via *8 (canais confirmados bridged via `show
channels`); ramal de grupo diferente tenta e falha. RCE confirmado bloqueado
via curl (`${system(id)}` → 400) sem quebrar `${user_data(...)}` legítimo.

TODO.md (PHASE 53) e docs/EXTENSIONS.md atualizados corrigindo as afirmações
incompletas da fase anterior ("nenhuma mudança de infra necessária").

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
2026-08-30 15:24:58 -03:00

88 lines
4.6 KiB
Docker
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# syntax=docker/dockerfile:1.7
#
# Imagem própria/controlada do FreeSWITCH (agente.md secao 19), usando os
# pacotes pré-compilados do SignalWire em vez de compilar da fonte — build
# de C/C++ e´ pesado demais pra VM de laboratorio (1.9GB RAM).
FROM debian:trixie-slim
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates gnupg curl \
&& rm -rf /var/lib/apt/lists/*
# FREESWITCH_PAT só existe durante este RUN (BuildKit secret, nunca vira
# camada da imagem) e o arquivo de credenciais do apt é apagado antes do fim
# do MESMO RUN — agente.md secao 11: "a credencial nao devera permanecer na
# imagem runtime".
RUN --mount=type=secret,id=freeswitch_pat,required=true \
set -eu; \
FS_PAT="$(cat /run/secrets/freeswitch_pat)"; \
curl -fsSL -u "signalwire:${FS_PAT}" \
https://freeswitch.signalwire.com/repo/deb/debian-release/signalwire-freeswitch-repo.gpg \
-o /usr/share/keyrings/signalwire-freeswitch-repo.gpg; \
install -d -m 700 /etc/apt/auth.conf.d; \
printf 'machine freeswitch.signalwire.com\nlogin signalwire\npassword %s\n' "${FS_PAT}" \
> /etc/apt/auth.conf.d/freeswitch.conf; \
chmod 600 /etc/apt/auth.conf.d/freeswitch.conf; \
echo "deb [signed-by=/usr/share/keyrings/signalwire-freeswitch-repo.gpg] https://freeswitch.signalwire.com/repo/deb/debian-release/ trixie main" \
> /etc/apt/sources.list.d/freeswitch.list; \
apt-get update; \
apt-get install -y --no-install-recommends \
freeswitch-meta-vanilla \
freeswitch-conf-vanilla \
freeswitch-mod-xml-curl \
freeswitch-mod-callcenter \
freeswitch-mod-avmd \
freeswitch-mod-curl; \
rm -f /etc/apt/auth.conf.d/freeswitch.conf /etc/apt/sources.list.d/freeswitch.list; \
rm -rf /var/lib/apt/lists/*
# Overrides mínimos por cima da config vanilla (agente.md secao 15: "carregar
# somente o necessário"). Directory/dialplan/sip_profiles ficam na config
# vanilla padrão por enquanto — serão substituídos por mod_xml_curl na fase
# "Extensions/Trunks/Dialplan" (ver docs/FREESWITCH.md).
COPY overrides/autoload_configs/modules.conf.xml /etc/freeswitch/autoload_configs/modules.conf.xml
COPY overrides/autoload_configs/event_socket.conf.xml /etc/freeswitch/autoload_configs/event_socket.conf.xml
COPY overrides/autoload_configs/acl.conf.xml /etc/freeswitch/autoload_configs/acl.conf.xml
COPY overrides/autoload_configs/xml_curl.conf.xml /etc/freeswitch/autoload_configs/xml_curl.conf.xml
COPY overrides/autoload_configs/callcenter.conf.xml /etc/freeswitch/autoload_configs/callcenter.conf.xml
RUN mkdir -p /etc/freeswitch/autoload_configs/callcenter_queues.conf.d
# Pino $${domain} num valor estavel em vez do IP dinamico do container
# (vars.xml vanilla usa "domain=$${local_ip_v4}", que muda a cada restart e
# nunca bateria com Tenant.telephonyDomain). Ver docs/EXTENSIONS.md.
ARG DEFAULT_SIP_DOMAIN=b2bcall.local
RUN sed -i "s/data=\"domain=\$\${local_ip_v4}\"/data=\"domain=${DEFAULT_SIP_DOMAIN}\"/" \
/etc/freeswitch/vars.xml \
&& grep -q "domain=${DEFAULT_SIP_DOMAIN}" /etc/freeswitch/vars.xml
# Multi-domínio real por tenant (PHASE 52, docs/EXTENSIONS.md) — achado
# real, confirmado só depois de registrar um SIP de verdade (o teste por
# curl direto no mod_xml_curl não pegava isto, porque roda ANTES do
# xml_curl ser consultado): o profile "internal" vanilla vem com
# `force-register-domain`/`force-subscription-domain`/
# `force-register-db-domain` fixados em `$${domain}` — REGISTER de
# QUALQUER identidade era resolvido contra o domínio fixo do profile
# ("b2bcall.local"), nunca o domínio que o tenant realmente tem
# (`acme.b2bcall.net`), dando 403 Forbidden sempre. `<domain name="all"
# alias="true".../>` (também vanilla) só afeta contexto de dialplan, não
# esta checagem de REGISTER. Removendo os 3 params (procedimento padrão
# documentado do próprio FreeSWITCH pra multi-domínio), o REGISTER passa
# a resolver o domínio do próprio pedido — confirmado registrando um
# softphone de teste de verdade contra `acme.b2bcall.net`.
RUN sed -i \
-e '/<param name="force-register-domain"/d' \
-e '/<param name="force-subscription-domain"/d' \
-e '/<param name="force-register-db-domain"/d' \
/etc/freeswitch/sip_profiles/internal.xml \
&& ! grep -q "force-register-domain\|force-subscription-domain\|force-register-db-domain" /etc/freeswitch/sip_profiles/internal.xml
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
EXPOSE 8021
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["/usr/bin/freeswitch", "-nonat"]