## https://sploitus.com/exploit?id=1FF17BFB-42C6-5866-9667-464BD03C4921
# Aerobi POC — Simulação local de monitoramento de câmeras
Laboratório 100% local (sem Cloudflare, sem câmeras reais) para validar a
arquitetura futura de monitoramento multi-aeródromo: VPN privada com
**Headscale + Tailscale**, streaming via **MediaMTX**, orquestração em
**NestJS**, player web em **React + hls.js**.
> Tudo sobe com `docker compose up`. Câmeras são simuladas pelo `ffmpeg`
> (color bars + áudio sintético). Nenhum hardware é necessário.
---
## Sumário
- [Arquitetura](#arquitetura)
- [Fluxo do vídeo](#fluxo-do-v%C3%ADdeo)
- [Papel de cada serviço](#papel-de-cada-servi%C3%A7o)
- [Como subir o ambiente](#como-subir-o-ambiente)
- [Como testar streams](#como-testar-streams)
- [Como validar a VPN](#como-validar-a-vpn)
- [Como validar HLS](#como-validar-hls)
- [Como acessar o frontend](#como-acessar-o-frontend)
- [Como debugar MediaMTX](#como-debugar-mediamtx)
- [Como debugar Headscale](#como-debugar-headscale)
- [Estrutura do projeto](#estrutura-do-projeto)
- [Limites do POC](#limites-do-poc)
---
## Arquitetura
```
+-----------------------------+
| Browser |
| http://localhost:5173 |
| http://localhost:3001 |
+-------------+---------------+
|
(rede do host) |
v
+---------------------------- VPS-NET ----------------------------+
| |
| +-------------+ +----------+ +-----------------------+ |
| | frontend | | postgres | | tailscale-vps | |
| | (nginx) | | | | (TUN, hostname vps-1) | |
| +------+------+ +-----+----+ +-----------+-----------+ |
| | | | |
| | (TCP) | | network_mode |
| | | v |
| | +-----+----+ +------------------+ |
| +--------->| postgres| mediamtx ---> nest-api ---> browser (hls.js)
^ ^ (proxy)
| |
cam-1..cam-4
```
1. `ffmpeg` gera 4 streams `testsrc` em loop infinito e publica via RTSP
no MediaMTX (`rtsp://127.0.0.1:8554/cam-N`).
2. MediaMTX remuxa para HLS low-latency e expõe `http://aerodrome-1:8888/cam-N/index.m3u8`
**somente na tailnet** (porta nunca é publicada no host).
3. NestJS recebe `GET /streams/:cameraId/index.m3u8` do browser, busca a
playlist no MediaMTX via tailnet e faz **passthrough HTTP** (sem ffmpeg,
sem transcoding) — apenas pipe de bytes.
4. hls.js no browser consome o m3u8 do NestJS como qualquer HLS comum.
---
## Papel de cada serviço
| serviço | papel | observações |
|---|---|---|
| `headscale` | control plane VPN (signaling + ACL + MagicDNS) | nunca processa tráfego de dados; só coordena |
| `headscale-bootstrap` | run-once: cria user `aerobi-poc` e duas pre-auth keys em `/shared/keys.env` | encerra após executar |
| `tailscale-vps` | nó Tailscale "VPS" (hostname `vps-1`) | publica a porta 3000 do nest-api ao host |
| `tailscale-aerodrome` | nó Tailscale "Aeródromo" (hostname `aerodrome-1`) | privado: nenhuma porta exposta ao host |
| `postgres` | banco do nest-api | uso interno na vps-net |
| `nest-api` | API REST + proxy HLS | **não** processa vídeo; apenas pipe HTTP |
| `mediamtx` | RTSP→HLS | escuta nas interfaces do tailscale-aerodrome |
| `ffmpeg-fake-camera` | gera 4 streams `testsrc` e publica RTSP | substituível por câmeras reais no futuro |
| `frontend` | nginx servindo o build Vite/React + hls.js | bate em `http://localhost:3001` |
---
## Como subir o ambiente
Pré-requisitos: Docker (≥ 24) e Docker Compose v2.
```bash
cd aerobi-poc
cp .env.example .env # opcional — default já funciona
docker compose build
docker compose up -d
```
Os tailscales só ficam prontos depois que o `headscale-bootstrap` termina.
Acompanhe:
```bash
docker compose logs -f headscale headscale-bootstrap tailscale-vps tailscale-aerodrome
```
Quando aparecer `Success.` em ambos os tailscales, a tailnet está formada.
Para derrubar tudo (preservando volumes):
```bash
docker compose down
```
Para reset total (apaga banco, estado do tailscale, tudo):
```bash
docker compose down -v
```
---
## Como testar streams
Liste os aeródromos:
```bash
curl -s http://localhost:3001/aerodromes | jq
```
Liste as câmeras de um aeródromo:
```bash
curl -s http://localhost:3001/aerodromes/aero-1/cameras | jq
```
Cada item tem `streamUrl` apontando para o proxy do NestJS, p.ex.
`http://localhost:3001/streams/cam-1/index.m3u8`.
Pegue a playlist HLS via proxy:
```bash
curl -s http://localhost:3001/streams/cam-1/index.m3u8
```
Você deve receber um manifest m3u8 com `#EXT-X-VERSION:9` e segmentos
`.mp4`/`.m4s` (low-latency HLS).
---
## Como validar a VPN
Listar nós conhecidos pelo Headscale:
```bash
docker compose exec headscale headscale nodes list
```
Esperado: `vps-1` e `aerodrome-1`, ambos online.
Status do tailscale na VPS:
```bash
docker compose exec tailscale-vps tailscale status
```
Ping do tailscale-vps até o aeródromo (via interface tailnet):
```bash
docker compose exec tailscale-vps tailscale ping aerodrome-1
```
Resolução MagicDNS dentro do nest-api (que herda a stack do tailscale-vps):
```bash
docker compose exec tailscale-vps wget -qO- http://aerodrome-1:8888/cam-1/index.m3u8 | head
```
---
## Como validar HLS
Direto no MediaMTX (via tailnet, a partir do nó da VPS):
```bash
docker compose exec tailscale-vps wget -qO- http://aerodrome-1:8888/cam-1/index.m3u8 | head
```
Via proxy no NestJS (a partir do host):
```bash
curl -s http://localhost:3001/streams/cam-1/index.m3u8 | head
```
Os dois devem retornar a mesma playlist.
Para reproduzir manualmente fora do browser:
```bash
ffplay -loglevel warning http://localhost:3001/streams/cam-1/index.m3u8
# ou
vlc http://localhost:3001/streams/cam-1/index.m3u8
```
---
## Como acessar o frontend
`http://localhost:5173`
1. Selecione um aeródromo no dropdown.
2. As câmeras do aeródromo aparecem em grid.
3. Cada player faz request para `http://localhost:3001/streams//index.m3u8`,
que internamente sai pela tailnet até o MediaMTX.
> **Importante**: o JS do frontend usa `VITE_API_URL` (default `http://localhost:3001`).
> O valor é gravado no build — ajuste no `.env` antes de `docker compose build`
> se for acessar de outra máquina.
---
## Como debugar MediaMTX
Logs:
```bash
docker compose logs -f mediamtx
```
Ver paths ativos (do dentro do container — porta só na tailnet):
```bash
docker compose exec tailscale-vps wget -qO- http://aerodrome-1:9997/v3/paths/list
```
(Habilite `api: yes` no `mediamtx.yml` se quiser usar o endpoint admin.)
Ver o que ffmpeg está publicando:
```bash
docker compose logs -f ffmpeg-fake-camera
docker compose exec ffmpeg-fake-camera tail -f /tmp/cam-1.log
```
Problemas comuns:
- **Player fica preto**: a primeira playlist HLS pode demorar 3–5s para
ter segmentos. Recarregue.
- **404 no proxy**: verifique se ffmpeg está conectado. Veja
`docker compose logs mediamtx` por linhas tipo `[RTSP] [conn] opened`.
---
## Como debugar Headscale
Healthcheck (a porta `80` do container fica exposta no host como `8080`):
```bash
curl -s http://localhost:8080/health
```
Logs:
```bash
docker compose logs -f headscale
```
Listar usuários e nós:
```bash
docker compose exec headscale headscale users list
docker compose exec headscale headscale nodes list -o json | jq '.[] | {hostname, online, ips: .ip_addresses}'
```
Recriar pre-auth keys (caso `keys.env` tenha sido perdido):
```bash
docker compose down
docker volume rm aerobi-poc_headscale-shared
docker compose up -d
```
ACLs estão em `headscale/acl.hujson` (tudo permite tudo no POC). Para
testar mudanças sem reiniciar o headscale:
```bash
docker compose exec headscale headscale policy check
```
---
## Estrutura do projeto
```
aerobi-poc/
├── docker-compose.yml
├── .env.example
├── README.md
├── plano.md
├── headscale/
│ ├── config.yaml
│ ├── acl.hujson
│ └── bootstrap.sh # cria user + pre-auth keys
├── tailscale-node/ # imagem comum aos dois clients tailscale
│ ├── Dockerfile
│ └── entrypoint.sh
├── nest-api/
│ ├── Dockerfile
│ ├── package.json
│ ├── tsconfig.json
│ ├── nest-cli.json
│ └── src/
│ ├── main.ts
│ ├── app.module.ts
│ ├── common/health.controller.ts
│ ├── aerodromes/{entity, service, controller, module}.ts
│ ├── cameras/{entity, service, module}.ts
│ ├── streams/{controller, hls-proxy.service, module}.ts
│ └── seed/seed.service.ts
├── frontend/
│ ├── Dockerfile
│ ├── nginx.conf
│ ├── package.json
│ ├── tsconfig.json
│ ├── vite.config.ts
│ ├── index.html
│ └── src/
│ ├── main.tsx
│ ├── App.tsx
│ ├── styles.css
│ ├── api/client.ts
│ ├── components/{Player, AerodromeSelector, CameraGrid}.tsx
│ └── pages/Home.tsx
└── aerodrome-node/
├── mediamtx/mediamtx.yml
└── ffmpeg/
├── Dockerfile
└── entrypoint.sh
```
---
## Limites do POC
Fora do escopo (intencionalmente):
- Cloudflare, TLS, autenticação real
- Câmeras IP reais, ONVIF, descoberta automática
- NVR / replay / persistência de gravações
- Escala horizontal real (vários aeródromos físicos)
- CI/CD e testes automatizados