estado-servicio.sh ya sabe decir si la máquina está sana y si el proceso veloz-api existe. Pero "el proceso existe" y "el servicio responde" no son lo mismo: un proceso puede estar vivo con el puerto 8080 cerrado, escuchando solo en 127.0.0.1 mientras el resto de la red no lo alcanza, o bloqueado sin aceptar conexiones. Para distinguir esos casos hay que salir del sistema local y mirar la red. Esta lección cubre el diagnóstico y la automatización de red desde un script: comprobar conectividad, resolver nombres, ver qué puertos escuchan, probar si uno responde —incluido el /dev/tcp que quedó apuntado en 05-05— y esperar con reintentos a que un servicio levante.

Contenido

  1. Las cuatro capas que puede fallar
  2. Conectividad: ping y su código de salida
  3. Resolución de nombres
  4. Puertos y sockets: ss
  5. Comprobar si un puerto responde: nc y /dev/tcp
  6. Interfaces, rutas y la IP propia
  7. curl frente a wget
  8. Comprobar disponibilidad y medir con curl
  9. Espera activa con reintentos y retroceso
  10. Transferencias y sesión remota
  11. Cortafuegos, en pinceladas
  12. Seguridad: credenciales y certificados
  13. Aplicación: veloz_puerto_abierto en el toolkit

  1. Las cuatro capas que puede fallar

Cuando "la API no va", el diagnóstico útil es saber dónde se rompe, y para eso conviene comprobar en orden:

Capa Pregunta Herramienta
Red ¿Llego a la máquina? ping -c 2 host
Nombres ¿El nombre se resuelve a la IP correcta? getent hosts, dig +short
Puerto ¿Hay algo escuchando y acepta conexiones? ss -tulpn, nc -z, /dev/tcp
Aplicación ¿Contesta lo que debe? curl -sfI, y el JSON de 06-05

Saltarse una capa es la causa de los diagnósticos equivocados: culpar a la red cuando lo que falla es el DNS, o dar por caída una API que en realidad devuelve un 500 perfectamente entregado. Un script de monitorización serio distingue los cuatro casos y lo dice en el mensaje de error.

  1. Conectividad: ping y su código de salida

ping interactivo se lanza sin más y se corta con Ctrl-C; en un script eso es inaceptable, así que siempre lleva -c (número de paquetes) y conviene -W (espera máxima por respuesta, en segundos):

ping -c 2 -W 2 -q srv-veloz-01 >/dev/null 2>&1 || veloz_log_error "srv-veloz-01 no responde"

La clave está en que lo que importa no es la salida de ping, sino su código de retorno: 0 si recibió alguna respuesta, distinto de 0 si no. Por eso -q (modo silencioso) y la redirección a /dev/null no pierden nada: toda la información que necesita el script está en $?. Es exactamente el mismo principio de 03-01 aplicado a la red.

Dos avisos. Primero, ping usa ICMP, y muchos cortafuegos lo bloquean aunque el servicio funcione perfectamente: un ping fallido no demuestra que la máquina esté caída, solo que no contesta a ICMP. Segundo, para ver por dónde se pierde el camino existen traceroute (o tracepath) y mtr, que combina traza y estadística continua; son herramientas de diagnóstico manual, no de script.

  1. Resolución de nombres

Antes de culpar a la red hay que comprobar que el nombre se traduce a una IP. La opción correcta en un script es getent hosts, la misma orden de 06-03: consulta las fuentes que el sistema tiene configuradas en /etc/nsswitch.conf —incluido /etc/hosts—, que es exactamente lo que hará la aplicación al conectarse.

