CreaRack-SL

Sonda TCP — Equipos que bloquean ICMP dejan de salir "down" falsos

Resumen

Desde v1.58.0, cada target de monitoreo puede llevar una sonda TCP opcional que prueba si un puerto específico acepta conexiones (SSH:22, HTTPS:443, etc.). Resuelve el problema de que servidores con ICMP bloqueado pero servicios TCP vivos aparecían como “caídos” en Observatory.

Impacto: Cierra la deuda DCIM — la disponibilidad de equipos endurecidos y firewalls ya no miente.

Estado: ✅ ACTIVA Y VALIDADA E2E en PROD (22-07-2026) — el loop del Agente viaja despierto desde Agent 2.16.0 (tanda #196/#197). Ver § Validación E2E. Los 3 flecos del click-test quedaron CERRADOS el 23-07-2026 (v1.63.4 server-side + Agent 2.16.3); task #205 done. Ver § Flecos.

Contexto de la deuda (DCIM, NEXT s90+)

Antes de esta feature:

  • Un servidor con ping_loss = 100% pero puerto SSH/HTTPS respondiendo se mostraba como DOWN.
  • Era falso en equipos que bloqueaban ICMP por seguridad (linux sin icmp_echo_ignore_broadcasts, firewalls perimetrales, appliances dedicados).
  • Los operadores no tenían forma de saber si el equipo estaba muerto o solo endurecido.

Ahora:

  • La semántica de up/down es combinada: si el TCP es fresco y vivo, el target cuenta como UP aunque pierda todos los pings.
  • Definimos “fresco” como: resultado TCP dentro de max(TCP_FRESHNESS_FLOOR_S=300s, interval_seconds × 3).
  • Aplica tanto en el check manual (endpoint SaaS contra destinos públicos) como en el ingest del Agente (LAN, sondeada por el tcp_loop del Agente desde 2.16.x).

Componentes de la feature

1. Modelo extendido: MonitoringTarget

Nuevos campos (migración 0028):

  • tcp_enabled: BooleanField(default=False) — toggle de la sonda
  • tcp_port: PositiveIntegerField(null=True, blank=True) — puerto a probar (1-65535)
  • last_tcp_up: BooleanField(null=True, blank=True) — resultado del último chequeo
  • last_tcp_check: DateTimeField(null=True, blank=True) — timestamp de ese resultado

Nuevo método:

  • resolve_reachability(ping_loss: float) -> str — devuelve “up” o “down” combinando ICMP y TCP.
    • Si ping_loss < 100% → siempre “up”.
    • Si ping_loss == 100% pero TCP fresco y vivo → “up”.
    • Si no hay TCP activo → “down”.

2. Servicio TCP: tcp_service.py

  • check_tcp(host: str, port: int) -> TcpResult: intenta conexión SYN, mide latencia del handshake, devuelve status (“up”/“down”), latency_ms, error opcional.
  • Guardia SSRF: solo para destinos públicos. Direcciones privadas retornan error “Blocked destination”.
  • Timeout: 3.0s por defecto (configurable).

Resultado:

@dataclass
class TcpResult:
    status: str  # "up" | "down"
    latency_ms: float | None
    error: str | None = None

3. Endpoint: POST /api/monitoring/targets/{target_id}/tcp/check

Operación: tcp_check(request, target_id: int)

Lógica:

  1. Valida permiso (observatory:edit).
  2. Si target.tcp_port no está configurado → 400 Bad Request.
  3. Si IP es privada → retorna el último resultado conocido (evita probar la LAN desde el SaaS).
  4. Si IP es pública → ejecuta check_tcp(), persiste resultado en last_tcp_up/last_tcp_check, recalcula resolve_reachability().
  5. Emite métrica tcp_latency si UP.
  6. Notifica via WebSocket (broadcast_metrics_sync).

Respuesta:

{
  "status": "up" | "down",
  "port": 22,
  "latency_ms": 12.45,
  "error": null,
  "timestamp": "2026-07-17T10:51:06Z"
}

4. Loop Sentinel (Agente): sentinel/tcp_check.py

Estado: ✅ DESPIERTO desde Agent 2.16.0 (22-07-2026, tanda #196/#197). El código viajó dormido en 1.58.0 y se activó con el release del .exe.

Qué hace:

  • Itera cada sched._tick (por defecto 1s).
  • Selecciona targets con tcp_enabled=True y cadencia vencida (ventana PING_FLOOR-PING_CAP como el ping).
  • Ejecuta sondeos en paralelo (semáforo 20, timeout 3s).
  • Escribe métricas tcp_up (0.0/1.0) y tcp_latency (ms) en el store SQLite.
  • Guarda el último resultado por target en memoria (sched._tcp_last, desde 2.16.3) — lo consulta el anomaly detector (ver §6).
  • Se activa cuando el usuario habilita TCP en el front (campo en monitor_types del target).
  • Detalle de diseño: un fallo TCP NO alimenta el circuit breaker (el ping sigue siendo la señal de host inalcanzable), pero un éxito TCP SÍ lo resetea — así un equipo solo-TCP con ICMP bloqueado no acaba silenciado por el breaker.
  • Telemetría (desde 2.16.3): get_status() expone targets.tcp y cycles.tcp en el /info local.

5. UI: Observatory Modal

Ubicación: sección Heartbeat, junto a SNMP/HTTP/Ping.

Elementos:

  • Botón TCP Probe (verde si activo, gris si inactivo).
  • Modal con:
    • Checkbox “Enable TCP probe”.
    • Input número “Port” (1-65535, validación cliente + servidor).
    • Help text: “El equipo cuenta como ACTIVO si este puerto acepta conexiones, aunque bloquee el ping (ICMP)”.

Comportamiento:

  • Al tocar “Guardar”, ejecuta toggleMonitorType() que:
    1. Persiste tcp_enabled y tcp_port en un PUT al target.
    2. Si enabled=true, ejecuta POST /tcp/check para trigger inmediato.
    3. Refresca la pestaña del dispositivo.
  • Default real del puerto (v1.63.4): activar la sonda con el campo Port vacío guarda 22 de verdad — el placeholder “22” ya no es solo decorativo. Un valor escrito fuera de rango sigue rechazándose con aviso (validación cliente + validate_tcp del schema en servidor, ambas presentes desde v1.58.0).

6. Sin falsas alarmas “total loss” para equipos solo-TCP (v1.63.4 + Agent 2.16.3)

Un equipo legítimamente solo-TCP (ICMP siempre bloqueado) hace que el check_ping del anomaly detector del Agente vea loss=100% en cada ciclo → dispararía la anomalía “packet_loss” cada 5 minutos de cooldown hasta comerse el rate limit de 10 insights/hora, sin emitir nunca recovery. Confirmado leyendo anomaly_detector.py; en el test E2E del 22-07 no se manifestó porque el rate limit estaba al tope.

Doble defensa, ambas desplegadas:

  • Fix de raíz (Agent 2.16.3, 23-07): el tcp_loop guarda el último resultado por target (sched._tcp_last) y check_ping recibe tcp_fresh_up (frescura 300s, el mismo suelo que resolve_reachability del SaaS). Con la sonda TCP fresca dando el equipo vivo, el loss=100% es su régimen normal: la anomalía ni se emite (ahorra el viaje y el cupo local). Un target en anomalía activa que vuelve por TCP emite recovery — misma semántica combinada de up/down del SaaS. Tests unitarios: tests/agent/test_anomaly_tcp_suppression.py.
  • Red server-side (v1.63.4): receive_insight (monitoring/api/insights.py) suprime el insight cuando el loss reportado es total (≥100%) pero resolve_reachability() da el target vivo por sonda TCP fresca. Responde 429 a propósito: la flota 2.16.x lo loguea como rate-limit benigno sin contarlo como error del reporter. Cubre flotas que aún no lleven el fix de raíz.

En ambas capas, una degradación parcial (loss<100%) NO se suprime aunque el TCP esté vivo (puede ser congestión real).

7. i18n

Strings nuevos traducibles en django.po y djangojs.po:

  • “TCP Probe”, “Sonda TCP”
  • “Enable TCP probe”, “Activar la sonda TCP”
  • “The device counts as UP if this port accepts connections…” (help)
  • “TCP port to probe (e.g. 22 for SSH…)” (placeholder)
  • “Please enter a valid port (1-65535)” (validación)

Refactor lateral: build_vendor_oids_lookup

Se movió de terminal/consumers.py (que superaba 500 LOC) a terminal/services.py, donde ya la invocaba terminal/api/sentinel.py (cruzando módulos). Ahora:

  • Importada desde terminal.services en lugar de terminal.consumers.AgentConsumer._build_vendor_oids_lookup().
  • Firma: build_vendor_oids_lookup(tenant_id: int) -> dict.

Testeo

Suite: tests/api/test_tcp_probe.py (18 tests desde v1.63.4) + tests/agent/test_anomaly_tcp_suppression.py (4 tests del detector, desde Agent 2.16.3).

  • Campos y validación de schema (tcp_enabled, tcp_port).
  • Semántica de resolve_reachability(): TCP fresco anula ICMP pérdida del 100%.
  • Ingest del Agente: lote TCP-only levanta un target caído; lote ping+TCP combinados.
  • Supresión de insights (v1.63.4): total loss + TCP fresco → 429 sin llamada de IA; TCP rancio o loss parcial → el insight se crea.
  • Supresión en origen (2.16.3): check_ping con tcp_fresh_up no emite packet_loss por loss total; loss parcial sí; recovery vía TCP.
  • Servicio TCP con listener real (socket abierto, test válido).
  • Guardias SSRF.

Suites vecinas:

  • monitoring: 80 tests en verde.
  • sentinel: 80 tests en verde.

Validación E2E en PROD (22-07-2026)

Click-test real con Agent 2.16.2 (YogaEdu) sobre el target AU-Camerino-02 (192.168.230.4, LAN CCIB):

  1. Modal → Agente: al guardar el modal (checkbox + puerto 22), la config llegó al Agente por WS al instante (notify_agents) y last_tcp_up=True apareció en PROD en <1 minuto.
  2. Rescate verificado en vivo: se bloqueó el ICMP saliente hacia el target en el host del Agente (regla de firewall temporal) → PROD registró loss=100% y el target siguió “up” gracias a la sonda TCP fresca. La gráfica de Heartbeat mostró el pico de loss al 100% con el header en UP.
  3. Métricas: tcp_reachable/tcp_latency_ms fluyen a VictoriaMetrics con tenant y target correctos. (Nota: el Agente NO escribe MetricSample en Postgres — por diseño, su telemetría vive en VictoriaMetrics; MetricSample solo lo escribe el check manual server-side de IPs públicas.)
  4. Recuperación: al retirar el bloqueo, loss=0 y todo volvió a régimen sin intervención.

Flecos del click-test (task del Gestor #205) — CERRADOS

Los 3 resueltos el 23-07-2026 (task #205 done):

  1. ✅ Placeholder del puerto — RESUELTO (v1.63.4): campo vacío + sonda activada = 22 real (ver §5). Matiz honesto: el “no-op silencioso” que describía la task no se reproducía en el código de v1.58.0+ (las validaciones cliente/servidor ya existían); lo real era el default inexistente que el placeholder sugería.
  2. ✅ tcp_cycles en /info del Agente — RESUELTO (Agent 2.16.3): get_status() expone targets.tcp y cycles.tcp. Verificado en vivo tras el auto-update de la flota (YogaEdu: targets.tcp=2, cycles.tcp contando).
  3. ✅ Falsas alarmas “total loss” para equipos solo-TCP — CONFIRMADAS Y RESUELTAS en doble capa: supresión server-side (v1.63.4) + fix de raíz en el detector del Agente (2.16.3). Ver §6.

Cambios en comportamiento existente

  • toggleMonitorType() ahora arrastra campos TCP: antes, un toggle de SNMP habría borrado silenciosamente tcp_enabled/tcp_port. Ahora se preservan.
  • Store SQLite del Agente: columna tcp_port añadida (ALTER idempotente). No rompe compatibilidad hacia atrás.
  • receive_insight del CNS (v1.63.4): puede responder 429 “suppressed” a un insight de loss total si la sonda TCP fresca da el target vivo (ver §6).
  • check_ping del Agente (2.16.3): firma ampliada con tcp_fresh_up (default False — compatible con callsites viejos).

Cronograma de activación

  1. ✅ v1.58.0 (17-07-2026): Feature completa en el SaaS (check manual, Modal, endpoint, ingest).
  2. ✅ Agent 2.16.0 (22-07-2026, tanda #196/#197): .exe del Agente con lógica TCP despierta.
  3. ✅ Validación E2E en PROD (22-07-2026): la LAN la sondea sentinel/tcp_check.py; el endpoint SaaS queda para destinos públicos y trigger manual.
  4. ✅ v1.63.4 (23-07-2026): flecos 1 y 3 del click-test resueltos server-side (default real 22 + supresión de falsos “total loss” en el CNS).
  5. ✅ Agent 2.16.3 (23-07-2026): fleco 2 (telemetría TCP en /info) + fix de raíz del detector (supresión en origen + recovery vía TCP). Flota auto-actualizada sin manos; task #205 CERRADA.

Véase también

  • [[entity—monitoring—model—monitoring-target]]
  • [[entity—monitoring—service—tcp-service]]
  • [[entity—monitoring—endpoint—tcp-check]]
  • [[entity—terminal—service—sentinel-tcp-check]]
  • [[feature—monitoring—tcp-probe-suppression-v1634]]
  • [[concept—monitoring—cns]]
  • [[concept—saas—multi-tenancy]]