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]]
Referenciado desde
- ¿Cómo funciona la sonda TCP de monitoring introducida en v1.58.0? ¿Dónde está el modal de TCP Probe en Observatory, qué campos tiene, y cómo viaja la orden hasta el Local Agent?
- Hotfix 2.21.4: la "autocuración" del motor SNMP era un diagnóstico equivocado
- Incidente 24-08-2026 · el cortacircuitos mixto silenció el SNMP vivo del CCIB 15 minutos
- Incidente 24-08-2026 · el motor SNMP quedaba corrupto tras cuelgues seguidos — timeout duro + autocuración (Agente 2.21.2→2.21.3)
- POST /api/monitoring/targets/{target_id}/tcp/check — Sonda TCP Bajo Demanda
- Remate de la sonda TCP: supresión inteligente de falsos positivos (v1.63.4)
- sentinel/tcp_check.py — Loop Asyncio de Sondeo TCP del Agente
- tcp_service: Sonda TCP con Guardia SSRF