getent hosts api.veloz.example devuelve 10.20.0.15 api.veloz.example, mientras que dig +short api.veloz.example da la misma IP pero preguntando solo al DNS, ignorando /etc/hosts. La diferencia importa: si alguien puso una entrada en /etc/hosts, dig dirá una cosa y la aplicación irá a otra. dig +short es en cambio insuperable para preguntar a un servidor concreto (dig @8.8.8.8 +short api.veloz.example) y así distinguir un problema del DNS interno de uno general. host es una versión resumida de dig, y nslookup es el veterano que sigue apareciendo en manuales antiguos: funciona, pero su salida es la más incómoda de parsear y su código de salida, poco fiable. Orden de preferencia: getent hosts en scripts, dig +short para diagnosticar.

  1. Puertos y sockets: ss

ss muestra los sockets del sistema y ha sustituido a netstat, que en muchas distribuciones ya ni se instala. La combinación que hay que memorizar es ss -tulpn:

Letra Significado
-t Sockets TCP
-u Sockets UDP
-l Solo los que están escuchando
-p Muestra el proceso dueño (requiere privilegios para ver los ajenos)
-n Números en vez de nombres: 8080 en lugar de http-alt
ss -tulpn | grep ':8080'
# LISTEN 0 511 127.0.0.1:8080 0.0.0.0:* users:(("veloz-api",pid=2114,fd=6))

Leer esa línea entera es el diagnóstico que faltaba en 06-03. LISTEN confirma que acepta conexiones; 127.0.0.1:8080 es la parte reveladora: la API escucha solo en la interfaz local, así que ningún otro equipo podrá conectarse aunque el proceso esté perfecto. Si pusiera 0.0.0.0:8080, escucharía en todas las interfaces. Y users:(("veloz-api",pid=2114)) dice qué proceso lo tiene, que es la respuesta a "algo ocupa mi puerto". Para lo mismo desde el ángulo de los ficheros abiertos está lsof -i :8080 (06-03), útil cuando ss no está disponible.

  1. Comprobar si un puerto responde: nc y /dev/tcp

ss mira desde dentro de la máquina. Para comprobar desde fuera —o simplemente para verificar que una conexión se establece de verdad— hay dos formas.

La primera es nc (netcat) con -z, que intenta conectar sin enviar datos, y -w con un tiempo máximo: nc -z -w2 localhost 8080 && echo abierto. La segunda no necesita instalar nada, porque es una función de Bash: los pseudo-ficheros /dev/tcp/host/puerto y /dev/udp/host/puerto. Abrirlos con la redirección de descriptores de 05-05 intenta una conexión TCP:

if timeout 2 bash -c 'exec 3<>/dev/tcp/localhost/8080' 2>/dev/null; then
    echo "puerto abierto"
fi

Tres detalles hacen que esto funcione. exec 3<> abre el descriptor 3 para lectura y escritura sobre la conexión; si el puerto está cerrado, la redirección falla y el bash -c devuelve un código distinto de 0. El timeout 2 de 05-02 es obligatorio: sin él, una máquina que descarta paquetes en silencio dejaría el script colgado durante el tiempo de espera del sistema, que puede ser de más de un minuto. Y el 2>/dev/null silencia el mensaje Connection refused que Bash imprime.

El aviso de portabilidad es serio: /dev/tcp no es un fichero real ni una característica del sistema, es una prestación de Bash. No existe en dash, que es el /bin/sh de Ubuntu, así que un script con #!/bin/sh que lo use fallará con No such file or directory. Además, algunas distribuciones compilan Bash sin esta prestación. La comparación queda así:

nc -z -w2 /dev/tcp
Requiere instalar algo Sí (netcat-openbsd) No, viene con Bash
Funciona en sh/dash No (bashismo, 08-07)
Control del tiempo de espera -w propio Requiere timeout
Variantes incompatibles Sí: -z no está en todas No

  1. Interfaces, rutas y la IP propia

La familia ip sustituyó a ifconfig y route. ip a (abreviatura de ip address show) lista las interfaces con sus direcciones, ip -4 a solo las IPv4, e ip r muestra la tabla de rutas, cuya primera línea default via ... es la puerta de enlace.

