From eee6d7aece8dcbda9cd5410d004ee670163bb10e Mon Sep 17 00:00:00 2001 From: B2BCall Bootstrap Date: Thu, 27 Aug 2026 10:47:28 -0300 Subject: [PATCH] feat: add asterisk pjsip integration - infrastructure/docker/asterisk.Dockerfile: Asterisk 22.10.1 LTS compilado do fonte oficial (Debian trixie nao distribui mais o pacote asterisk), com PJSIP, AMI, ARI, res_odbc/res_config_odbc, cdr_adaptive_odbc, cel_odbc, app_queue - infrastructure/asterisk/config: templates renderizados no entrypoint (secrets via envsubst, nunca versionados) + sorcery.conf/extconfig.conf para PJSIP realtime, dialplan de teste (extensao 600) - infrastructure/postgres/init/002-asterisk-realtime.sql: tabelas ps_endpoints/ps_auths/ps_aors/ps_contacts/ps_endpoint_id_ips/ ps_registrations/cdr/cel no schema 'asterisk' - infrastructure/nftables: protege AMI/ARI/SIP contra acesso pela LAN (interface ens18), trazido da Fase 9 do TODO para ja, sem afetar SSH nem a rede interna do Docker - docker-compose.yml: servico asterisk em network_mode: host (RTP); Postgres publicado em 127.0.0.1:5432 (nao 0.0.0.0) para o Asterisk alcanca-lo, ja que host network nao enxerga a rede Docker interna Testado: AMI login, ARI com auth (401 sem auth), pjsip realtime consultando Postgres sem erro, dialplan originate -> CDR e CEL gravados corretamente, healthcheck do compose passando. --- .dockerignore | 17 ++ .env.example | 5 + TODO.md | 40 ++- docker-compose.yml | 52 +++- docs/ARCHITECTURE.md | 14 ++ infrastructure/asterisk/config/ari.conf.tpl | 13 + infrastructure/asterisk/config/cdr.conf | 6 + .../asterisk/config/cdr_adaptive_odbc.conf | 7 + infrastructure/asterisk/config/cel.conf | 4 + infrastructure/asterisk/config/cel_odbc.conf | 4 + infrastructure/asterisk/config/extconfig.conf | 10 + .../asterisk/config/extensions.conf | 21 ++ infrastructure/asterisk/config/http.conf | 10 + infrastructure/asterisk/config/logger.conf | 7 + .../asterisk/config/manager.conf.tpl | 19 ++ infrastructure/asterisk/config/odbc.ini.tpl | 12 + infrastructure/asterisk/config/pjsip.conf | 13 + infrastructure/asterisk/config/queues.conf | 8 + infrastructure/asterisk/config/res_odbc.conf | 9 + infrastructure/asterisk/config/sorcery.conf | 11 + infrastructure/docker/asterisk-entrypoint.sh | 33 +++ infrastructure/docker/asterisk.Dockerfile | 70 ++++++ .../nftables/b2bcall-nftables.service | 15 ++ infrastructure/nftables/b2bcall.nft | 47 ++++ .../postgres/init/002-asterisk-realtime.sql | 236 ++++++++++++++++++ 25 files changed, 675 insertions(+), 8 deletions(-) create mode 100644 .dockerignore create mode 100644 infrastructure/asterisk/config/ari.conf.tpl create mode 100644 infrastructure/asterisk/config/cdr.conf create mode 100644 infrastructure/asterisk/config/cdr_adaptive_odbc.conf create mode 100644 infrastructure/asterisk/config/cel.conf create mode 100644 infrastructure/asterisk/config/cel_odbc.conf create mode 100644 infrastructure/asterisk/config/extconfig.conf create mode 100644 infrastructure/asterisk/config/extensions.conf create mode 100644 infrastructure/asterisk/config/http.conf create mode 100644 infrastructure/asterisk/config/logger.conf create mode 100644 infrastructure/asterisk/config/manager.conf.tpl create mode 100644 infrastructure/asterisk/config/odbc.ini.tpl create mode 100644 infrastructure/asterisk/config/pjsip.conf create mode 100644 infrastructure/asterisk/config/queues.conf create mode 100644 infrastructure/asterisk/config/res_odbc.conf create mode 100644 infrastructure/asterisk/config/sorcery.conf create mode 100644 infrastructure/docker/asterisk-entrypoint.sh create mode 100644 infrastructure/docker/asterisk.Dockerfile create mode 100644 infrastructure/nftables/b2bcall-nftables.service create mode 100644 infrastructure/nftables/b2bcall.nft create mode 100644 infrastructure/postgres/init/002-asterisk-realtime.sql diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c50457d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,17 @@ +.git +.claude +.env +.env.* +!.env.example +node_modules +**/node_modules +dist +**/dist +.next +**/.next +build +**/build +coverage +**/coverage +*.log +docs diff --git a/.env.example b/.env.example index 6d27fbe..5d92217 100644 --- a/.env.example +++ b/.env.example @@ -27,6 +27,11 @@ DATABASE_URL=postgresql://b2bcall:CHANGE_ME_STRONG_PASSWORD@postgres:5432/b2bcal # Schema separado usado pelo Asterisk Realtime (res_odbc) ASTERISK_DB_SCHEMA=asterisk +# Host que o container do Asterisk (network_mode: host) usa para alcançar o +# Postgres — via loopback do host, já que ele não enxerga a rede Docker +# interna por nome (ver docker-compose.yml e docs/ARCHITECTURE.md). +ASTERISK_DB_HOST=127.0.0.1 + # --- Redis ----------------------------------------------------------------- REDIS_HOST=redis REDIS_PORT=6379 diff --git a/TODO.md b/TODO.md index 1429d96..143734d 100644 --- a/TODO.md +++ b/TODO.md @@ -22,13 +22,39 @@ mestre original (`agente.md`, seções 90-93). - [ ] packages/database: schema inicial + migrations tool ## Fase 2 — Asterisk -- [ ] Dockerfile Asterisk 22 LTS (host network) -- [ ] PJSIP (transports, sem chan_sip) -- [ ] AMI habilitado (bind local/rede interna apenas) -- [ ] ARI habilitado (idem) -- [ ] Realtime ODBC -> Postgres (schema asterisk) -- [ ] CDR, CEL, queue_log habilitados -- [ ] Dialplan inicial de teste +- [x] Dockerfile Asterisk 22.10.1 LTS compilado do fonte (host network) — + Debian trixie não tem mais o pacote `asterisk` nos repos oficiais +- [x] PJSIP (transport UDP:5060; chan_sip nem existe mais a partir do Ast 21) +- [x] AMI habilitado — testado login via 127.0.0.1:5038, bloqueado na LAN via nftables +- [x] ARI habilitado — testado GET /ari/asterisk/info via 127.0.0.1:8088, 401 sem auth +- [x] Realtime ODBC -> Postgres schema `asterisk` (sorcery.conf + extconfig.conf + + res_odbc.conf) — `pjsip show endpoints` consulta o Postgres sem erro +- [x] CDR via cdr_adaptive_odbc — testado, grava em asterisk.cdr (ANSWERED, + duration/billsec corretos). Nota: coluna `end` renomeada para + `end_time` (palavra reservada no Postgres, cdr_adaptive_odbc não + quota identificadores) +- [x] CEL via cel_odbc — testado, grava em asterisk.cel (CHAN_START/ANSWER/ + HANGUP/CHAN_END) +- [x] queue_log habilitado (padrão do app_queue, arquivo em /var/log/asterisk) +- [x] Dialplan inicial de teste (contexto `b2bcall-healthcheck`, exten 600, + Answer/Playback/Echo) — testado via `channel originate ... extension` +- [x] nftables protegendo AMI/ARI/SIP na interface da LAN (ens18), trazido da + Fase 9 para já — não testado a partir de outra máquina na LAN (só + localmente, onde tráfego para o próprio IP vai por `lo` e não + exercita a regra; revisar quando OpenSIPS/outro host estiver disponível) + +### Pendências conhecidas da Fase 2 (não bloqueiam, revisitar depois) +- [ ] `res_pjsip_outbound_registration` recusa carregar no boot (mensagem + "no existing documentation" para o tipo 'registration' do sorcery) — + carregamento manual via `module load` funciona mas é desfeito por uma + flag interna de "declined at boot". Só afeta troncos com registration + outbound; endpoints/aors funcionam normalmente. Investigar na Fase 4 + ao implementar cadastro de troncos com registration. +- [ ] `res_config_ldap` gera ERROR de log no boot (sem LDAP configurado, + não usamos). Cosmético — considerar `noload` em modules.conf. +- [ ] `cdr_pgsql`/`cel_pgsql` não compilaram (falta libpq-dev na imagem); + usamos `cdr_adaptive_odbc`/`cel_odbc` como alternativa funcional via + ODBC — decisão adequada, não precisa dos módulos pgsql nativos. ## Fase 3 — Backend base - [ ] apps/api (NestJS + Fastify) bootstrap diff --git a/docker-compose.yml b/docker-compose.yml index d504473..abae78b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,9 @@ networks: volumes: postgres-data: redis-data: + asterisk-lib: + asterisk-log: + asterisk-spool: services: postgres: @@ -22,7 +25,12 @@ services: - ./infrastructure/postgres/init:/docker-entrypoint-initdb.d:ro networks: - b2bcall-net - # Sem "ports": Postgres não deve ficar acessível fora da rede Docker interna. + ports: + # Publicado SOMENTE em loopback do host: o container do Asterisk usa + # network_mode: host (necessário para RTP) e por isso não enxerga a + # rede Docker interna por nome — precisa alcançar o Postgres via + # 127.0.0.1. Isso nunca fica acessível pela LAN/Internet (não é 0.0.0.0). + - "127.0.0.1:${POSTGRES_PORT:-5432}:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-b2bcall} -d ${POSTGRES_DB:-b2bcall}"] interval: 10s @@ -71,3 +79,45 @@ services: resources: limits: memory: 160M + + asterisk: + build: + context: . + dockerfile: infrastructure/docker/asterisk.Dockerfile + image: b2bcall-asterisk:22.10.1 + container_name: b2bcall-asterisk + restart: unless-stopped + network_mode: host + # RTP exige uma faixa larga de portas UDP — inviável de mapear uma a uma + # em bridge network, por isso host mode (ver docs/ARCHITECTURE.md 3.1). + # AMI (5038), ARI (8088) e SIP (5060) ficam protegidos pelo nftables + # (infrastructure/nftables/b2bcall.nft), que bloqueia acesso pela LAN. + depends_on: + postgres: + condition: service_healthy + env_file: + - .env + environment: + # O Asterisk (host network) alcança o Postgres via loopback do host, + # não pelo nome do container (ver publicação de porta do serviço postgres). + ASTERISK_DB_HOST: ${ASTERISK_DB_HOST:-127.0.0.1} + volumes: + - ./infrastructure/asterisk/config:/etc/asterisk-templates:ro + - asterisk-lib:/var/lib/asterisk + - asterisk-log:/var/log/asterisk + - asterisk-spool:/var/spool/asterisk + healthcheck: + test: ["CMD-SHELL", "asterisk -rx 'core show uptime' | grep -q 'System uptime'"] + interval: 15s + timeout: 5s + retries: 5 + start_period: 20s + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + deploy: + resources: + limits: + memory: 400M diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 7412917..2832555 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -84,6 +84,20 @@ O Asterisk nunca terá IP público. OpenSIPS e troncos são acessados via IP pri AMI/ARI nunca expostos além do host local (bind em 127.0.0.1 ou rede interna Docker quando possível). +### 3.2.1 Asterisk (host network) ↔ Postgres/Redis (rede Docker interna) + +Como o Asterisk usa `network_mode: host`, ele não enxerga a rede Docker +interna (`b2bcall-net`) por nome — DNS de containers só funciona para quem +está anexado à mesma rede definida pelo usuário. Solução adotada: o Postgres +publica a porta 5432 **somente em `127.0.0.1` do host** +(`127.0.0.1:5432:5432`, nunca `0.0.0.0`), e o Asterisk se conecta via +`ASTERISK_DB_HOST=127.0.0.1` (variável própria, distinta de +`POSTGRES_HOST=postgres` usada pelos demais serviços na rede interna). Isso +mantém o Postgres inacessível pela LAN/Internet, alcançável apenas por +processos no próprio host — incluindo um container em host network. Redis +não precisa disso: só a aplicação (bridge network) fala com Redis, nunca o +Asterisk diretamente. + ### 3.3 Asterisk Realtime - PJSIP objects (`ps_endpoints`, `ps_auths`, `ps_aors`, `ps_contacts`, diff --git a/infrastructure/asterisk/config/ari.conf.tpl b/infrastructure/asterisk/config/ari.conf.tpl new file mode 100644 index 0000000..5520729 --- /dev/null +++ b/infrastructure/asterisk/config/ari.conf.tpl @@ -0,0 +1,13 @@ +; ARI (Asterisk REST Interface). Usado apenas quando controle fino de +; canais/bridges exigir (ver packages/telephony/AsteriskTelephonyProvider). +; Protegido pelo nftables da mesma forma que o AMI (porta 8088 bloqueada na +; interface da LAN, liberada apenas em loopback/rede interna Docker). +[general] +enabled = yes +pretty = yes +allowed_origins = + +[${ARI_USERNAME}] +type = user +read_only = no +password = ${ARI_SECRET} diff --git a/infrastructure/asterisk/config/cdr.conf b/infrastructure/asterisk/config/cdr.conf new file mode 100644 index 0000000..c6c8725 --- /dev/null +++ b/infrastructure/asterisk/config/cdr.conf @@ -0,0 +1,6 @@ +[general] +enable = yes +unanswered = yes +congestion = yes +endbeforehexten = no +initiatedseconds = no diff --git a/infrastructure/asterisk/config/cdr_adaptive_odbc.conf b/infrastructure/asterisk/config/cdr_adaptive_odbc.conf new file mode 100644 index 0000000..c4c558b --- /dev/null +++ b/infrastructure/asterisk/config/cdr_adaptive_odbc.conf @@ -0,0 +1,7 @@ +; CDR via Adaptive ODBC: introspecciona as colunas reais da tabela +; asterisk.cdr (start/answer/end/uniqueid/linkedid/...) em vez de usar o +; esquema legado fixo do cdr_odbc (que exige colunas antigas como +; "calldate" e não suporta uniqueid/linkedid). +[asterisk] +connection = asterisk +table = cdr diff --git a/infrastructure/asterisk/config/cel.conf b/infrastructure/asterisk/config/cel.conf new file mode 100644 index 0000000..ef66b9d --- /dev/null +++ b/infrastructure/asterisk/config/cel.conf @@ -0,0 +1,4 @@ +[general] +enable = yes +apps = Dial,Queue,Bridge,Park,ParkedCall,AGI,VoiceMail +events = CHAN_START,CHAN_END,ANSWER,HANGUP,APP_START,APP_END,BRIDGE_ENTER,BRIDGE_EXIT,PARK_START,PARK_END,BLINDTRANSFER,ATTENDEDTRANSFER diff --git a/infrastructure/asterisk/config/cel_odbc.conf b/infrastructure/asterisk/config/cel_odbc.conf new file mode 100644 index 0000000..22fe677 --- /dev/null +++ b/infrastructure/asterisk/config/cel_odbc.conf @@ -0,0 +1,4 @@ +; CEL gravado via ODBC, mesma conexão "asterisk" (ver cdr_odbc.conf). +[global] +connection = asterisk +table = cel diff --git a/infrastructure/asterisk/config/extconfig.conf b/infrastructure/asterisk/config/extconfig.conf new file mode 100644 index 0000000..2138ddd --- /dev/null +++ b/infrastructure/asterisk/config/extconfig.conf @@ -0,0 +1,10 @@ +; Mapeia as famílias de configuração realtime para a conexão ODBC "asterisk" +; (definida em res_odbc.conf) e a respectiva tabela no schema "asterisk" do +; Postgres. Usado pelos objetos PJSIP referenciados em sorcery.conf. +[settings] +ps_endpoints => odbc,asterisk,ps_endpoints +ps_auths => odbc,asterisk,ps_auths +ps_aors => odbc,asterisk,ps_aors +ps_contacts => odbc,asterisk,ps_contacts +ps_endpoint_id_ips => odbc,asterisk,ps_endpoint_id_ips +ps_registrations => odbc,asterisk,ps_registrations diff --git a/infrastructure/asterisk/config/extensions.conf b/infrastructure/asterisk/config/extensions.conf new file mode 100644 index 0000000..fcc8a4b --- /dev/null +++ b/infrastructure/asterisk/config/extensions.conf @@ -0,0 +1,21 @@ +; Dialplan mínimo de validação da Fase 2 (infraestrutura). O editor +; estruturado + versionamento (dialplan_versions) chega na Fase 4 — +; Telefonia, e substituirá este arquivo estático por dialplan gerado pela +; aplicação (AEL/realtime), com backup/validação/rollback antes de ativar. +[general] +static = yes +writeprotect = no + +[globals] + +; Contexto de teste: confirma que o core do dialplan está funcionando. +[b2bcall-healthcheck] +exten => 600,1,Answer() + same => n,Playback(demo-echotest) + same => n,Echo() + same => n,Hangup() + +; Placeholder para o contexto de discagem outbound (Fase 6 — Campanhas). +[outbound] +exten => _X.,1,NoOp(B2BCall outbound placeholder - ${EXTEN}) + same => n,Hangup() diff --git a/infrastructure/asterisk/config/http.conf b/infrastructure/asterisk/config/http.conf new file mode 100644 index 0000000..3894c72 --- /dev/null +++ b/infrastructure/asterisk/config/http.conf @@ -0,0 +1,10 @@ +; Servidor HTTP embutido do Asterisk — necessário para ARI (res_ari) e para o +; WebSocket usado por ARI. Mesma política de exposição do AMI/ARI: bind em +; 0.0.0.0 pois o container roda em network_mode: host, mas o nftables bloqueia +; acesso pela interface da LAN (ver infrastructure/nftables/b2bcall.nft). +[general] +enabled = yes +bindaddr = 0.0.0.0 +bindport = 8088 +prefix = +enablestatic = no diff --git a/infrastructure/asterisk/config/logger.conf b/infrastructure/asterisk/config/logger.conf new file mode 100644 index 0000000..65b4e7c --- /dev/null +++ b/infrastructure/asterisk/config/logger.conf @@ -0,0 +1,7 @@ +[general] +dateformat = %F %T + +[logfiles] +console => notice,warning,error +messages => notice,warning,error +queue_log => queue_log diff --git a/infrastructure/asterisk/config/manager.conf.tpl b/infrastructure/asterisk/config/manager.conf.tpl new file mode 100644 index 0000000..505d3ae --- /dev/null +++ b/infrastructure/asterisk/config/manager.conf.tpl @@ -0,0 +1,19 @@ +; AMI (Asterisk Manager Interface). +; NUNCA exposto à LAN/Internet: bind em 0.0.0.0 apenas porque o container usa +; network_mode: host, mas o acesso externo é bloqueado pelo nftables +; (infrastructure/nftables/b2bcall.nft) — só loopback e a rede interna dos +; containers Docker (172.16.0.0/12) alcançam a porta 5038. +[general] +enabled = yes +port = 5038 +bindaddr = 0.0.0.0 +displayconnects = no +webenabled = no + +[${AMI_USERNAME}] +secret = ${AMI_SECRET} +deny = 0.0.0.0/0.0.0.0 +permit = 127.0.0.1/255.255.255.255 +permit = 172.16.0.0/255.240.0.0 +read = system,call,agent,user,config,command,dtmf,reporting,cdr,dialplan,originate +write = system,call,agent,user,config,command,dtmf,reporting,originate diff --git a/infrastructure/asterisk/config/odbc.ini.tpl b/infrastructure/asterisk/config/odbc.ini.tpl new file mode 100644 index 0000000..2b9d0cd --- /dev/null +++ b/infrastructure/asterisk/config/odbc.ini.tpl @@ -0,0 +1,12 @@ +; DSN do sistema usado por res_odbc (conexão nomeada "asterisk" em +; res_odbc.conf). "ConnSettings" ajusta o search_path para o schema dedicado +; "asterisk" sem misturar com o schema de domínio da aplicação ("public"). +[asterisk] +Description = B2BCall Asterisk Realtime (PostgreSQL) +Driver = PostgreSQL Unicode +Servername = ${ASTERISK_DB_HOST} +Port = ${POSTGRES_PORT} +Database = ${POSTGRES_DB} +Username = ${POSTGRES_USER} +Password = ${POSTGRES_PASSWORD} +ConnSettings = SET search_path TO asterisk, public; diff --git a/infrastructure/asterisk/config/pjsip.conf b/infrastructure/asterisk/config/pjsip.conf new file mode 100644 index 0000000..5e09a01 --- /dev/null +++ b/infrastructure/asterisk/config/pjsip.conf @@ -0,0 +1,13 @@ +; Transporte PJSIP estático (transportes não são gerenciados via realtime). +; Endpoints/auths/aors/contacts SÃO realtime (ver sorcery.conf + +; extconfig.conf) — cadastrados pela aplicação (Fase 4 — Telefonia). +; +; O Asterisk não possui IP público: este transporte escuta na rede interna/ +; privada. SIP só deve ser alcançável a partir do OpenSIPS e de troncos +; explicitamente autorizados (ver infrastructure/nftables/b2bcall.nft — +; atualmente bloqueado por padrão na interface da LAN até essas origens +; serem conhecidas). +[transport-udp] +type = transport +protocol = udp +bind = 0.0.0.0:5060 diff --git a/infrastructure/asterisk/config/queues.conf b/infrastructure/asterisk/config/queues.conf new file mode 100644 index 0000000..bf630e6 --- /dev/null +++ b/infrastructure/asterisk/config/queues.conf @@ -0,0 +1,8 @@ +; Configuração global de filas. As filas em si (Fase 5 — Call Center) serão +; gerenciadas pela aplicação. queue_log é usado por app_queue por padrão +; (grava em /var/log/asterisk/queue_log) e será consumido pelo +; apps/asterisk-events para reconciliação (ver docs/ARCHITECTURE.md). +[general] +persistentmembers = yes +autofill = yes +shared_lastcall = yes diff --git a/infrastructure/asterisk/config/res_odbc.conf b/infrastructure/asterisk/config/res_odbc.conf new file mode 100644 index 0000000..ce4c840 --- /dev/null +++ b/infrastructure/asterisk/config/res_odbc.conf @@ -0,0 +1,9 @@ +; Conexão ODBC nomeada "asterisk", usada pelo Realtime (PJSIP, CDR, CEL) via +; res_config_odbc. As credenciais reais ficam no DSN do sistema (/etc/odbc.ini, +; renderizado a partir de infrastructure/asterisk/config/odbc.ini.tpl) — este +; arquivo não contém segredos. +[asterisk] +enabled => yes +dsn => asterisk +pre-connect => yes +sanitysql => select 1 diff --git a/infrastructure/asterisk/config/sorcery.conf b/infrastructure/asterisk/config/sorcery.conf new file mode 100644 index 0000000..d4642dc --- /dev/null +++ b/infrastructure/asterisk/config/sorcery.conf @@ -0,0 +1,11 @@ +; Mapeia os objetos PJSIP (sorcery) para o backend realtime/ODBC. As tabelas +; físicas (ps_endpoints, ps_auths, ...) vivem no schema "asterisk" do Postgres +; (ver infrastructure/postgres/init/002-asterisk-realtime.sql) e o mapeamento +; nome-lógico -> conexão ODBC/tabela está em extconfig.conf. +[res_pjsip] +endpoint = realtime,ps_endpoints +auth = realtime,ps_auths +aor = realtime,ps_aors +contact = realtime,ps_contacts +endpoint_id_ip = realtime,ps_endpoint_id_ips +registration = realtime,ps_registrations diff --git a/infrastructure/docker/asterisk-entrypoint.sh b/infrastructure/docker/asterisk-entrypoint.sh new file mode 100644 index 0000000..74ec7af --- /dev/null +++ b/infrastructure/docker/asterisk-entrypoint.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# Renderiza os templates de configuração (que contêm variáveis ${VAR}) usando +# as variáveis de ambiente do container, para nunca versionar segredos em +# texto puro nos arquivos .conf. /etc/asterisk já vem populado pelo `make +# samples` no build da imagem — aqui só sobrepomos/adicionamos os arquivos +# específicos do B2BCall (o restante dos samples permanece intacto). +set -euo pipefail + +CONFIG_SRC=/etc/asterisk-templates +CONFIG_DST=/etc/asterisk + +mkdir -p "$CONFIG_DST" + +# odbc.ini é configuração do sistema (unixODBC), não do Asterisk. +if [ -e "$CONFIG_SRC/odbc.ini.tpl" ]; then + envsubst < "$CONFIG_SRC/odbc.ini.tpl" > /etc/odbc.ini +fi + +for tpl in "$CONFIG_SRC"/*.conf.tpl; do + [ -e "$tpl" ] || continue + name="$(basename "$tpl" .tpl)" + envsubst < "$tpl" > "$CONFIG_DST/$name" +done + +for f in "$CONFIG_SRC"/*.conf; do + [ -e "$f" ] || continue + name="$(basename "$f")" + cp "$f" "$CONFIG_DST/$name" +done + +chown -R asterisk:asterisk "$CONFIG_DST" /var/lib/asterisk /var/log/asterisk /var/spool/asterisk /var/run/asterisk + +exec "$@" diff --git a/infrastructure/docker/asterisk.Dockerfile b/infrastructure/docker/asterisk.Dockerfile new file mode 100644 index 0000000..e6a87a9 --- /dev/null +++ b/infrastructure/docker/asterisk.Dockerfile @@ -0,0 +1,70 @@ +# Asterisk 22 LTS compilado a partir do fonte oficial. +# Debian não distribui mais o pacote `asterisk` em seus repositórios, então +# compilamos aqui com controle total dos módulos: PJSIP (chan_sip não existe +# mais a partir do Asterisk 21), AMI, ARI, Realtime ODBC (Postgres), CDR, CEL. +FROM debian:trixie-slim + +ARG ASTERISK_VERSION=22.10.1 +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential wget ca-certificates git subversion patch \ + pkg-config autoconf automake libtool \ + libxml2-dev libsqlite3-dev uuid-dev libjansson-dev libedit-dev \ + unixodbc unixodbc-dev odbc-postgresql \ + libsrtp2-dev libssl-dev libncurses-dev libnewt-dev \ + libspeex-dev libspeexdsp-dev libogg-dev libvorbis-dev \ + libcurl4-openssl-dev libsystemd-dev gettext-base \ + odbc-postgresql \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /usr/src +RUN wget -q "https://downloads.asterisk.org/pub/telephony/asterisk/asterisk-${ASTERISK_VERSION}.tar.gz" \ + && tar xzf asterisk-${ASTERISK_VERSION}.tar.gz \ + && rm asterisk-${ASTERISK_VERSION}.tar.gz \ + && mv asterisk-${ASTERISK_VERSION} asterisk + +WORKDIR /usr/src/asterisk + +# Instala dependências adicionais específicas da distro (detectadas pelo +# próprio script oficial do Asterisk) sem prompts interativos. +RUN contrib/scripts/install_prereq install --yes || true + +RUN ./configure --with-jansson-bundled --with-pjproject-bundled --with-crypto --with-ssl=ssl + +RUN make menuselect.makeopts \ + && menuselect/menuselect \ + --enable res_odbc \ + --enable res_config_odbc \ + --enable cdr_odbc \ + --enable cdr_pgsql \ + --enable cel_odbc \ + --enable cel_pgsql \ + --enable app_queue \ + --enable res_pjsip \ + --enable res_pjsip_session \ + --enable res_ari \ + --enable res_ari_applications \ + --enable res_ari_channels \ + --enable res_ari_bridges \ + --enable CORE-SOUNDS-EN-WAV \ + --enable MOH-OPSOUND-WAV \ + menuselect.makeopts + +RUN make -j"$(nproc)" +RUN make install +RUN make samples +RUN ldconfig + +RUN groupadd -r asterisk && useradd -r -g asterisk -d /var/lib/asterisk asterisk \ + && mkdir -p /etc/asterisk /var/lib/asterisk /var/log/asterisk /var/spool/asterisk /var/run/asterisk \ + && chown -R asterisk:asterisk /etc/asterisk /var/lib/asterisk /var/log/asterisk /var/spool/asterisk /var/run/asterisk /usr/lib/asterisk + +EXPOSE 5060/udp 5060/tcp 5061/tcp 8088/tcp 8089/tcp 5038/tcp +EXPOSE 10000-20000/udp + +COPY infrastructure/docker/asterisk-entrypoint.sh /usr/local/bin/asterisk-entrypoint.sh +RUN chmod +x /usr/local/bin/asterisk-entrypoint.sh + +ENTRYPOINT ["/usr/local/bin/asterisk-entrypoint.sh"] +CMD ["asterisk", "-f", "-vvv", "-U", "asterisk", "-G", "asterisk"] diff --git a/infrastructure/nftables/b2bcall-nftables.service b/infrastructure/nftables/b2bcall-nftables.service new file mode 100644 index 0000000..a20a7c9 --- /dev/null +++ b/infrastructure/nftables/b2bcall-nftables.service @@ -0,0 +1,15 @@ +[Unit] +Description=B2BCall custom nftables rules (protects Asterisk admin ports) +After=docker.service network-online.target +Wants=network-online.target +PartOf=docker.service + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStartPre=-/usr/sbin/nft delete table inet b2bcall_fw +ExecStart=/usr/sbin/nft -f /opt/b2bcall/infrastructure/nftables/b2bcall.nft +ExecStop=/usr/sbin/nft delete table inet b2bcall_fw + +[Install] +WantedBy=multi-user.target diff --git a/infrastructure/nftables/b2bcall.nft b/infrastructure/nftables/b2bcall.nft new file mode 100644 index 0000000..0915309 --- /dev/null +++ b/infrastructure/nftables/b2bcall.nft @@ -0,0 +1,47 @@ +#!/usr/sbin/nft -f +# Regras de firewall do B2BCall. +# +# IMPORTANTE: este arquivo NUNCA usa "flush ruleset" — o Docker gerencia suas +# próprias tabelas iptables-nft (visíveis como "table ip nat" / "table ip +# filter" com o aviso "managed by iptables-nft, do not touch"). Uma tabela +# separada (`inet b2bcall_fw`) convive com elas sem conflito: cada tabela +# anexada ao hook "input" é avaliada em ordem de prioridade; um veredito de +# "drop" nesta tabela encerra o processamento do pacote antes mesmo de ele +# chegar às regras do Docker, sem jamais apagar as regras do Docker. +# +# SSH (porta 22) é sempre permitido — nunca deve ficar bloqueado. +# +# Instalado/atualizado via systemd unit b2bcall-nftables.service (ver +# infrastructure/nftables/b2bcall-nftables.service), que roda: +# nft delete table inet b2bcall_fw (ignora erro se não existir) +# nft -f este arquivo +# após o docker.service subir. + +table inet b2bcall_fw { + chain input_admin_protect { + type filter hook input priority filter - 1; policy accept; + + # Loopback sempre liberado + iifname "lo" accept + + # Conexões já estabelecidas / relacionadas sempre liberadas + ct state established,related accept + + # Redes internas dos bridges Docker (comunicação entre containers e + # entre containers <-> serviços do Asterisk em host network) + ip saddr 172.16.0.0/12 accept + + # SSH nunca é bloqueado + tcp dport 22 accept + + # --- Serviços administrativos/telefônicos sensíveis do Asterisk --- + # Nunca devem ser alcançáveis pela LAN (interface ens18). Habilitar + # exceções pontuais aqui somente quando o IP do OpenSIPS/tronco for + # conhecido e explicitamente autorizado (ver docs/OPENSIPS.md, a + # ser criado na Fase 2 de telefonia). + iifname "ens18" tcp dport { 5038, 8088, 8089 } counter drop + iifname "ens18" tcp dport { 5060, 5061 } counter drop + iifname "ens18" udp dport 5060 counter drop + iifname "ens18" udp dport 10000-20000 counter drop + } +} diff --git a/infrastructure/postgres/init/002-asterisk-realtime.sql b/infrastructure/postgres/init/002-asterisk-realtime.sql new file mode 100644 index 0000000..e4538b4 --- /dev/null +++ b/infrastructure/postgres/init/002-asterisk-realtime.sql @@ -0,0 +1,236 @@ +-- Tabelas de Realtime do Asterisk (PJSIP via res_config_odbc) + CDR/CEL +-- (cdr_pgsql / cel_pgsql), todas no schema "asterisk" (nunca em "public", +-- que é reservado ao domínio da aplicação — ver docs/ARCHITECTURE.md). +-- +-- Colunas seguem o schema oficial documentado pelo Asterisk 22 para +-- res_pjsip realtime (sorcery.conf + extconfig.conf apontam para estas +-- tabelas). Caso uma coluna adicional seja necessária futuramente +-- (ex.: campo específico de um módulo PJSIP menos comum), ela deve ser +-- adicionada via migration, nunca editada manualmente em produção. + +SET search_path TO asterisk, public; + +-- --------------------------------------------------------------------- +-- ps_endpoints +-- --------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS asterisk.ps_endpoints ( + id VARCHAR(255) PRIMARY KEY, + transport VARCHAR(255), + aors VARCHAR(255), + auth VARCHAR(255), + context VARCHAR(255) DEFAULT 'default', + disallow VARCHAR(255) DEFAULT 'all', + allow VARCHAR(255), + direct_media VARCHAR(3) DEFAULT 'yes', + connected_line_method VARCHAR(255), + direct_media_method VARCHAR(255), + direct_media_glare_mitigation VARCHAR(255), + disable_direct_media_on_nat VARCHAR(3), + dtmf_mode VARCHAR(255) DEFAULT 'rfc4733', + external_media_address VARCHAR(255), + force_rport VARCHAR(3) DEFAULT 'yes', + ice_support VARCHAR(3) DEFAULT 'no', + identify_by VARCHAR(255) DEFAULT 'username,ip', + mailboxes VARCHAR(255), + moh_suggest VARCHAR(255) DEFAULT 'default', + outbound_auth VARCHAR(255), + outbound_proxy VARCHAR(255), + rewrite_contact VARCHAR(3) DEFAULT 'yes', + rtp_symmetric VARCHAR(3) DEFAULT 'yes', + send_diversion VARCHAR(3), + send_pai VARCHAR(3), + send_rpid VARCHAR(3), + timers VARCHAR(255) DEFAULT 'yes', + timers_min_se INTEGER DEFAULT 90, + timers_sess_expires INTEGER DEFAULT 1800, + trust_id_inbound VARCHAR(3) DEFAULT 'no', + trust_id_outbound VARCHAR(3) DEFAULT 'no', + type VARCHAR(255) DEFAULT 'endpoint', + use_avpf VARCHAR(3) DEFAULT 'no', + use_ptime VARCHAR(3) DEFAULT 'no', + callerid VARCHAR(255), + callerid_privacy VARCHAR(255), + callerid_tag VARCHAR(255), + device_state_busy_at INTEGER DEFAULT 0, + language VARCHAR(255) DEFAULT 'en', + max_audio_streams INTEGER DEFAULT 1, + max_video_streams INTEGER DEFAULT 1, + media_encryption VARCHAR(255) DEFAULT 'no', + media_use_received_transport VARCHAR(3) DEFAULT 'no', + one_touch_recording VARCHAR(3) DEFAULT 'no', + record_off_feature VARCHAR(255), + record_on_feature VARCHAR(255), + rtp_engine VARCHAR(255) DEFAULT 'asterisk', + rtp_ipv6 VARCHAR(3) DEFAULT 'no', + rtp_keepalive INTEGER, + rtp_timeout INTEGER, + rtp_timeout_hold INTEGER, + sdp_owner VARCHAR(255), + sdp_session VARCHAR(255), + set_var TEXT, + srtp_tag_32 VARCHAR(3) DEFAULT 'no', + tone_zone VARCHAR(255), + tos_audio VARCHAR(255), + tos_video VARCHAR(255), + cos_audio INTEGER, + cos_video INTEGER, + allow_transfer VARCHAR(3) DEFAULT 'yes', + user_eq_phone VARCHAR(3) DEFAULT 'no', + moh_passthrough VARCHAR(3) DEFAULT 'no', + account_code VARCHAR(255), + notify_early_inuse_ringing VARCHAR(3), + max_contacts INTEGER DEFAULT 1, + remove_existing VARCHAR(3) DEFAULT 'yes', + voicemail_extension VARCHAR(255) +); +CREATE INDEX IF NOT EXISTS idx_ps_endpoints_id ON asterisk.ps_endpoints (id); + +-- --------------------------------------------------------------------- +-- ps_auths +-- --------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS asterisk.ps_auths ( + id VARCHAR(255) PRIMARY KEY, + auth_type VARCHAR(255) DEFAULT 'userpass', + nonce_lifetime INTEGER DEFAULT 32, + md5_cred VARCHAR(255), + password VARCHAR(255), + realm VARCHAR(255), + username VARCHAR(255) +); + +-- --------------------------------------------------------------------- +-- ps_aors +-- --------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS asterisk.ps_aors ( + id VARCHAR(255) PRIMARY KEY, + contact VARCHAR(255), + default_expiration INTEGER DEFAULT 3600, + mailboxes VARCHAR(255), + max_contacts INTEGER DEFAULT 1, + minimum_expiration INTEGER DEFAULT 60, + remove_existing VARCHAR(3) DEFAULT 'yes', + qualify_frequency INTEGER DEFAULT 0, + authenticate_qualify VARCHAR(3) DEFAULT 'no', + max_expiration INTEGER DEFAULT 7200, + outbound_proxy VARCHAR(255), + support_path VARCHAR(3) DEFAULT 'no', + qualify_timeout NUMERIC(5,3) DEFAULT 3.0, + voicemail_extension VARCHAR(255), + remove_unavailable VARCHAR(3) DEFAULT 'no' +); + +-- --------------------------------------------------------------------- +-- ps_contacts (populada dinamicamente pelo registro dos ramais) +-- --------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS asterisk.ps_contacts ( + id VARCHAR(255) PRIMARY KEY, + uri VARCHAR(511), + expiration_time BIGINT, + qualify_frequency INTEGER DEFAULT 0, + outbound_proxy VARCHAR(255), + path TEXT, + user_agent VARCHAR(255), + qualify_timeout NUMERIC(5,3) DEFAULT 3.0, + reg_server VARCHAR(255), + authenticate_qualify VARCHAR(3) DEFAULT 'no', + via_addr VARCHAR(255), + via_port INTEGER, + call_id VARCHAR(255), + endpoint VARCHAR(255), + prune_on_boot VARCHAR(3) DEFAULT 'no' +); +CREATE INDEX IF NOT EXISTS idx_ps_contacts_endpoint ON asterisk.ps_contacts (endpoint); + +-- --------------------------------------------------------------------- +-- ps_endpoint_id_ips (identificação de troncos por IP) +-- --------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS asterisk.ps_endpoint_id_ips ( + id VARCHAR(255) PRIMARY KEY, + endpoint VARCHAR(255), + match VARCHAR(80), + srv_lookups VARCHAR(3) DEFAULT 'yes', + match_header VARCHAR(255) +); +CREATE INDEX IF NOT EXISTS idx_ps_endpoint_id_ips_endpoint ON asterisk.ps_endpoint_id_ips (endpoint); + +-- --------------------------------------------------------------------- +-- ps_registrations (troncos com registration outbound) +-- --------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS asterisk.ps_registrations ( + id VARCHAR(255) PRIMARY KEY, + auth_rejection_permanent VARCHAR(3) DEFAULT 'yes', + client_uri VARCHAR(255), + contact_user VARCHAR(255), + expiration INTEGER DEFAULT 3600, + max_retries INTEGER DEFAULT 10, + outbound_auth VARCHAR(255), + outbound_proxy VARCHAR(255), + retry_interval INTEGER DEFAULT 60, + forbidden_retry_interval INTEGER DEFAULT 0, + server_uri VARCHAR(255), + transport VARCHAR(255), + support_path VARCHAR(3) DEFAULT 'no', + fatal_retry_interval INTEGER DEFAULT 0, + line VARCHAR(3) DEFAULT 'no', + endpoint VARCHAR(255) +); + +-- --------------------------------------------------------------------- +-- CDR (cdr_pgsql) — colunas padrão do CDR nativo do Asterisk +-- --------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS asterisk.cdr ( + id BIGSERIAL PRIMARY KEY, + accountcode VARCHAR(20), + src VARCHAR(80), + dst VARCHAR(80), + dcontext VARCHAR(80), + clid VARCHAR(80), + channel VARCHAR(80), + dstchannel VARCHAR(80), + lastapp VARCHAR(80), + lastdata VARCHAR(80), + start TIMESTAMP, + answer TIMESTAMP, + end_time TIMESTAMP, + duration INTEGER, + billsec INTEGER, + disposition VARCHAR(45), + amaflags VARCHAR(45), + uniqueid VARCHAR(150), + userfield VARCHAR(255), + linkedid VARCHAR(150), + peeraccount VARCHAR(20), + sequence INTEGER +); +CREATE INDEX IF NOT EXISTS idx_asterisk_cdr_uniqueid ON asterisk.cdr (uniqueid); +CREATE INDEX IF NOT EXISTS idx_asterisk_cdr_linkedid ON asterisk.cdr (linkedid); +CREATE INDEX IF NOT EXISTS idx_asterisk_cdr_start ON asterisk.cdr (start); + +-- --------------------------------------------------------------------- +-- CEL (cel_pgsql) +-- --------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS asterisk.cel ( + id BIGSERIAL PRIMARY KEY, + eventtype VARCHAR(30), + eventtime TIMESTAMP, + userdeftype VARCHAR(255), + cid_name VARCHAR(80), + cid_num VARCHAR(80), + cid_ani VARCHAR(80), + cid_rdnis VARCHAR(80), + cid_dnid VARCHAR(80), + exten VARCHAR(80), + context VARCHAR(80), + channame VARCHAR(80), + appname VARCHAR(80), + appdata VARCHAR(512), + accountcode VARCHAR(20), + uniqueid VARCHAR(150), + linkedid VARCHAR(150), + peer VARCHAR(80), + userfield VARCHAR(255), + extra TEXT +); +CREATE INDEX IF NOT EXISTS idx_asterisk_cel_uniqueid ON asterisk.cel (uniqueid); +CREATE INDEX IF NOT EXISTS idx_asterisk_cel_linkedid ON asterisk.cel (linkedid); +CREATE INDEX IF NOT EXISTS idx_asterisk_cel_eventtime ON asterisk.cel (eventtime);