Tudo começou com um problema chato: o meu servidor Proxmox passou a travar e desligar sozinho, às vezes um minuto depois de ligar. Os logs terminavam de repente, sem kernel panic, sem erro de hardware, sem nada. Investigando, percebi que eu simplesmente não tinha visibilidade do que acontecia no homelab: nem no servidor, nem na rede, nem nos links de internet.
Este post conta como montei uma stack de monitoramento completa, que hoje cobre a rede UniFi (gateway, access points, switch e clientes), as duas WANs, os logs do roteador, o tráfego por cliente via NetFlow, o Proxmox e os dois Pi-holes, tudo em uma VM pequena, com Docker Compose. Todos os arquivos estão no GitHub: github.com/rmmarconi/homelab.
O homelab
- Servidor: Lenovo ThinkCentre Tiny (i7-4785T, 16 GB, SSD de 1 TB) com Proxmox VE 9.
- Rede: UniFi Cloud Gateway Fiber, dois U7 Pro e um USW Flex 2.5G 8 PoE.
- Internet: duas WANs PPPoE em balanceamento ponderado (Go Internet e Eletrodata, 500 Mbps cada) e Starlink como failover.
- DNS: dois Pi-hole v6 em containers LXC, sincronizados com o nebula-sync.
- VMs: um webserver, o openclaw e agora a VM
monitor.
A stack
Escolhi ferramentas leves e bem integradas ao Grafana. A página awesome-unifi.com lista várias opções para UniFi. Usei o UnPoller e deixei de fora o ELK e o Graylog, que pedem de 4 a 8 GB só para eles. O Loki faz o mesmo trabalho com uma fração da memória.
UniFi API ─────► UnPoller ────────┐
Proxmox API ───► pve-exporter ────┤
Pi-hole API ───► pihole-exporter ─┼──► Prometheus ──┐
node-exporter ─► (pve, piholes) ──┘ ├──► Grafana
UniFi SIEM :514 ──► Vector ──► Loki ────────────────┘
UniFi NetFlow :2055 ► GoFlow2 ► Vector ► Loki + Prometheus
| Componente | Para que serve |
|---|---|
| UnPoller | Lê a API da controladora UniFi e exporta métricas de dispositivos, clientes, DPI e WANs. |
| Prometheus | Armazena as séries temporais (90 dias). |
| Loki | Armazena logs e fluxos (30 dias). |
| Vector | Recebe os eventos SIEM do roteador e os fluxos NetFlow, interpreta e distribui. |
| GoFlow2 | Coletor NetFlow v9 / IPFIX / sFlow. |
| pve-exporter e node-exporter | Proxmox: VMs, containers, storages, CPU, RAM, disco e temperatura. |
| pihole-exporter | Consultas, bloqueios e clientes de cada Pi-hole. |
| Grafana | Dashboards, com datasources e painéis provisionados por arquivo. |
A VM tem 2 vCPU e 4 GB de RAM. Cada container tem um mem_limit, para que uma consulta pesada no Loki não derrube o resto. Em uso normal, a stack inteira consome menos de 500 MB.
Passo 1: métricas da UniFi com UnPoller
O UnPoller precisa de um usuário local somente leitura na UniFi (ou de uma API key). Criei o usuário monitor e, em vez de colocar a senha no docker-compose.yml, ela fica no .env, fora do git. O repositório tem um script que grava o valor sem mostrar na tela:
./scripts/set-secret.sh UP_UNIFI_DEFAULT_PASS
Primeira pegadinha: a VM está na VLAN de servidores, e o firewall zone-based da UniFi bloqueia por padrão o acesso à interface do gateway a partir dessa VLAN. O ping passava, mas a porta 443 dava timeout. A solução foi uma regra Allow com origem no IP da VM e destino na zona Gateway, TCP 443.
Com isso, o UnPoller passou a exportar pouco mais de 3 mil métricas a cada minuto: o gateway, os dois U7 Pro, o switch, 27 clientes e o DPI por aplicação. Para visualizar, usei os dashboards oficiais do UnPoller (UAP, USW, USG, Client Insights, Client DPI e Network Sites), que são provisionados automaticamente.
Passo 2: quanto trafegou em cada WAN
Eu queria um número exato: quanto baixei e quanto enviei em cada operadora hoje e neste mês. O UnPoller exporta contadores de bytes por interface (unpoller_device_wan_receive_bytes_total e ..._transmit_bytes_total), mas com o label port="eth1" ou port="eth4", sem o nome da WAN.
Para descobrir qual interface era de qual operadora, cruzei duas informações. As métricas de porta do gateway mostram qual porta tem qual IP público. Um whois nesses IPs devolve o dono: a Porta 2 (eth1) pertence ao AS da Go Internet e a Porta 5 (eth4) ao AS da Eletrodata. O próprio UnPoller confirma isso na métrica unpoller_wan_service_provider_asn.
Em vez de espalhar esse mapeamento pelas consultas, coloquei o nome na origem, com metric_relabel_configs no Prometheus:
metric_relabel_configs:
- source_labels: [__name__, port]
regex: "unpoller_device_wan_.+;eth1"
target_label: wan
replacement: "Go Internet"
- source_labels: [__name__, port]
regex: "unpoller_device_wan_.+;eth4"
target_label: wan
replacement: "Eletrodata"
O dashboard Consumo por WAN mostra:
- download e upload de cada WAN em três blocos: no período selecionado, hoje e este mês. Os dois últimos usam o relative time do painel (
now/denow/M); - throughput em gráfico espelhado (download para cima, upload para baixo);
- total por dia nos últimos 30 dias;
- o percentual do link contratado em uso;
- o estado de cada WAN (ativa ou backup), além de erros e descartes.
Passo 3: os logs do roteador, e a maior pegadinha
A UniFi envia eventos para um servidor SIEM (Settings → Control Plane → Integrations → Activity Logging). Configurei o Grafana Alloy para receber syslog e… nada. O Alloy só registrava invalid or unsupported framing, e o tcpdump marcava os pacotes como (invalid).
Olhando o conteúdo cru do pacote, o motivo ficou claro. O UniFi manda CEF sem o cabeçalho <PRI> do syslog:
Sep 26 20:12:25 gateway CEF:0|Ubiquiti|UniFi Network|10.6.106|401|WiFi Client Disconnected|2|UNIFIcategory=Client Devices ...
Os parsers de syslog, tanto em RFC 3164 quanto em RFC 5424, esperam que a mensagem comece com <número>. A solução foi trocar o Alloy pelo Vector: ele recebe o socket UDP como texto puro e já traz a função parse_cef na linguagem VRL. Cada evento vira uma linha no Loki com os labels category, event e severity:
if starts_with(string!(rest), "CEF:") {
c, e = parse_cef(string!(rest))
if e == null {
.event = c.name
.severity = c.severity
.category = to_string(c.UNIFIcategory)
}
}
O dashboard Logs do Roteador tem contadores (eventos, severidade alta, desconexões de clientes, acessos administrativos), eventos por categoria ao longo do tempo, os eventos mais frequentes e o painel de logs com filtros e busca.
Passo 4: NetFlow, ou quem está usando a internet
O Cloud Gateway exporta NetFlow v9/IPFIX. Para coletar, usei o GoFlow2, que ouve na porta UDP 2055 e escreve cada fluxo como JSON no stdout. O Vector lê esses logs direto do Docker (fonte docker_logs) e, para cada fluxo:
- classifica a direção (download, upload, interno ou externo) comparando os IPs com as redes locais. É preciso incluir o prefixo IPv6 público delegado, senão todo o tráfego IPv6 aparece como “externo”;
- identifica o serviço pela porta remota: HTTPS, QUIC, DNS, STUN/WebRTC, WireGuard/Tailscale etc.;
- multiplica bytes e pacotes pela taxa de amostragem;
- envia o fluxo completo ao Loki e gera contadores Prometheus por cliente, serviço e protocolo.
O detalhe de que mais gostei: o nome de cada cliente não é resolvido no pipeline. Ele vem de um join no PromQL com as métricas do UnPoller, que já sabe o nome de cada dispositivo:
sum by (local_ip) (rate(netflow_bytes_total{direction="download"}[5m])) * 8
* on(local_ip) group_left(name)
(label_replace(max by (ip, name) (unpoller_client_uptime_seconds),
"local_ip", "$1", "ip", "(.*)") * 0 + 1)
O dashboard NetFlow mostra os 10 clientes que mais baixaram e os 10 que mais enviaram, o consumo por serviço e protocolo, os principais destinos e portas remotas, e a lista dos fluxos com busca.
Passo 5: Proxmox e Pi-hole
Proxmox: o prometheus-node-exporter no host fornece CPU, memória, disco, rede e, principalmente, as temperaturas, que eram exatamente o que eu precisava para investigar os travamentos. O prometheus-pve-exporter usa um token de API de um usuário com papel PVEAuditor, somente leitura, e exporta o estado e o consumo de cada VM, container e storage.
Pi-hole v6: o pihole-exporter se autentica com uma app password. A pegadinha aqui: o nebula-sync sincroniza a configuração, mas não as app passwords, e, se uma instância falha na autenticação, o exporter não publica métricas de nenhuma. A solução foi passar uma senha por host, separadas por vírgula.
O monitoramento dos Pi-holes já rendeu uma descoberta: um deles tinha 79 mil domínios na lista de bloqueio e o outro, 1,37 milhão. A sincronização das listas estava quebrada, e os clientes tinham bloqueios diferentes conforme o DNS que respondia.
E o Proxmox que travava?
A investigação ainda está em andamento, mas os dados já descartaram vários suspeitos:
- RAM: o Memtest86+ passou sem erros.
- Disco: o SMART está limpo.
- Temperatura: 58 °C no momento do travamento, com a CPU ociosa.
- Placa de rede: ela já travou no passado (o conhecido Hardware Unit Hang da e1000e), mas isso foi resolvido meses atrás desligando o offload.
A pista mais forte veio de cruzar os logs de dois computadores diferentes: o mediacenter, uma máquina física separada, caiu e voltou no mesmo minuto que o Proxmox em várias ocasiões. Isso aponta para a alimentação elétrica. Enquanto isso, limitei os C-states da CPU (intel_idle.max_cstate=1), o que aumentou bastante o tempo de uptime. O journal do Proxmox agora é enviado em tempo real para outra máquina, para que o próximo travamento fique registrado.
Lições aprendidas
- Se
tcpdumpdiz(invalid), olhe o payload comtcpdump -A. Não confie que o fabricante segue a RFC. docker compose up -dnão percebe mudanças noenv_file: use--force-recreate.- Arquivos montados individualmente num container não acompanham a substituição do arquivo no host: reinicie o container (ou monte o diretório).
- Arquivos
._*do macOS vão junto numtare quebram o provisionamento do Grafana. UseCOPYFILE_DISABLE=1. - Segredos nunca no compose nem no chat:
.envcomchmod 600e um script que lê o valor comread -s.
Próximos passos
O repositório rmmarconi/homelab vai ganhar as pastas terraform/, para criar as VMs e os containers no Proxmox, e ansible/, para configurar os hosts e fazer o deploy desta stack. A ideia é conseguir recriar o homelab inteiro a partir do git. Também quero adicionar alertas (WAN caída, Proxmox fora do ar, temperatura, Pi-hole parado) e um painel de correlação entre os eventos de energia e os travamentos.
Se quiser replicar, o README da pasta monitoring tem o passo a passo completo.