Cloner de Sitios Públicos — Referencia Técnica¶
Versión concisa en español. Para detalles más profundos (algoritmo de captura browser engine, reescritura de URLs, tuning de rendimiento) consulte
CLONER.md(inglés).Estado del alcance (post-congelación de alcance 2026-05-10) — CLONER ahora es formalmente MÓDULO CLONER.Art con 9 funciones (no sólo clonación web). Vea
modules/cloner-art.mdpara el desglose canónico de las 9 funciones yhelp-center/primers/obp-operator-bridge-proxy.mdpara la integración de egress air-gap.
1. Para qué sirve¶
El Cloner descarga sitios públicos reales (noticias, e-commerce, media, etc.) y produce una copia local servible por la flota de Cloned Personas. De esta forma, la flota de agentes puede probar inspección TLS a través del NGFW usando contenido real de sitios conocidos — sin depender de internet durante las pruebas.
2. Por qué existe¶
La inspección TLS de sitios reales es el caso de uso central de NGFWs modernos. Las 20 Synthetic Personas sirven contenido determinístico (Caddy estático, mock APIs, replay HAR), lo cual cubre la superficie TLS pero no la heterogeneidad real de la web. Los 10 slots de Cloned Persona resuelven eso: el Cloner captura sitios reales y los replica en los slots, manteniendo la prueba reproducible pero representativa.
3. Arquitectura¶
┌─────────────────────────────────────────────────────────────────────┐
│ Cloner Pod (deployment: cloner, namespace: web-agents) │
│ │
│ Internet (TCP 80/443, UDP 443/QUIC, DNS, ICMP) │
│ ▲ │
│ │ net1 ─── VLAN 40 macvlan (DHCP vía CNI plugin) │
│ ┌───────────┴────────────────────────────────────────────────┐ │
│ │ browser engine headful + plugin stealth │ │
│ │ (headless browser, navigator.webdriver = undefined) │ │
│ └───────────┬────────────────────────────────────────────────┘ │
│ │ │
│ ▼ escribe en │
│ /mnt/cloned/{personaName}/ (PVC NFS RWX cloned-sites) │
│ │
│ Dashboard (eth0 / OOBI) ◄──── controla jobs, heartbeat │
└─────────────────────────────────────────────────────────────────────┘
▼ servidor NFS in-cluster (k8s/dut/35-nfs-server.yaml)
NFS server (web-agents/nfs-server) — respaldado por hostPath
en UCS-4 (multi-node) o en el único nodo (single-node).
▼ exportado vía 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 inspecciona TLS
┌─────────────────────────────────────────────────────────────────────┐
│ Agentes (browser-engine VLAN 20, synthetic-load VLAN 30) │
│ https://cloned-{n}.persona.internal/ → ruteado vía NGFW │
└─────────────────────────────────────────────────────────────────────┘
Separación de planos de red:
- Cloner descarga vía
net1(VLAN 40) → ISP → internet pública. NGFW no está en ese camino. - Agentes prueban vía
net1(VLAN 20/30) → NGFW → Cloned Persona Caddy. NGFW está en ese camino. - Storage NFS entre cloner y slots pasa SOLO por la OOBI (eth0 / pod network). Nunca toca el data plane.
4. Red¶
| Interfaz | Función | VLAN | Subnet |
|---|---|---|---|
| eth0 | OOBI / pod network: Dashboard, NFS, DNS | — | k8s pod CIDR |
| net1 | macvlan VLAN 40: egreso ISP vía DHCP | 40 | DHCP del router upstream |
DNS forzado (vía dnsPolicy: None):
- 10.43.0.10 → CoreDNS (k3s)
- 10.96.0.10 → CoreDNS (kubeadm)
- 8.8.8.8 → Google Public DNS (sitios externos)
- 208.67.222.222 → OpenDNS (fallback)
Policy routing: paquetes no-RFC1918 (TCP/UDP/ICMP) reciben fwmark 100 y usan tabla 100 (default vía gateway DHCP). Tráfico RFC1918 (incluyendo OOBI y NFS vía 10.x.x.x) sigue por eth0.
CNI DHCP daemon (k8s/dut/30-cni-dhcp-daemon.yaml): DaemonSet en todos los nodos que expone el socket /run/cni/dhcp.sock requerido por el IPAM tipo dhcp del NAD VLAN 40.
5. Almacenamiento — NFS in-cluster, solo OOBI¶
El volumen cloned-sites se comparte entre el Cloner (escritor) y los 10 slots Cloned Persona (lectores). Como los PVCs son namespace-scoped y los slots viven en 10 namespaces separados, el compartido se hace mediante 11 pares estáticos PV/PVC apuntando al mismo export del servidor NFS in-cluster.
| Componente | Manifiesto | Propósito |
|---|---|---|
| NFS Deployment + Service | k8s/dut/35-nfs-server.yaml |
Servidor NFSv4 réplica única, nodeAffinity prefiere role=infra. Respaldado por hostPath /var/lib/agent-cluster/cloned-sites. Estrategia Recreate garantiza writer único. InitContainer prepare-share chowna a UID 10004 antes que el daemon arranque. |
| 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 apuntan al mismo export. storageClassName: "" previene CSI race. |
Topología multi-node:
- Cloner corre en
role=infra(UCS-4) — mismo nodo que el NFS server. - Slots Cloned Persona corren en
role=ngfw-dut(UCS-1). - Tráfico NFS fluye por OOBI (eth0 → Service
nfs-server.web-agents.svc.cluster.local:2049). - Data plane (
net1macvlan VLAN 40 + 200-209) nunca lleva I/O de storage.
Topología single-node: todo en el mismo nodo. NFS round-trip es loopback, overhead despreciable.
| Atributo | Valor |
|---|---|
| Backend | NFSv4 in-cluster Service |
| Capacidad | 50 Gi (claim) — limitada por el disco del nodo del 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 red | OOBI solamente (eth0) |
| Co-localización multi-node | Cloner + NFS server en el mismo nodo (role=infra) |
6. Ciclo de vida de un job¶
- Operador crea job:
POST /api/clone/jobs {url, personaName: "shop"}. - Cloner hace long-poll en
/api/clone/agents/{id}/joby reclama el job. - browser engine headful (con plugin stealth) navega a la URL, intercepta cada response vía
route.fetch(), captura body+headers. - Reescritura HTML:
(href|src|action)="${origin}→="(paths relativos). - Persistencia:
/mnt/cloned/{personaName}/index.html+ assets (mismo dominio en carpeta original, cross-origin bajo_ext/). - Cloner reporta:
PATCH /api/clone/jobs/{id} {status: "completed", assetCount, totalBytes}. - Operador asigna slot:
PATCH /api/clone/persona-slots/N {siteName: "shop", replicas: 1}. - Dashboard: actualiza ConfigMap (SITE_NAME), escala Deployment, upsert en la tabla
targets(la URLhttps://cloned-N.persona.internal/se vuelve target automáticamente — ver PR #155). - Stakater Reloader detecta ConfigMap → reinicia pod.
- Caddy slot arranca sirviendo
/mnt/cloned/shop/en la IP del macvlan VLAN 200+N-1. - Agentes browser-engine/synthetic-load toman la nueva target en el siguiente poll, navegan a través del NGFW.
7. Troubleshooting esencial¶
| Síntoma | Verificar |
|---|---|
| Cloner en pod incorrecto (multi-node) | kubectl get pod -o wide — NODE debe ser UCS-4 |
net1 sin IP |
kubectl get ds cni-dhcp-daemon -n web-agents (Ready en todo nodo) |
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 pero sirviendo vacío | Cloner escribió en otro nodo? Reinicie NFS y luego cloner |
| Sin internet | kubectl exec deploy/cloner -- ping -c 3 -I net1 8.8.8.8 |
Detalles operativos: CLONER_OPERATIONS.es.md.
8. Archivos clave¶
| Archivo | Descripción |
|---|---|
cloner/src/cloner.ts |
Motor de cloning con 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 del cloner con initContainer routing |
k8s/dut/30-cni-dhcp-daemon.yaml |
DaemonSet CNI DHCP (requerido para IPAM de VLAN 40) |
k8s/dut/35-nfs-server.yaml |
NFS server in-cluster para 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 y agents del cloner |
© 2026 André Luiz Gallon — Distribuido bajo PolyForm Noncommercial 1.0.0 con Restricciones Adicionales de Uso (Apéndice A).