Cloner de Sites Públicos — Referência Técnica¶
Versão concisa em português. Para detalhes mais profundos (algoritmo de captura browser engine, rewrite de URLs, tuning de performance), consulte
CLONER.md(inglês).Status do escopo (pós-congelamento de escopo 2026-05-10) — CLONER agora é formalmente MÓDULO CLONER.Art com 9 funções (não só clonagem web). Veja
modules/cloner-art.mdpara o detalhamento canônico das 9 funções ehelp-center/primers/obp-operator-bridge-proxy.mdpara integração de egress em air-gap.
1. Para que serve¶
O Cloner baixa sites públicos reais (notícias, e-commerce, mídia, etc.) e produz uma cópia local servível pela frota de Cloned Personas. Dessa forma a frota de agentes pode testar inspeção TLS pelo NGFW usando conteúdo real de sites conhecidos — sem depender de conexão à internet durante os testes.
2. Por que existe¶
A inspeção TLS performante de sites reais é o caso de uso central de NGFWs modernos. As 20 Synthetic Personas servem conteúdo determinístico (Caddy estático, mock APIs, HAR replay), o que cobre superfície TLS mas não a heterogeneidade real da web. Os 10 slots de Cloned Persona resolvem isso: o Cloner captura sites reais e replica nas slots, mantendo o teste reprodutível mas representativo.
3. Arquitetura¶
┌─────────────────────────────────────────────────────────────────────┐
│ Cloner Pod (deployment: cloner, namespace: web-agents) │
│ │
│ Internet (TCP 80/443, UDP 443/QUIC, DNS, ICMP) │
│ ▲ │
│ │ net1 ─── VLAN 40 macvlan (DHCP via CNI plugin) │
│ ┌───────────┴────────────────────────────────────────────────┐ │
│ │ browser engine headful + plugin stealth │ │
│ │ (headless browser, navigator.webdriver = undefined) │ │
│ └───────────┬────────────────────────────────────────────────┘ │
│ │ │
│ ▼ escreve em │
│ /mnt/cloned/{personaName}/ (PVC NFS RWX cloned-sites) │
│ │
│ Dashboard (eth0 / OOBI) ◄──── controla jobs, heartbeat │
└─────────────────────────────────────────────────────────────────────┘
▼ NFS server in-cluster (k8s/dut/35-nfs-server.yaml)
NFS server (web-agents/nfs-server) — backed by hostPath
em UCS-4 (multi-node) ou no único nó (single-node).
▼ exportado via NFSv4 / OOBI ClusterIP
PVCs por namespace (k8s/dut/36-cloned-sites-pvs.yaml)
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Cloned Persona Pods (clone-persona-1 … clone-persona-10) │
│ VLANs 200–209 / 10.2.{1..10}.0/27 │
│ Caddy: file_server /mnt/cloned/{SITE_NAME} │
└─────────────────────────────────────────────────────────────────────┘
▲ NGFW inspeciona TLS
┌─────────────────────────────────────────────────────────────────────┐
│ Agentes (browser-engine VLAN 20, synthetic-load VLAN 30) │
│ https://cloned-{n}.persona.internal/ → roteado via NGFW │
└─────────────────────────────────────────────────────────────────────┘
Separação de planos de rede:
- Cloner baixa via
net1(VLAN 40) → ISP → internet pública. NGFW não está nesse caminho. - Agentes testam via
net1(VLAN 20/30) → NGFW → Cloned Persona Caddy. NGFW está nesse caminho. - Storage NFS entre cloner e slots passa SOMENTE pela OOBI (eth0 / pod network). Nunca toca o data plane.
4. Rede¶
| Interface | Função | VLAN | Subnet |
|---|---|---|---|
| eth0 | OOBI / pod network: Dashboard, NFS, DNS | — | k8s pod CIDR |
| net1 | macvlan VLAN 40: egresso ISP via DHCP | 40 | DHCP do roteador upstream |
DNS forçado (via dnsPolicy: None):
- 10.43.0.10 → CoreDNS (k3s)
- 10.96.0.10 → CoreDNS (kubeadm)
- 8.8.8.8 → Google Public DNS (sites externos)
- 208.67.222.222 → OpenDNS (fallback)
Policy routing: pacotes não-RFC1918 (TCP/UDP/ICMP) ganham fwmark 100 e usam tabela 100 (default via gateway DHCP). Tráfego RFC1918 (incluindo OOBI e NFS via 10.x.x.x) segue eth0.
CNI DHCP daemon (k8s/dut/30-cni-dhcp-daemon.yaml): DaemonSet em todos os nós que expõe o socket /run/cni/dhcp.sock exigido pelo IPAM tipo dhcp da NAD VLAN 40.
5. Storage — NFS in-cluster, OOBI only¶
O volume cloned-sites é compartilhado entre o Cloner (escritor) e os 10 slots Cloned Persona (leitores). Como PVCs são namespace-scoped e os slots vivem em 10 namespaces separados, o compartilhamento é feito por 11 pares estáticos PV/PVC apontando para o mesmo export do NFS server in-cluster.
| Componente | Manifest | Propósito |
|---|---|---|
| NFS Deployment + Service | k8s/dut/35-nfs-server.yaml |
Servidor NFSv4 réplica única, nodeAffinity prefere role=infra. Backed por hostPath /var/lib/agent-cluster/cloned-sites. Recreate strategy garante writer único. InitContainer prepare-share chowna pra UID 10004 antes do daemon iniciar. |
| 11 PV/PVC estáticos | k8s/dut/36-cloned-sites-pvs.yaml |
1 par RWX writer (web-agents/cloned-sites) + 10 ROX readers (clone-persona-N/cloned-sites). Todos apontam pro mesmo export. storageClassName: "" previne CSI race. |
Topologia multi-node:
- Cloner roda em
role=infra(UCS-4) — mesmo nó do NFS server. - Slots Cloned Persona rodam em
role=ngfw-dut(UCS-1). - Tráfego NFS flui via OOBI (eth0 → Service
nfs-server.web-agents.svc.cluster.local:2049). - Data plane (
net1macvlan VLAN 40 + 200-209) nunca carrega I/O de storage.
Topologia single-node: tudo no mesmo nó. NFS round-trip é loopback, overhead desprezível.
| Atributo | Valor |
|---|---|
| Backend | NFSv4 in-cluster Service |
| Capacidade | 50 Gi (claim) — limitada pelo disco do nó do NFS server |
| Access mode (writer) | ReadWriteMany |
| Access mode (10 readers) | ReadOnlyMany |
| Mount (Cloner) | /mnt/cloned (read-write) |
| Mount (Cloned Personas) | /mnt/cloned (read-only) |
| Plano de rede | OOBI somente (eth0) |
| Co-localização multi-node | Cloner + NFS server no mesmo nó (role=infra) |
6. Lifecycle de um job¶
- Operador cria job:
POST /api/clone/jobs {url, personaName: "shop"}. - Cloner faz long-poll em
/api/clone/agents/{id}/jobe claima o job. - browser engine headful (com plugin stealth) navega até a URL, intercepta cada response via
route.fetch(), captura body+headers. - Reescrita HTML:
(href|src|action)="${origin}→="(paths relativos). - Persistência:
/mnt/cloned/{personaName}/index.html+ assets (mesmo domínio em pasta original, cross-origin sob_ext/). - Cloner reporta:
PATCH /api/clone/jobs/{id} {status: "completed", assetCount, totalBytes}. - Operador binda slot:
PATCH /api/clone/persona-slots/N {siteName: "shop", replicas: 1}. - Dashboard: atualiza ConfigMap (SITE_NAME), escala Deployment, upsert na tabela
targets(a URLhttps://cloned-N.persona.internal/vira target automaticamente — ver PR #155). - Stakater Reloader detecta ConfigMap → reinicia pod.
- Caddy slot sobe servindo
/mnt/cloned/shop/no IP da macvlan VLAN 200+N-1. - Agentes browser-engine/synthetic-load puxam a nova target no próximo poll, navegam pela NGFW.
7. Troubleshooting essencial¶
| Sintoma | Verificar |
|---|---|
| Cloner em pod errado (multi-node) | kubectl get pod -o wide — NODE deve ser UCS-4 |
net1 sem IP |
kubectl get ds cni-dhcp-daemon -n web-agents (Ready em todo nó) |
Slot pod MountVolume.SetUp failed |
kubectl get pod -n web-agents -l app.kubernetes.io/name=nfs-server -o wide |
PVC cloned-sites Pending |
kubectl describe pv cloned-sites-writer |
| Slots Ready mas servindo vazio | Cloner gravou em outro nó? Reinicie NFS depois cloner |
| Sem internet | kubectl exec deploy/cloner -- ping -c 3 -I net1 8.8.8.8 |
Detalhes operacionais: CLONER_OPERATIONS.pt-BR.md.
8. Arquivos-chave¶
| Arquivo | Descrição |
|---|---|
cloner/src/cloner.ts |
Engine de cloning com browser engine + stealth |
cloner/src/index.ts |
Loop principal: register, heartbeat, poll, run |
cloner/src/health-monitor.ts |
Ping ICMP + métricas Prometheus |
k8s/80-cloner-nad.yaml |
NAD ISP (master: eth1.40, IPAM dhcp) |
k8s/81-cloner-deployment.yaml |
Deployment do cloner com initContainer routing |
k8s/dut/30-cni-dhcp-daemon.yaml |
DaemonSet CNI DHCP (necessário pra IPAM da VLAN 40) |
k8s/dut/35-nfs-server.yaml |
NFS server in-cluster pra cloned-sites cross-node |
k8s/dut/36-cloned-sites-pvs.yaml |
11 PV/PVC estáticos (1 writer + 10 readers) |
dashboard/src/app/api/clone/ |
APIs de jobs, slots e agents do cloner |
© 2026 André Luiz Gallon — Distribuído sob PolyForm Noncommercial 1.0.0 com Restrições Adicionais de Uso (Apêndice A).