Obtener la IP propia en un script tiene una trampa: una máquina puede tener varias interfaces, y hostname -I devuelve todas separadas por espacios, sin decir cuál se usará para salir. La forma fiable es preguntárselo a la tabla de rutas:

ip -4 route get 1.1.1.1 | awk '{ for (i=1;i<=NF;i++) if ($i=="src") { print $(i+1); exit } }'

ip route get no envía ningún paquete: consulta qué ruta e IP de origen usaría el núcleo para llegar a ese destino, que es justo la respuesta correcta. El awk de 06-01 busca la palabra src y toma el campo siguiente, en lugar de fiarse de una posición fija que cambia según la configuración —el mismo principio de robustez que $NF—.

  1. curl frente a wget

Las dos descargan por HTTP, pero están pensadas para cosas distintas:

curl wget
Salida por defecto La salida estándar (para tuberías) Un fichero en el disco
Fuerte en APIs: cabeceras, métodos, cuerpos Descargas: reintentos, recursividad, reanudar
Sigue redirecciones Solo con -L Por defecto
Descarga recursiva de un sitio No Sí (-r)
Presencia Casi universal Habitual en Linux, ausente en macOS
Falla con error HTTP Solo con -f Sí por defecto

Para un script de operaciones, la regla es sencilla: curl para hablar con servicios, wget para bajarse un fichero grande de forma desatendida. Descargar en ambos:

curl -sSfL -o /tmp/veloz-cli.tgz https://descargas.veloz.example/cli.tgz
wget -q --tries=3 --timeout=20 -O /tmp/veloz-cli.tgz https://descargas.veloz.example/cli.tgz

Y una advertencia de higiene: curl ... | bash —el "instalador de una línea" que promocionan muchos proyectos— ejecuta como tuyo un código que no has visto y que puede cambiar entre dos ejecuciones. Descarga primero, revisa, ejecuta después.

  1. Comprobar disponibilidad y medir con curl

Para saber si un servicio HTTP está vivo sin descargar el cuerpo, -I pide solo las cabeceras y -f hace que curl falle con código 22 ante un error HTTP en lugar de devolver 0 tan contento:

curl -sfI --max-time 5 http://localhost:8080/salud >/dev/null && veloz_log_info "api viva"

Ese -sfI con --max-time es la comprobación de disponibilidad estándar: silencioso, fallando ante errores y sin poder colgarse. El desglose fino de -s, -S y -f y de los códigos HTTP corresponde a 06-05; aquí lo que interesa es el código de salida.

Para medir, -w imprime variables al terminar, con -o /dev/null para tirar el cuerpo:

curl -s -o /dev/null -w 'codigo=%{http_code} total=%{time_total}s conexion=%{time_connect}s\n' \
    http://localhost:8080/salud          # -> codigo=200 total=0.043s conexion=0.001s

%{time_total} es la latencia completa y %{time_connect} solo el establecimiento de la conexión: si el total es alto pero la conexión es rápida, el problema está en la aplicación, no en la red. Registrar ese número en cada ejecución da una serie temporal con la que detectar degradaciones antes de que se conviertan en caídas —la base del proyecto 09-04—.

  1. Espera activa con reintentos y retroceso

Tras reiniciar veloz-api, el puerto tarda unos segundos en aceptar conexiones. Comprobar inmediatamente da un falso negativo; dormir un tiempo fijo es adivinar. La solución es la espera activa con retroceso exponencial de 05-03, aplicada a la red:

# veloz_espera_servicio — Espera a que un puerto acepte conexiones. Uso: ... <host> <puerto> [intentos]
veloz_espera_servicio() {
    local host="${1:?}" puerto="${2:?}" intentos="${3:-6}" espera=1 i
    for (( i = 1; i <= intentos; i++ )); do
        if veloz_puerto_abierto "$host" "$puerto"; then
            veloz_log_info "$host:$puerto disponible tras $i intento(s)"
            return 0
        fi
        veloz_log_info "intento $i/$intentos fallido; reintento en ${espera}s"
        sleep "$espera"
        espera=$(( espera * 2 ))
    done
    veloz_log_error "$host:$puerto no respondio tras $intentos intentos"
    return 1
}

