# 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. `` (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 '/ *-->##' \ /etc/freeswitch/sip_profiles/internal.xml \ && grep -q '' /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###' \ /etc/freeswitch/sip_profiles/internal.xml \ && grep -q '' /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###' \ /etc/freeswitch/sip_profiles/internal.xml \ && grep -q '' /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 '//d' \ -e '//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 # Docker pela primeira vez (nenhuma porta SIP/RTP estava publicada até # aqui — só o Event Socket interno). Restrito a 200 portas, suficiente # pra alguns testes/chamadas simultâneas em laboratório. RUN sed -i \ -e 's###' \ -e 's###' \ /etc/freeswitch/autoload_configs/switch.conf.xml \ && grep -q '' /etc/freeswitch/autoload_configs/switch.conf.xml \ && grep -q '' /etc/freeswitch/autoload_configs/switch.conf.xml # Rotas de entrada por DID (PHASE 56, docs/INBOUND_ROUTES.md) — achado # real: nenhuma chamada que chega pelo profile "external" carrega # `b2bcall_tenant_id` (só REGISTER de ramal e discagem de saída setam essa # variable), então uma chamada de entrada não tem como saber de qual # tenant é. O contexto "public" vanilla (pra onde o profile "external" # aponta por padrão) é um ARQUIVO ESTÁTICO (`dialplan/public.xml`) — a # config estática sempre ganha da consulta ao mod_xml_curl, então esse # contexto NUNCA seria dinâmico enquanto se chamar "public". Renomeado # pra "inbound" (sem arquivo estático nenhum): toda chamada de entrada # passa a cair no mod_xml_curl (b2bcall-fs-config), que resolve o dono do # DID e injeta o tenant antes de encaminhar (packages/telephony # buildInboundRouteXml). `dialplan/public.xml` fica no lugar, sem efeito # — não precisa remover, só não é mais alcançado por nenhum profile. RUN sed -i 's###' \ /etc/freeswitch/sip_profiles/external.xml \ && grep -q '' /etc/freeswitch/sip_profiles/external.xml COPY entrypoint.sh /usr/local/bin/entrypoint.sh RUN chmod +x /usr/local/bin/entrypoint.sh EXPOSE 8021 EXPOSE 5060/udp 5060/tcp EXPOSE 16384-16584/udp ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] CMD ["/usr/bin/freeswitch", "-nonat"]