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_loopdel 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 sondatcp_port: PositiveIntegerField(null=True, blank=True)— puerto a probar (1-65535)last_tcp_up: BooleanField(null=True, blank=True)— resultado del último chequeolast_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”.
- Si
2. Servicio TCP: tcp_service.py
check_tcp(host: str, port: int) -> TcpResult: intenta conexión SYN, mide latencia del handshake, devuelvestatus(“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:
- Valida permiso (
observatory:edit). - Si
target.tcp_portno está configurado → 400 Bad Request. - Si IP es privada → retorna el último resultado conocido (evita probar la LAN desde el SaaS).
- Si IP es pública → ejecuta
check_tcp(), persiste resultado enlast_tcp_up/last_tcp_check, recalcularesolve_reachability(). - Emite métrica
tcp_latencysi UP. - 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=Truey 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) ytcp_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_typesdel 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()exponetargets.tcpycycles.tcpen el/infolocal.
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:- Persiste
tcp_enabledytcp_porten un PUT al target. - Si
enabled=true, ejecuta POST/tcp/checkpara trigger inmediato. - Refresca la pestaña del dispositivo.
- Persiste
- 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_tcpdel 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_loopguarda el último resultado por target (sched._tcp_last) ycheck_pingrecibetcp_fresh_up(frescura 300s, el mismo suelo queresolve_reachabilitydel 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%) peroresolve_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.servicesen lugar determinal.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_upno 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):
- Modal → Agente: al guardar el modal (checkbox + puerto 22), la config llegó al Agente por WS al instante (
notify_agents) ylast_tcp_up=Trueapareció en PROD en <1 minuto. - 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. - Métricas:
tcp_reachable/tcp_latency_msfluyen a VictoriaMetrics con tenant y target correctos. (Nota: el Agente NO escribeMetricSampleen Postgres — por diseño, su telemetría vive en VictoriaMetrics;MetricSamplesolo lo escribe el check manual server-side de IPs públicas.) - Recuperación: al retirar el bloqueo,
loss=0y 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):
- ✅ 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.
- ✅
tcp_cyclesen/infodel Agente — RESUELTO (Agent 2.16.3):get_status()exponetargets.tcpycycles.tcp. Verificado en vivo tras el auto-update de la flota (YogaEdu:targets.tcp=2,cycles.tcpcontando). - ✅ 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 silenciosamentetcp_enabled/tcp_port. Ahora se preservan.- Store SQLite del Agente: columna
tcp_portañadida (ALTER idempotente). No rompe compatibilidad hacia atrás. receive_insightdel 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_pingdel Agente (2.16.3): firma ampliada contcp_fresh_up(default False — compatible con callsites viejos).
Cronograma de activación
- ✅ v1.58.0 (17-07-2026): Feature completa en el SaaS (check manual, Modal, endpoint, ingest).
- ✅ Agent 2.16.0 (22-07-2026, tanda #196/#197): .exe del Agente con lógica TCP despierta.
- ✅ 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. - ✅ 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).
- ✅ 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]]