El retroceso (1, 2, 4, 8…) es preferible a un intervalo fijo por dos motivos: reacciona rápido si el servicio levanta enseguida y no machaca a una máquina que ya está en apuros. Los seis intentos por defecto cubren unos 63 segundos, suficiente para un reinicio normal. Y el return 1 final es lo que permite encadenar veloz_espera_servicio localhost 8080 || veloz_morir 1 "la API no levanto".

  1. Transferencias y sesión remota

Copiar ficheros entre máquinas se hace con scp origen usuario@destino:/ruta para copias puntuales o con rsync -az --delete origen/ usuario@destino:/ruta/ cuando hay que sincronizar directorios, ya que solo transfiere las diferencias. Ejecutar órdenes en otra máquina es ssh usuario@destino 'orden'. Los tres son la base de la automatización remota y tienen su propia lección: claves sin contraseña, ssh-agent, known_hosts, opciones no interactivas y los peligros de ejecutar a ciegas en varias máquinas se ven en 07-06. Lo único que conviene retener ahora: en un script, ssh necesita -o BatchMode=yes para que falle en vez de quedarse esperando una contraseña que nadie va a teclear.

  1. Cortafuegos, en pinceladas

Cuando el puerto está en LISTEN en 0.0.0.0 pero desde otra máquina no se llega, el sospechoso habitual es el cortafuegos. En Ubuntu, ufw status verbose (requiere privilegios) lista las reglas activas; por debajo están iptables -L -n o nft list ruleset. En la nube hay además un segundo cortafuegos fuera de la máquina —grupos de seguridad— que ningún comando local puede ver, y que explica muchos diagnósticos imposibles. Regla mental: si ss dice que escucha en 0.0.0.0, nc funciona desde la propia máquina y no desde fuera, el problema está en el camino, no en el servicio.

  1. Seguridad: credenciales y certificados

Tres reglas que no se negocian, y que se desarrollan en 08-03:

  • Nunca incrustes credenciales en la URL. curl https://usuario:[email protected]/ deja la contraseña en el historial (02-06), en ps mientras dura la orden y en los registros del servidor. Usa --netrc, o una variable de entorno leída de un fichero con permisos 600 como veloz-ops.conf.
  • No desactives la verificación de certificados. curl -k (o --insecure) acepta cualquier certificado, incluido el de quien se interponga en la conexión: convierte HTTPS en HTTP con un candado falso. Si el certificado es interno, lo correcto es instalarlo o pasarlo con --cacert /ruta/ca.pem.
  • Usa siempre HTTPS, incluso en la red interna. Una red "de confianza" deja de serlo el día que alguien conecta un portátil comprometido.

  1. Aplicación: veloz_puerto_abierto en el toolkit

La función que faltaba en lib/comun.sh, con las dos implementaciones y elección automática:

# veloz_puerto_abierto — ¿Acepta conexiones host:puerto? Uso: veloz_puerto_abierto <host> <puerto> [seg]
veloz_puerto_abierto() {
    local host="${1:?falta el host}" puerto="${2:?falta el puerto}" seg="${3:-2}"
    [[ "$puerto" =~ ^[0-9]+$ ]] && (( puerto > 0 && puerto < 65536 )) || {
        veloz_log_error "puerto invalido: $puerto"; return 64; }
    if command -v nc >/dev/null 2>&1; then
        nc -z -w"$seg" "$host" "$puerto" >/dev/null 2>&1
    else
        timeout "$seg" bash -c "exec 3<>/dev/tcp/$host/$puerto" 2>/dev/null
    fi
}

La validación del puerto con =~ (05-04) no es paranoia: el número acaba dentro de una cadena que bash -c va a ejecutar, así que un valor sin validar sería una inyección de código en toda regla. command -v (06-03) elige la implementación disponible, y ambas ramas devuelven directamente su código de salida, que se convierte en el de la función.

