Criando um mini servidor ACS com Python
Hoje em dia o caminho padrão é instalar o GenieACS, apontar o CPE e seguir a vida. Funciona. O efeito colateral é que muita gente opera ACS o dia inteiro sem nunca ter visto a conversa crua na porta 7547: o Inform chegando, o SOAP de resposta, o POST vazio, o Connection Request.
Neste post a gente monta um ACS mínimo em Python — sem Mongo, sem UI, sem provision. Só o suficiente para o roteador bater, você ler o Inform em memória e (quando a rede deixar) disparar um Connection Request. Validei com um ZTE ZXHN H3601P de verdade. O código está no Diretório → Scripts.
- acs-python-cwmp.py — ACS didático (stdlib)
- acs-python-smoke_inform.py — Inform fake para smoke test
Isso não substitui GenieACS em produção. Substitui a caixa-preta na cabeça do aluno.
preencha uma vez · clique no token no texto para voltar
URL no CPE: http://ACS_IP:7547/
Como a sessão CWMP acontece (na prática)
Esquece o desenho de slides por um minuto. O que o CPE faz é HTTP:
- O roteador abre TCP até o ACS na porta 7547 e manda um POST com SOAP: método
Inform. Dentro vão serial, OUI, modelo, eventos (0 BOOTSTRAP,2 PERIODIC,6 CONNECTION REQUEST…) e uma lista de parâmetros (versão de software, ConnectionRequestURL, IP de WAN…). - O ACS responde 200 com
InformResponse(MaxEnvelopes=1). - O CPE manda outro POST — muitas vezes vazio. É a vez do ACS falar: pode mandar
GetParameterValues,SetParameterValues, etc., ou responder 204 e encerrar. - Se o ACS quiser “acordar” o CPE fora do Inform periódico, faz HTTP GET no
ConnectionRequestURLque veio no Inform (às vezes com Digest). O CPE, se alcançar, inicia outra sessão (evento6 CONNECTION REQUEST).
GenieACS faz isso o tempo todo. A diferença é que aqui você vê cada passo no log.
Armadilha de lab: LAN × WAN
Alguns roteadores (no meu lab, o ZTE H3601P) não disparam TR-069 de verdade contra um ACS só na LAN. Você coloca http://192.168.1.x:7547/, salva, e… silêncio. O mesmo CPE, apontando para um ACS alcançável pela WAN (IP público / VPS), manda Inform na hora.
Não é “Python errado”. É comportamento do firmware + caminho de gerenciamento. Se o Inform não chega com ACS na LAN:
- suba o ACS num host com IP que o CPE enxergue pela WAN (lab em VPS, ou port-forward bem feito);
- libere a porta 7547/tcp no firewall;
- no CPE, URL no formato
http://ACS_IP:7547/(barra no final ajuda em vários modelos).
Outro detalhe que apareceu no teste: o ConnectionRequestURL veio em faixa 100.64/10 (CGNAT do provedor). Inform WAN → ACS público funciona; Connection Request do ACS de volta para o CPE pode falhar sem rota. Para aula de Inform/SOAP, Inform periódico já basta. CR fica como “próximo nível” quando a WAN do CPE for endereçável.
Passo 1 — subir o ACS
Python 3.10+ e zero pip (só biblioteca padrão):
curl -fsSL https://www.mauroalexandre.com.br/wp-content/uploads/mauro-diretorio/acs-python-cwmp.py -o acs.py
python3 acs.py --port 7547 -v
Se o CPE tiver usuário/senha de Connection Request:
python3 acs.py --port 7547 -v --cr-user SEU_USER --cr-pass SUA_SENHA
No terminal aparece o bind e um prompt acs> (list, show, cr, gpv). Em outro shell, smoke sem roteador:
curl -fsSL https://www.mauroalexandre.com.br/wp-content/uploads/mauro-diretorio/acs-python-smoke_inform.py -o smoke_inform.py
python3 smoke_inform.py
Esperado: OK: InformResponse recebido. Se a 7547 já estiver com GenieACS no host, pare o serviço ou use outra porta nos dois lados.
Passo 2 — o que o código está fazendo (sem magia)
O arquivo é um único acs.py de propósito: dá para ler de cima a baixo na aula.
Memória: cada Inform vira um registro por serial (manufacturer, OUI, params, ConnectionRequestURL, fila de RPCs). Cai o processo, some o estado — de propósito.
SOAP: parse com xml.etree.ElementTree, namespaces urn:dslforum-org:cwmp-1-0/1-1/1-2 (CPE mistura). Resposta mínima de Inform:
<cwmp:InformResponse>
<MaxEnvelopes>1</MaxEnvelopes>
</cwmp:InformResponse>
HTTP: ThreadingHTTPServer na 7547. POST com corpo → trata método; POST vazio → manda RPC da fila ou 204.
Connection Request: GET na URL do Inform; se vier 401 Digest, monta o challenge (comum em ZTE/Huawei).
Não tem TLS mTLS, nem multi-tenant, nem preset. É o miolo CWMP.
Passo 3 — configurar o CPE
No ZTE (menu varia: Network / Management / TR-069):
- ACS URL:
http://ACS_IP:7547/ - Periodic Inform: Enable
- Intervalo:
60no lab (em campo costuma ser bem maior) - Connection Request user/pass: anote se for testar
cr - Salve e force Inform (reboot ou botão Inform, se existir)
Lembre da nota LAN/WAN acima. No meu teste, só pela WAN o H3601P falou com o ACS.
Passo 4 — o que você deve ver no log
Quando funciona, o ACS imprime algo nesta linha:
→ Inform serial=ZTEYH8TQ6U18507 events=['0 BOOTSTRAP'] manufacturer=ZTE
param … SoftwareVersion = V9.0.0P10_MUL
param … ConnectionRequestURL = http://100.x.x.x:7547/…
← InformResponse (MaxEnvelopes=1)
→ POST vazio …
← 204 No Content
Depois do periódico:
events=['2 PERIODIC']
No prompt:
acs> list
acs> show ZTEYH8TQ6U18507
acs> gpv ZTEYH8TQ6U18507 InternetGatewayDevice.DeviceInfo.SoftwareVersion
gpv enfileira o GetParameterValues e tenta o Connection Request. Se o CR URL estiver em CGNAT, o Inform periódico seguinte ainda roda; o “acordar agora” pode não.
O que levar para o GenieACS
Quando você voltar para o GenieACS, a UI deixa de ser mistério:
- “device apareceu” = Inform + sessão HTTP que o ACS aceitou;
- “Summon” = Connection Request no URL que o CPE anunciou;
- fault / sessão estranha = SOAP/HTTP no meio do caminho, não “Mongo bravo” na primeira hipótese.
O ACS Python é o laboratório de protocolo. O GenieACS é a ferramenta de operação. Os dois se complementam — desde que a gente não confunda um com o outro.
Fechando
Se a meta é entender CWMP, um Inform + InformResponse bem lidos valem mais do que três slides de arquitetura. Sobe o script, aponta o CPE por um caminho que ele aceite (muitas vezes WAN), e olha o log.
Dúvida ou log de outro vendor: comenta no post. Gostou? Veja os cursos — TR-069 na prática, com lab.