Con ella, estado-servicio.sh distingue por fin los casos que antes confundía:

comprobar_api() {
    local host=localhost puerto=8080
    if ! pgrep -f veloz-api >/dev/null; then
        veloz_log_error "veloz-api: proceso no encontrado"; return 1
    elif ! veloz_puerto_abierto "$host" "$puerto"; then
        veloz_log_error "veloz-api: proceso vivo pero $puerto no acepta conexiones"; return 1
    elif ! curl -sfI --max-time 5 "http://$host:$puerto/salud" >/dev/null; then
        veloz_log_error "veloz-api: puerto abierto pero /salud no responde correctamente"; return 1
    fi
    veloz_log_info "veloz-api: correcto"
}

Esos tres mensajes son la lección entera resumida: proceso, puerto y aplicación son tres cosas distintas, y decir cuál falló ahorra media hora de diagnóstico a quien lea la alerta a las tres de la mañana. Esta función es el germen del proyecto 09-04.

Errores Comunes y Consejos

  • ping sin -c. En un script queda colgado para siempre. Y un ping fallido no prueba que la máquina esté caída: muchos cortafuegos bloquean ICMP.
  • Confiar solo en pgrep. Que el proceso exista no significa que el puerto acepte conexiones ni que la aplicación conteste.
  • Usar /dev/tcp sin timeout. Ante una máquina que descarta paquetes, el script se cuelga más de un minuto.
  • /dev/tcp con #!/bin/sh. Es un bashismo: falla en dash (08-07). Si necesitas portabilidad, usa nc.
  • curl sin -f. Devuelve 0 aunque el servidor conteste 500: tu comprobación siempre dirá que todo va bien.
  • dig para saber a qué IP irá la aplicación. dig ignora /etc/hosts; usa getent hosts.
  • Interpretar mal 127.0.0.1:8080 en ss. Significa que solo se atiende a la propia máquina, por muy en LISTEN que esté.
  • Consejo: pon --max-time (o timeout) a toda orden de red. Un script de monitorización que se cuelga es peor que uno que falla, porque nadie se entera.
  • Consejo: diagnostica siempre en el orden red → nombre → puerto → aplicación, y haz que el mensaje de error diga en qué capa se rompió.

Ejercicios

Ejercicio 1. Escribe una función veloz_dns_ok que compruebe que un nombre resuelve y que la IP obtenida coincide con la esperada, registrando mensajes distintos para "no resuelve" y "resuelve a una IP inesperada".

Ejercicio 2. Escribe un fragmento que, tras reiniciar veloz-api, espere hasta 30 segundos a que /salud conteste correctamente, midiendo cuánto tardó, y salga con error si no lo consigue.

Ejercicio 3. Escribe una comprobación que detecte el caso concreto de "la API escucha solo en 127.0.0.1" y lo avise como problema de configuración, no como caída.

Soluciones

Solución 1.

# veloz_dns_ok — Uso: veloz_dns_ok <nombre> <ip_esperada>
veloz_dns_ok() {
    local nombre="${1:?}" esperada="${2:?}" obtenida
    obtenida=$(getent hosts "$nombre" | awk 'NR==1 { print $1 }') || true
    if [[ -z "$obtenida" ]]; then
        veloz_log_error "DNS: $nombre no resuelve"; return 1
    elif [[ "$obtenida" != "$esperada" ]]; then
        veloz_log_error "DNS: $nombre resuelve a $obtenida, se esperaba $esperada"; return 2
    fi
    veloz_log_info "DNS: $nombre -> $obtenida"
}

getent hosts puede devolver varias líneas si hay varias direcciones, de ahí el NR==1. El || true evita que set -e (05-03) mate el script cuando el nombre no resuelve: aquí el fallo es un resultado esperado que queremos tratar, no un error. Los códigos 1 y 2 distintos permiten al llamante reaccionar de forma diferente a "no hay DNS" y "el DNS apunta mal", que son incidencias muy distintas.

Solución 2.

inicio=$(date +%s)
if veloz_espera_servicio localhost 8080 5 &&
   curl -sfI --max-time 5 http://localhost:8080/salud >/dev/null; then
    veloz_log_info "veloz-api lista en $(( $(date +%s) - inicio ))s"
else
    veloz_morir 1 "veloz-api no levanto en 30s"
fi

Cinco intentos con retroceso 1+2+4+8+16 cubren 31 segundos. Se encadenan las dos comprobaciones porque son distintas: primero que el puerto acepte conexiones, después que la aplicación conteste bien —un servicio puede abrir el puerto antes de terminar de arrancar—. La medida con date +%s es la aritmética de época de 04-06.

Solución 3.

escuchas=$(ss -tuln | awk '$1=="tcp" && $5 ~ /:8080$/ { print $5 }')
if [[ -z "$escuchas" ]]; then
    veloz_log_error "nadie escucha en 8080"
elif [[ "$escuchas" == 127.0.0.1:* || "$escuchas" == "[::1]:"* ]]; then
    veloz_log_error "CONFIGURACION: veloz-api solo escucha en local ($escuchas)"
else
    veloz_log_info "veloz-api escucha en $escuchas"
fi

ss -tuln sin -p no necesita privilegios, que es lo deseable en un script de comprobación. El awk filtra la columna de dirección local que acabe en :8080, y las comparaciones con globs de 04-05 detectan tanto 127.0.0.1 como la variante IPv6 [::1]. Distinguir este caso importa mucho: es un fallo de configuración que se arregla en un fichero, no una caída que se arregla reiniciando.

Conclusión

Diagnosticar red desde un script es recorrer cuatro capas en orden —red, nombres, puerto, aplicación— y decir en cuál se rompió. ping -c -W responde por la primera, y lo que se usa de él es su código de salida, no su salida; recuerda que ICMP se bloquea a menudo. Los nombres se resuelven con getent hosts en scripts (ve lo mismo que la aplicación, incluido /etc/hosts) y con dig +short para diagnosticar contra un servidor concreto. ss -tulpn enseña quién escucha y en qué dirección, y esa dirección distingue un 0.0.0.0 accesible de un 127.0.0.1 que condena al servicio a hablar solo consigo mismo. Para probar un puerto desde fuera están nc -z -w2 y el bashismo /dev/tcp/host/puerto, que no requiere instalar nada pero exige timeout y no existe en sh. ip route get da la IP propia de forma fiable, curl -sfI --max-time comprueba disponibilidad HTTP fallando de verdad ante un error, curl -w mide latencias, y la espera activa con retroceso exponencial sustituye a los sleep a ojo cuando hay que aguardar a que un servicio levante. Todo con tiempo de espera, nunca sin él; y sin credenciales en la URL ni curl -k.

estado-servicio.sh ya distingue proceso, puerto y aplicación. Pero se queda en la puerta: sabe que /salud devuelve un 200, no qué dice. La veloz-api responde JSON —estado de la base de datos, envíos en cola, versión desplegada, métricas—, y para un script eso es texto que no se debe tocar con grep ni con sed. La próxima lección (06-05) cierra el módulo con curl a fondo para APIs y con jq, la herramienta que convierte JSON en datos manejables y también en salida: informe-diario.sh dejará de escribir solo texto para publicar su resumen como JSON.

Curso de Programación en Bash

Módulo 1: Introducción a Bash

Módulo 2: Comandos Básicos de Bash

Módulo 3: Fundamentos de Scripting

Módulo 4: Scripting Intermedio

Módulo 5: Técnicas Avanzadas de Scripting

Módulo 6: Trabajando con Herramientas Externas

Módulo 7: Automatización y Programación

Módulo 8: Mejores Prácticas y Optimización

Módulo 9: Proyectos del Mundo Real

© Copyright 2026. Todos los derechos reservados