Una conexión que expira no te dice casi nada. El otro extremo puede no tener nada escuchando, un firewall a tres saltos puede estar tragándose tu SYN en silencio sin generar ni una sola línea de registro, o la ruta puede simplemente no existir — y desde donde estás sentado, todo eso tiene el mismo aspecto. Esperas. No pasa nada.

Así que el ticket se escribe como «el puerto 445 está bloqueado en algún sitio», y «en algún sitio» es lo que lo hace rebotar. Tu proveedor comprueba su borde, lo encuentra limpio, y te lo devuelve. Tú compruebas el firewall de tu máquina, lo encuentras limpio, y se lo devuelves. Pasa una semana. Nada se arregla.

La palabra que hace el daño es «en algún sitio». No tiene por qué estar ahí. Cada router entre tú y el destino está obligado a decirte cuándo es él, y esa obligación está escrita en las normas desde 1995. Solo tienes que preguntar de la forma correcta.

Por qué hay que dejarlo por escrito

Porque demasiada gente en este sector no sabe hacer lo básico, y le cuesta a sus empresas dinero real cada semana.

No hablo de los junior. Hablo de gente con años a la espalda, certificaciones en la pared y «senior» en el título del puesto, cuyo diagnóstico de un puerto que expira se para en «está bloqueado» y no va más allá. Lanzan ping. Falla, o funciona, y en cualquier caso no han aprendido nada sobre el puerto por el que les preguntaban. Luego el ticket va al proveedor, el proveedor lo rebota, y quince días del sueldo de alguien se van en un hilo que podría haber sido un solo comando.

Nada de esto es difícil. Distinguir un descarte de un rechazo, leer el TTL de una respuesta, saber que una estrella en un traceroute no significa nada por sí sola — es una tarde de aprender y dura una carrera. La razón de que la gente no lo sepa no es que sean cortos. Es que nadie lo enseña. La formación de fabricante te enseña la consola de un fabricante. Las certificaciones te enseñan el examen. Los fundamentos de debajo se dan por sabidos el primer día y nunca se cubren de verdad, así que la gente llega a puestos senior sin que nunca se los hayan mostrado, y a esas alturas da vergüenza preguntar.

Así que en vez de quejarme, aquí lo tienes por escrito. Esto es lo básico, detallado, con los comandos y un script que puedes lanzar hoy.

Y una palabra sobre por qué me molesté: lancé esto contra mi propia línea mientras lo escribía y encontré tres defectos que no sabía que tenía. Un descarte de SMB a once saltos. Resets de SMTP forjados a un salto. Una regla de firewall que existe en IPv4 y no en IPv6. Una tarde, sin root, en una línea que gestiono yo mismo y a la que presto atención. Piensa un momento en lo que hay ahí, sin que nadie lo vea, en las redes que a alguien le pagan por gestionar.

Por qué el Fisher-Price OS (Windows) no está aquí

Dos razones por las que no está aquí. Una es técnica y la otra no, y prefiero darte las dos antes que fingir que todo es ingeniería.

La técnica es que no sabe hacer esto. tracert envía peticiones de echo de ICMP y nada más — la propia referencia de Microsoft lo describe como «sending Internet Control Message Protocol (ICMP) echo Request or ICMPv6 messages to the destination with incrementally increasing time to live (TTL) field values», y no hay ningún parámetro de puerto en ninguna parte de su sintaxis. pathping es esa misma herramienta con estadísticas atornilladas encima. Test-NetConnection te dirá que un puerto TCP está abierto o cerrado y absolutamente nada sobre a qué distancia se originó la respuesta. Ninguno de ellos puede sondear el puerto que de verdad te importa a una distancia elegida, que es todo el método de esta entrada.

Y tampoco puedes salirte del paso con un script. Poner el TTL en un socket es bastante fácil en .NET, pero releer el error ICMP es la mitad difícil, y no hay equivalente del truco en el que se apoya esta entrada — ninguna forma de que el núcleo informe del error en el socket corriente que lo causó. Queda el socket en bruto, y la propia documentación de Microsoft dice «only members of the Administrators group can create sockets of type SOCK_RAW». Así que la vía sin privilegios no existe y la privilegiada quiere un token de administrador local. Ya estás metido en descargas de terceros antes de haber empezado.

La otra razón es que me da igual, y prefiero decirlo a disfrazarlo. El nombre tampoco es un golpe gratuito, es una descripción. Es el sistema que te endosan cuando nunca has tenido otro, te esconde la máquina como objetivo de diseño y no por accidente, y en el instante en que quieres hacerle a la red una pregunta precisa resulta que la herramienta nunca se construyó — porque de la gente para la que está hecho no se esperaba que preguntara. Treinta años después y tracert todavía no sabe apuntar un paquete a un puerto.

No es un sistema operativo de verdad para informáticos de verdad, y si es el único que has usado en tu vida entonces no estás haciendo este trabajo al nivel para el que está escrita esta entrada. Sé que eso sienta mal. No escribo para caer bien, escribo lo que mido para gente que mide cosas, y me importa poco que a alguien que nunca ha corrido otra cosa le gustara más que lo dijera de otro modo.

La lista de herramientas es el argumento, no la opinión. Cada Unix de la tabla de más abajo te dejará poner un límite de saltos y elegir un protocolo, cuatro de ellos desde un solo comando, y Linux hará toda la medición sin ni siquiera un sudo. Y no es porque sean más difíciles de usar. Es porque los construyeron personas que esperaban que quien estuviera al teclado quisiera saber cosas. Un sistema operativo cuyo diagnóstico acaba en ping te ha dicho a las claras lo que piensa de la persona que lo usa, y treinta años de gente que lo aceptó son cómo acabamos con un sector incapaz de localizar un paquete perdido.

Así que no lo corro, no lo he corrido en serio desde hace años, y no voy a escribirle una sección propia para parecer ecuánime sobre una carencia que es real. Todo sobre lo que construyo y todo desde lo que vale la pena medir es Unix, y ahí es donde vive esta entrada.

Si la máquina rota resulta estar corriendo el Fisher-Price OS (Windows), eso no cambia nada del método. Consigue un shell en cualquier otra cosa — una VM Linux, un Mac, una Raspberry Pi en el mismo switch — y sube el límite de saltos hacia ella. A la medición le da absolutamente igual qué corre el otro extremo. Solo le importa qué hay en medio.

El TTL es un presupuesto de saltos, y cada router te debe un recibo

La cabecera IPv4 tiene un campo Time to Live de 8 bits. El nombre es un resto: se especificó en segundos, y nada lo trata como segundos desde hace décadas. La RFC 1812 zanjó la discusión en 1995 e hizo normativa la lectura en número de saltos:

Each router (or other module) that handles a packet MUST decrement the TTL by at least one, even if the elapsed time was much less than a second. Since this is very often the case, the TTL is effectively a hop count limit on how far a datagram can propagate through the Internet.

Luego la parte que importa aquí, de la misma sección:

If the TTL is reduced to zero (or less), the packet MUST be discarded, and if the destination is not a multicast address the router MUST send an ICMP Time Exceeded message, Code 0 (TTL Exceeded in Transit) message to the source.

Eso es un MUST. No una cortesía, no una sugerencia. La RFC 792 de 1981 solo decía que una pasarela «may also notify the source host»; trece años después el requisito se apretó, y la RFC 1812 dice por qué con todas las letras:

ICMP Time Exceeded messages are required because the traceroute diagnostic tool depends on them.

IPv6 dejó el disimulo y renombró el campo. La RFC 8200 lo llama Hop Limit — «8-bit unsigned integer. Decremented by 1 by each node that forwards the packet» — y el mensaje de expiración pasó a ser el tipo 3, código 0 de ICMPv6, «hop limit exceeded in transit».

Lee eso como un instrumento en vez de como una regla y dice algo útil. Cada router del camino es una baliza a la que puedes dirigirte por la distancia. Pon el límite de saltos a 4 y el cuarto router se identifica. No necesitas conocer la topología, no necesitas acceso a nada, y no necesitas la cooperación del operador. Necesitas un paquete por salto.

Traceroute hace exactamente esto desde finales de los años 1980. Lo que hace mal es justo la cosa que te importa, porque por defecto sondea puertos UDP en el rango de 33434, un puerto que nadie filtra y que nadie sirve, así que te informa de un camino que nada real usa jamás. Un traceroute limpio hacia una máquina a la que no llegas por TCP/445 solo prueba que el UDP/33434 llega ahí. Que nunca fue la pregunta.

Así que sondea el puerto que te importa.

Lee lo que volvió, no si volvió algo

Antes de contar saltos, mira qué hace el otro extremo cuando lo alcanzas con un límite de saltos normal. Hay cinco respuestas distintas y la gente las reduce habitualmente a una sola.

Lo que vuelveLo que significaQuién lo envió
SYN-ACK, la conexión se abreel puerto está abiertoel host, o algo que responde por él
RST TCPrechazado activamenteel host sin nada escuchando, o un equipo configurado para rechazar
ICMP 3/13, communication administratively prohibitedun equipo rechaza por política y lo diceese equipo — su dirección de origen es tu respuesta
ICMP 3/1, 3/2, 3/3host, protocolo o puerto inalcanzableel último router, o el host
nada en absolutoalguien descarta en silenciodesconocido, así que ve y mídelo

La tercera fila es aquella por la que vale la pena cambiar tus costumbres. Cuando un firewall está configurado para rechazar en vez de descartar, pone su propia dirección en el campo de origen del ICMP y te entrega al culpable gratis. En Linux es lo que produce nft ... reject with icmpx admin-prohibited, y lo que iptables -j REJECT --reject-with icmp-admin-prohibited ha producido siempre. La mayoría de las herramientas lo tiran y muestran «filtrado». nmap --reason te lo enseñará. El script de más abajo también.

Las filas dos y cinco son el fallo interesante. Un descarte silencioso es una decisión de política de no decirte nada, y como es el valor por defecto en casi todos los firewalls comerciales, es el que te encontrarás de verdad. Cuenta con el silencio.

La fila dos también merece recelo. Un reset no es prueba de que el host lo enviara, y volveré a ello con un ejemplo real, porque encontré uno en mi propia línea mientras escribía esto.

Recorre el puerto que te importa, dos veces

El método son dos recorridos y un diff. Eso es todo.

  1. Sube el límite de saltos de 1 a 20 con el protocolo y el puerto exactos que fallan, y anota qué router responde en cada salto.
  2. Haz lo mismo con algo que funcione — idealmente el mismo host y un puerto que abre.
  3. El salto donde las respuestas se paran en el recorrido uno, y siguen en el recorrido dos, es el equipo que te descarta. El recorrido dos te da su dirección.

Por qué las respuestas se paran en vez de cambiar: un router aplica su lista de acceso de entrada antes de hacer cualquier otra cosa con el paquete, así que si la política dice descartar entonces el paquete ya se ha ido antes de que el camino de reenvío mire siquiera el límite de saltos, no se genera ningún Time Exceeded, y el equipo nunca firma lo que hizo. Por eso, el silencio empieza en el salto culpable, no después de él.

Escribí una pequeña herramienta para el recorrido porque ninguna de las nativas lo hace de forma portable. Pone IP_TTL (o IPV6_UNICAST_HOPS) en un socket corriente, conecta, y relee el error ICMP. En Linux, IP_RECVERR informa de ese error en el mismísimo socket que lo provocó, lo que significa que todo funciona sin privilegios — sin root, sin sockets en bruto, sin capacidades. En macOS, los BSD y Solaris el núcleo no te entrega el ICMP de esa forma, así que recurre a un socket en bruto y necesita root.

hopfind.py — el recorredor de TTL hopfind.py · 11 kB

Son 279 líneas de biblioteca estándar y nada más, y todo está impreso al final de esta entrada si prefieres leerlo a descargarlo.

python3 hopfind.py example.net 445      # the port under suspicion
python3 hopfind.py example.net 443      # the reference run
python3 hopfind.py example.net 53 --proto udp
python3 hopfind.py 2001:db8::1 443 -6

Usa el mismo protocolo para los dos recorridos. Comparar una traza TCP con una traza ICMP es comparar dos caminos, porque el balanceo de carga hace hash sobre la quíntupla y el ICMP no tiene puertos que hashear. Mismo host, mismo protocolo, puerto distinto: esa es la comparación honesta.

Todo lo de abajo es salida real de mi propia línea el 28 de agosto de 2026, lanzada como usuario sin privilegios en Fedora. Cada dirección de ella se ha reescrito en los rangos de documentación — RFC 5737 para IPv4, RFC 3849 para IPv6, con el único identificador de interfaz alterado también. Así que 198.51.100.x es mi propio router y mi ISP, 203.0.113.x es tránsito y peering, 192.0.2.x es la red remota, y 2001:db8::/32 es todo el camino IPv6. La estructura se conserva en todas partes: mismos límites de prefijo, mismas formas de parte de host, mismo número de redes distintas. Los números de salto, los tiempos, los tipos ICMP y qué salto se calló son exactamente como se midieron.

Un host, tres puertos, tres defectos distintos

El mismo destino en todo: una máquina en el internet público, a catorce saltos, con el 443 abierto. Primero el recorrido de referencia.

$ python3 hopfind.py 192.0.2.4 443 --max 15
walking to 192.0.2.4  TCP/443  hop limit 1-15
  1  198.51.100.254                              0.3 ms  ICMP 11/0 time exceeded in-transit
  2  198.51.100.133                             28.8 ms  ICMP 11/0 time exceeded in-transit
  3  *
  4  198.51.100.153                              6.3 ms  ICMP 11/0 time exceeded in-transit
  5  *
  6  203.0.113.240                              16.5 ms  ICMP 11/0 time exceeded in-transit
  7  203.0.113.188                              16.5 ms  ICMP 11/0 time exceeded in-transit
  8  203.0.113.185                              16.5 ms  ICMP 11/0 time exceeded in-transit
  9  203.0.113.15                               15.6 ms  ICMP 11/0 time exceeded in-transit
 10  203.0.113.125                              16.1 ms  ICMP 11/0 time exceeded in-transit
 11  192.0.2.31                                 26.6 ms  ICMP 11/0 time exceeded in-transit
 12  *
 13  *
 14  192.0.2.4                                  19.5 ms  connected

Verdict: TCP/443 is open. It answered at hop 14.

Fíjate en los saltos 3, 5, 12 y 13. Cuatro routers en un camino que manifiestamente funciona no dijeron nada, porque mucho equipo está configurado para no generar ICMP por sí mismo, o lo limita en tasa muy fuerte. Una estrella no es prueba de un firewall. Quédate con eso aunque no te quedes con nada más. La señal nunca es la presencia de estrellas en un solo recorrido; es el punto donde dos recorridos dejan de coincidir.

Ahora el mismo host en el 445, que expira desde aquí.

$ python3 hopfind.py 192.0.2.4 445 --max 15
walking to 192.0.2.4  TCP/445  hop limit 1-15
  1  198.51.100.254                              0.3 ms  ICMP 11/0 time exceeded in-transit
  2  198.51.100.133                              8.0 ms  ICMP 11/0 time exceeded in-transit
  3  *
  4  198.51.100.153                              6.4 ms  ICMP 11/0 time exceeded in-transit
  5  203.0.113.76                                5.6 ms  ICMP 11/0 time exceeded in-transit
  6  203.0.113.240                              15.9 ms  ICMP 11/0 time exceeded in-transit
  7  203.0.113.188                              15.8 ms  ICMP 11/0 time exceeded in-transit
  8  203.0.113.185                              16.6 ms  ICMP 11/0 time exceeded in-transit
  9  203.0.113.15                               15.9 ms  ICMP 11/0 time exceeded in-transit
 10  203.0.113.125                              15.0 ms  ICMP 11/0 time exceeded in-transit
 11  *
 12  *
 13  *
 14  *
 15  *

Verdict: answers stop after hop 10 (203.0.113.125).
         Whatever swallows TCP/445 is hop 11.
         Walk a port that works and read off the address at hop 11.

El salto 11 respondió al recorrido del 443 en 26,6 ms y no dijo nada en absoluto al recorrido del 445. Misma caja, mismo camino, los mismos diez routers delante. Cruza con el recorrido que funciona y el salto 11 tiene nombre: 192.0.2.31. Ese es el equipo que descarta SMB, a tres saltos del destino y a ocho saltos más allá del borde de mi proveedor. No el mío, y no el de mi ISP.

El salto 5 hace la demostración sobre las estrellas en el otro sentido. Era una estrella en el recorrido del 443 y respondió en el del 445 — al revés que el defecto. Puede ser limitación de tasa de ICMP, o pueden ser los dos recorridos tomando caminos distintos a través de un balanceador de carga. No sé cuál, y tú tampoco. Repite los dos recorridos antes de creerte cualquiera de ellos.

Dos recorridos de límite de saltos al mismo host, y el salto donde dejan de coincidirDos recorridos al mismo host, y el salto donde dejan de coincidirUna sonda por límite de saltos. Una celda sombreada significa que ese router devolvió ICMP Time Exceeded y se nombró a sí mismo.el router respondióno volvió nadaen silencio a partir de aquí1234567891011121314límite de saltos en la sondaTCP/443referencia, abre••*•*••••••**✓abreTCP/445bajo prueba, expira••*•••••••****el salto 11 respondió a un recorrido y al otro noLos saltos 3, 5, 12 y 13 no dijeron nada en un camino que funciona, así que una estrella por sí sola no dice nada.El salto 11 respondió al recorrido de referencia en 26,6 ms y al de prueba no respondió nunca.Esa es la frontera, y el recorrido de referencia es lo que le da una dirección: 192.0.2.31.
Los mismos dos recorridos, uno al lado del otro. La única celda que importa es el salto 11, y lo que importa de ella es el desacuerdo: respondió a un recorrido y al otro no.

Luego el puerto 25. Este me paró en seco.

$ python3 hopfind.py 192.0.2.4 25 --max 15
walking to 192.0.2.4  TCP/25  hop limit 1-15
  1  192.0.2.4                                   0.4 ms  TCP reset

Verdict: a reset came back to a probe with a hop limit of 1.
         Nothing more than one hop away can have sent it, so check the reply
         TTL before you believe the host did.

Un límite de saltos de 1 significa que el paquete murió en mi propio router. Dio un salto. No puede haber dado catorce. Y sin embargo volvió un reset TCP en 0,4 ms con la dirección del destino encima, y mi núcleo informó dócilmente de «conexión rechazada». Sin el límite de saltos puesto lo habría leído como «el otro extremo no tiene servidor de correo» y habría cerrado el ticket.

Algo a un salto forja resets para el SMTP saliente y los firma con la dirección del destino. Bloquear el 25 saliente es una cosa perfectamente corriente para un router de consumo o un ISP, y hacerlo con un reset en vez de un descarte es probablemente la versión educada, pero el reset lleva la dirección de otro y yo no tenía ni idea de que la mía lo hiciera. El límite de saltos es lo que lo pilló, y nada más en la respuesta lo habría hecho.

Fuera por uno: dónde se coloca la lista de acceso en el pipeline

El veredicto de arriba dice que el que descarta es el salto 11 porque las respuestas se pararon después del salto 10. Cuidado con esa aritmética, porque depende del orden en que el equipo culpable hace dos trabajos.

La mayoría del equipo aplica la política de entrada primero y la comprobación del límite de saltos después. El rechazo golpea, el paquete se descarta, y no se genera nunca ningún Time Exceeded, así que el equipo no aparece jamás y el silencio empieza en su propio número de salto. Ese es el caso de arriba. También es el caso común.

Algunas plataformas tratan la expiración del límite de saltos en el camino rápido, antes de evaluar la política. Ahí, el equipo responde a la sonda dirigida a él y solo descarta las sondas que apuntan más allá de él, así que aparece con normalidad y el silencio empieza un salto más tarde.

Por qué el salto culpable no suele aparecer: la política se evalúa antes de la comprobación del límite de saltosDentro del salto que te descarta, el orden de dos comprobaciones decide lo que vesTu sonda llega con un salto restante en su presupuesto, y coincide con una regla que dice deny.La política primero — casi todos los firewalls, y cada caso medido en esta entradael router en el salto Nsonda entrapolítica de entradalímite de saltosreenviardescartada antes de que nada mire el límite de saltosNo se genera ningún ICMP, así que este router nunca firma el descarte con su nombre.Tu recorrido se calla at en el salto N.La expiración primero — algunas plataformas la tratan en el camino rápidoel router en el salto Nsonda entralímite de saltospolítica de entradareenviarexpiró, así que ICMP 11/0 vuelve con la dirección de este routerResponde por sí mismo y solo se traga las sondas que apuntan más allá.Tu recorrido se calla desde el salto N+1.
Por qué el salto que descarta suele quedar invisible. En casi todos los firewalls el deny se evalúa primero, así que la sonda ya se ha ido antes de que el camino de reenvío note que el límite de saltos expiró y no se genera ningún ICMP. En equipos que tratan la expiración en el camino rápido, el mismo equipo responde a la sonda dirigida a él y solo se traga las que apuntan más allá.

Así que la lectura honesta de «las respuestas se paran después del salto N» es: el que descarta es el salto N+1 si tiró tu sonda antes de notar que el presupuesto se había agotado, o el salto N mismo si respondió a la sonda dirigida a él y se tragó todo lo que apuntaba más allá. Dos equipos vecinos, y el recorrido que funciona los nombra a los dos. Cita las direcciones, no el número de salto — un número de salto no significa nada para la persona que lee tu ticket, que cuenta desde otro sitio.

El TTL de la respuesta te dice quién respondió de verdad

El reset de SMTP de arriba lo pilló el límite de saltos a la ida. Hay una segunda comprobación, independiente, disponible en cada respuesta que vuelve, y no cuesta nada.

Los valores iniciales de TTL no están normalizados, pero en la práctica hay tres:

Parte deEmisor típico
64Linux, macOS, los BSD, illumos, la mayoría de los hosts
255Cisco IOS, Junos, Solaris, el tráfico propio de la mayoría del equipo de red
128el Fisher-Price OS (Windows), que aún necesitas para leer una respuesta salida de uno de ellos

Resta el TTL que recibiste del valor inmediatamente superior y tienes el número de saltos a la vuelta. Una respuesta que llega con TTL 50 salió de 64 e hizo 14 saltos. Una que llega a 250 salió de 255 e hizo 5. ping lo muestra sin que se lo pidas:

ping -c1 192.0.2.4          # ttl=50 → 14 hops away
tcpdump -n -v 'icmp'        # -v prints the ttl of every packet it shows

Dos cosas salen de ahí. Las dos son gratis.

Una respuesta cuyo número de saltos a la vuelta no coincide con las demás respuestas del host no la envió el host. Si una respuesta de echo de un servidor vuelve desde 14 saltos y el RST en el puerto 25 vuelve desde 1 salto, una caja intermedia escribió el RST. Mismo truco que la sección de arriba, por el otro extremo, y funciona incluso cuando no puedes poner el TTL de salida.

Una respuesta que salió de 255 vino de equipo de red, no de un servidor. Útil cuando intentas averiguar si la cosa que te rechaza es el host o el router que tiene delante.

Para ver el campo en TCP en vez de en ICMP necesitas una captura, y el filtro merece aprenderse de memoria:

# every hop-limit expiry coming back to you, IPv4 and IPv6
tcpdump -n -v 'icmp[icmptype] == 11 or icmp6[icmp6type] == 3'

# who is resetting you, and from how far
tcpdump -n -v 'tcp[tcpflags] & tcp-rst != 0'

tcpdump muestra la expiración como ICMP time exceeded in-transit — la misma fórmula que usan las normas, y el mismo evento que aparece como «TTL expired in transit» en las plataformas que lo formulan así.

El mismo truco con las herramientas nativas, en cinco Unix

Si prefieres no lanzar un script, las herramientas nativas harán lo esencial. Solo que están más en desacuerdo entre ellas de lo que esperarías. -P en particular significa tres cosas distintas según el traceroute que tengas en la mano, y una de ellas arruinará tu prueba en silencio.

Linux (traceroute 2.1.x)macOS / FreeBSDOpenBSDNetBSDSolaris 11
Sondas TCP-T-P tcpinutilizablenono
Sondas ICMP-I-I-I-I-I
UDP a un puerto fijo-U -p N-e -p Nnonono
puerto de destino-p N (constante para TCP)-p N (se incrementa sin -e)-p N (se incrementa)-p N (se incrementa)-p N (se incrementa)
qué significa -Pno se usaprotocolo de sondaprotocolo numérico, «will not work reliably for most protocols»pone DF y sondea el MTU del caminopausa entre sondas, en segundos
necesita privilegiossí, para -T y -Isísísísí

Cada uno de ellos necesita sockets en bruto, así que cada uno necesita privilegios — aunque macOS y los BSD suelen entregar traceroute setuid root, así que quizá no tengas que teclear sudo delante. Linux no, y Fedora tampoco, que es la mitad de la razón de ser del script de arriba.

Tres trampas en esa tabla, y he visto a las tres estropear una tarde.

En los BSD y macOS, -p es un puerto de base que se incrementa con cada sonda. Así que traceroute -P tcp -p 445 host prueba el 445, luego el 446, luego el 447, y para el salto 10 estás preguntando por un puerto del que nadie ha oído hablar. Necesitas -e además, que la página de manual llama modo de evasión de firewall y que en realidad solo significa «mantén el puerto quieto»:

sudo traceroute -P tcp -e -p 445 example.net     # macOS, FreeBSD

En Linux, -p a secas hace lo mismo para el método UDP por defecto y necesitas -U -p para un puerto UDP constante. Para TCP, -T -p ya es constante — la página de manual es explícita: «for TCP and others specifies just the (constant) destination port to connect».

sudo traceroute -T -p 445 example.net            # Linux
sudo traceroute -U -p 53  example.net            # Linux, UDP/53 specifically

En Solaris y NetBSD no hay forma alguna de fijar el puerto, y en Solaris -P es una pausa en segundos, así que una línea de comandos copiada de Linux se ejecutará sin error y no medirá nada de lo que pediste. Solaris tampoco tiene modo de sonda TCP. Este es el caso donde quieres el script.

Redox es el caso aparte y merece una frase porque me espero la pregunta. Toda su caja de herramientas de red es netutils — dns, ifconfig, nc, ping, telnetd, wget. Sin traceroute, sin tcpdump, nada con lo que capturar. Si una máquina Redox es un extremo del problema, mide desde el otro extremo y apunta el recorrido hacia ella.

mtr también merece una mención, porque hace la parte de «repetir y promediar» que las tablas de arriba te hacen hacer a mano:

sudo mtr -T -P 445 --report --report-cycles 20 example.net

Lanza eso contra el puerto que falla y otra vez contra uno que funciona, uno al lado del otro. Mismo método, salida más bonita.

Nada de esto funciona si alguien bloquea el ICMP

Cada medición de esta entrada está hecha de errores ICMP que me vuelven. Bloquéalos y todo el diagnóstico se apaga — y bastante más con él.

Bloquear el ICMP en bloque todavía se trata como una postura de seguridad en algunos sitios. No lo es de forma defendible desde los años 1990. Los ataques que se imagina que detiene eran el ping of death y el smurf, ambos corregidos en las pilas en vez de en la frontera, y ambos corregidos antes de que naciera alguno de los ingenieros que aún repiten el consejo. Lo que el bloqueo total detiene ahora es el diagnóstico. Nada más.

La RFC 1812 no deja margen para la interpretación en este punto: Time Exceeded es un MUST, y la norma declara que su razón de ser es que traceroute depende de él. Descártalo y has roto una herramienta que el propio documento de requisitos de los routers de internet nombra como la justificación de que el mensaje exista.

El descubrimiento de MTU del camino es el que sale caro. Necesita que el ICMP 3/4, fragmentation needed, vuelva al emisor. Filtra eso y obtienes el defecto que todo ingeniero de red ha perseguido al menos una vez, aquel donde el handshake se completa y las transferencias pequeñas funcionan y todo lo que lleva un paquete de tamaño completo se cuelga para siempre. SSH conecta y scp se atasca. La página carga y la imagen no llega nunca. Nada en los registros. Nada que grepear.

En IPv6 deja de ser una cuestión de gusto. La RFC 4890 §4.3.1 lista los mensajes que un firewall no debe descartar:

  • Destination Unreachable (Type 1) - All codes
  • Packet Too Big (Type 2)
  • Time Exceeded (Type 3) - Code 0 only
  • Parameter Problem (Type 4) - Codes 1 and 2 only

y sobre Packet Too Big es franca sobre la consecuencia: «Effectively, parts of the Internet will become inaccessible.» Los routers IPv6 no fragmentan. Si Packet Too Big no puede alcanzar al emisor, no hay vía de recuperación.

El control es una limitación de tasa, no un descarte. Permite los tipos 3 y 11 de entrada, cuéntalos, tópalos en algo como cien por segundo, registra lo que pase del tope, y has conservado el diagnóstico, conservado el descubrimiento de MTU del camino en marcha, y conservado hasta la última brizna de la protección que la regla total se imaginaba que aportaba en primer lugar. Restringir cuánto aceptas de una cosa es un control. Rechazarla entera y llamar a eso endurecimiento es solo negarse a que te midan.

Para cualquiera que gestione un MSP: si la línea de tu cliente descarta los errores ICMP, le has quitado la capacidad de probar en qué red está un defecto — y la tuya con ella. La próxima vez que un defecto se siente entre dos proveedores que ambos dicen estar limpios, esa es la factura de la política.

Salvo el echo. Ese, descártalo.

Todo lo anterior habla de los errores ICMP. El echo es un animal distinto, y es la única parte del protocolo que yo quitaría del cable en la frontera.

Mira qué le exige la norma. En IPv4, la RFC 792 dice de una petición de echo que «the data received in the echo message must be returned in the echo reply message». IPv6 es más franca aún — la RFC 4443 define el campo como «zero or more octets of arbitrary data» y luego exige que «MUST be returned entirely and unmodified in the ICMPv6 Echo Reply message».

Lee eso como atacante en vez de como operador. La norma obliga a cada host del planeta a aceptar un bloque de bytes que tú eliges y a devolvértelo tal cual. Eso no es un efecto secundario. Es un canal bidireccional, de carga arbitraria, impuesto por la norma, que circula sobre un protocolo que la mayoría de los firewalls dejan pasar sin inspeccionar y que la mayoría de los sistemas de registro anotan como un contador de paquetes en vez de como contenido.

Llevan treinta años construyendo túneles encima. Loki lo hizo en Phrack 49 en 1996. Ptunnel llevará una sesión TCP entera dentro de ping y está a un paquete de distancia desde hace dos décadas. Si tu política de salida es «bloquear todo, permitir el ICMP porque el equipo de red lo necesita», no tienes política de salida. Tienes una VPN con pasos de más, y el tráfico sale con pinta de alguien probando si internet funciona.

La RFC 4890 no está de acuerdo conmigo, y más vale decirlo con claridad que citar solo la mitad que me conviene. El §4.3.1 mete Echo Request y Echo Response en la misma lista de no descartar que los errores. Luego lee la justificación que da:

For Teredo tunneling [RFC4380] to IPv6 nodes on the site to be possible, it is essential that the connectivity checking messages are allowed through the firewall.

La razón que se aduce para mantener el echo abierto es que alguien necesita construir un túnel a través de tu firewall con él. Ese es mi argumento, escrito por la gente que defiende lo contrario.

Así que la política es estrecha, no total:

# transit rules. permit the errors, drop the ping
ip protocol icmp icmp type { destination-unreachable, time-exceeded, parameter-problem } \
    limit rate 100/second accept
ip protocol icmp icmp type { echo-request, echo-reply } drop

ip6 nexthdr ipv6-icmp icmpv6 type { destination-unreachable, packet-too-big, \
    time-exceeded, parameter-problem } limit rate 100/second accept
ip6 nexthdr ipv6-icmp icmpv6 type { echo-request, echo-reply } drop

En IPv6, no lleves ese patrón a una cadena de enlace local o de host sin conservar el descubrimiento de vecinos. Los tipos 133 a 137 — de nd-router-solicit a nd-redirect — son cómo IPv6 hace el trabajo que ARP hace en IPv4. Descártalos y el segmento deja de funcionar en cuestión de minutos, y no tendrá pinta de un defecto de firewall. Filtra el echo en la frontera, no en el cable entre un host y su propio router.

¿Qué te cuesta eso? ping cruzando la frontera, y nada más. Todo lo de esta entrada sigue funcionando, porque ni una sola medición de aquí envía una petición de echo. hopfind.py recorre TCP y UDP y lee los errores que vuelven; traceroute -T y -U hacen lo mismo. El descubrimiento de MTU del camino necesita Packet Too Big, que es un error. El truco del TTL inverso funciona en cualquier respuesta, y un handshake TCP te dará una. Lo único que pierdes es la herramienta menos instructiva de la caja, y toda esta entrada es un argumento sobre por qué pararse en ping es el problema de partida.

Mi propia línea ya hace exactamente esto, aunque dudo que fuera a propósito. ping a mi pasarela por defecto da 100 % de pérdida, y el ICMP Time Exceeded de esa misma pasarela vuelve en 0,3 ms — como muestra cada traza de esta entrada. Echo cerrado, errores abiertos. Quien entregó ese firmware dio con la respuesta correcta, y solo me enteré de que lo había hecho al ir a mirar.

La misma regla, escrita una sola vez

Aquí está el defecto que no esperaba encontrar en mi propia casa. Un resolver DNS público, recorrido en TCP/443 sobre las dos familias, con minutos de diferencia.

$ python3 hopfind.py 192.0.2.53 443 --max 10
walking to 192.0.2.53  TCP/443  hop limit 1-10
  1  *
  2  *
  3  *
  4  *
  5  *
  6  *
  7  *
  8  *
  9  *
 10  *

Verdict: nothing answered at all, not even the first hop.

Nada en absoluto. Ni un solo salto. Mi propio router no informó ni siquiera de la expiración que forzosamente produjo, la misma expiración que informó en 0,3 ms para todos los demás recorridos de esta entrada — así que el rechazo se produce en el salto 1, antes incluso de que se ejecute la comprobación del límite de saltos, y el salto 1 es el mío.

El camino va bien, cosa que el mismo destino prueba en UDP:

$ python3 hopfind.py 192.0.2.53 53 --proto udp --max 10
  1  198.51.100.254                              0.3 ms  ICMP 11/0 time exceeded in-transit
  2  198.51.100.133                              5.4 ms  ICMP 11/0 time exceeded in-transit
  3  *
  4  198.51.100.167                             14.5 ms  ICMP 11/0 time exceeded in-transit
  5  203.0.113.50                                6.3 ms  ICMP 11/0 time exceeded in-transit
  6  203.0.113.174                               5.7 ms  ICMP 11/0 time exceeded in-transit
  7  203.0.113.201                               6.4 ms  ICMP 11/0 time exceeded in-transit
  8  *

Siete saltos de respuestas limpias hacia la misma dirección. Así que no es enrutado y no es el destino — algo en mi línea descarta el TCP hacia ese host y deja pasar el UDP.

Luego el mismo resolver, mismo puerto, en IPv6:

$ python3 hopfind.py 2001:db8:53::53 443 -6 --max 10
walking to 2001:db8:53::53  TCP/443  hop limit 1-10
  1  2001:db8:1:ee:beef:abcd:fec0:2a30           0.5 ms  ICMP 3/0 hop limit exceeded in-transit
  2  2001:db8:1::15c                            24.6 ms  ICMP 3/0 hop limit exceeded in-transit
  3  *
  4  2001:db8:2:200::50                          5.5 ms  ICMP 3/0 hop limit exceeded in-transit
  5  2001:db8:2:2::4                             5.7 ms  ICMP 3/0 hop limit exceeded in-transit
  6  2001:db8:2:991::                           16.6 ms  ICMP 3/0 hop limit exceeded in-transit
  7  2001:db8:53::53                             5.7 ms  connected

Verdict: TCP/443 is open. It answered at hop 7.

Todo recto. Siete saltos, sin historia. Mismo servicio, mismo puerto, misma intención, y la regla existe en una sola familia de direcciones.

Quien escribió esa regla la escribió para IPv4 y nunca escribió la gemela. No tengo ni idea de qué pretendía conseguir — mi suposición es algo relacionado con mantener el DNS local — pero fuera lo que fuera, lo lleva consiguiendo sobre la mitad del tráfico desde que esta línea tiene IPv6. Si estaba ahí por una razón, no funciona. Si no lo estaba, no debería estar ahí.

Esa es la versión de cada día del problema de la doble pila, y es mucho más común que las discusiones sobre si conviene desplegar IPv6 siquiera. Dos libros de reglas. Uno mantenido.

El script, al completo

Sin dependencias, sin instalación, nada más que la biblioteca estándar. Python 3.6 o posterior, y en Linux ningún privilegio en absoluto.

Las dos clases son toda la historia de la portabilidad. ErrorQueue es la vía Linux: arma IP_RECVERR en el socket, y después de que la sonda falle, lee MSG_ERRQUEUE y saca la dirección del router de la estructura sock_extended_err a la que el núcleo se la adosa. RawIcmp es todo lo demás: abre un socket ICMP en bruto, lee lo que llegue, coge la dirección de origen del paquete. La primera no necesita nada, la segunda necesita root, y al resto del programa le da igual cuál le tocó.

Un detalle merece señalarse porque es la diferencia entre una respuesta correcta y una plausible. En probe() la cola de errores se vacía antes de consultar SO_ERROR. Un error ICMP llega a un socket TCP como un simple errno — un ICMP 3/3 port unreachable llega como ECONNREFUSED, exactamente igual que un reset real — así que mirar SO_ERROR primero habría informado del reset de SMTP forjado más arriba en esta entrada como un rechazo honesto del otro extremo. Lee la cola primero y ee_origin te dice que habló un router.

#!/usr/bin/env python3
"""hopfind - work out how many hops away the thing blocking your port is.

Walks the IPv4 TTL, or the IPv6 hop limit, up from 1 and records which router
answers at each step - using the protocol and port you actually care about
instead of traceroute's default UDP high ports.

Run it twice. Once against something that works, once against the port that
does not. The hop where the answers stop is the device dropping you, and the
run that works gives you its address.

    python3 hopfind.py example.net 445            # the port under suspicion
    python3 hopfind.py example.net 443            # the reference run
    python3 hopfind.py example.net 53 --proto udp
    python3 hopfind.py 2001:db8::1 443 -6

On Linux this needs no privileges at all: IP_RECVERR and IPV6_RECVERR hand the
ICMP errors back on the ordinary socket that caused them. On macOS, the BSDs
and Solaris the errors have to be read off a raw ICMP socket, which means root.

Written for https://blogs.damiendye.uk/networking/how-far-away-is-the-firewall/
Public domain. Do what you like with it.
"""

import argparse
import errno
import os
import select
import socket
import struct
import sys
import time

# Linux socket options. Absent from the socket module on some builds, so they
# are spelled out rather than looked up.
IP_RECVERR = 11
IPV6_RECVERR = 25

# ee_origin values from linux/errqueue.h. Anything else means the errno came
# from the local stack rather than from a router.
SO_EE_ORIGIN_ICMP = 2
SO_EE_ORIGIN_ICMP6 = 3

ICMP_V4 = {
    (11, 0): "time exceeded in-transit",
    (11, 1): "fragment reassembly time exceeded",
    (3, 0): "net unreachable",
    (3, 1): "host unreachable",
    (3, 2): "protocol unreachable",
    (3, 3): "port unreachable",
    (3, 4): "fragmentation needed",
    (3, 9): "net administratively prohibited",
    (3, 10): "host administratively prohibited",
    (3, 13): "communication administratively prohibited",
    (5, 0): "redirect",
}

ICMP_V6 = {
    (3, 0): "hop limit exceeded in-transit",
    (3, 1): "fragment reassembly time exceeded",
    (1, 0): "no route to destination",
    (1, 1): "communication administratively prohibited",
    (1, 3): "address unreachable",
    (1, 4): "port unreachable",
    (2, 0): "packet too big",
}


def describe(family, icmp_type, icmp_code):
    table = ICMP_V4 if family == socket.AF_INET else ICMP_V6
    return table.get((icmp_type, icmp_code), "unrecognised")


def is_expiry(family, icmp_type):
    """Was this the router saying 'your hop budget ran out here'?"""
    return icmp_type == (11 if family == socket.AF_INET else 3)


class ErrorQueue:
    """Linux. The kernel reports the ICMP error on the socket that provoked it."""

    def arm(self, sock, family):
        if family == socket.AF_INET:
            sock.setsockopt(socket.IPPROTO_IP, IP_RECVERR, 1)
        else:
            sock.setsockopt(socket.IPPROTO_IPV6, IPV6_RECVERR, 1)

    def extra_readers(self):
        return []

    def collect(self, sock, family):
        try:
            _, ancillary, _, _ = sock.recvmsg(0, 1024, socket.MSG_ERRQUEUE)
        except OSError:
            return None
        wanted = (socket.IPPROTO_IP, IP_RECVERR) if family == socket.AF_INET \
            else (socket.IPPROTO_IPV6, IPV6_RECVERR)
        for level, kind, data in ancillary:
            if (level, kind) != wanted or len(data) < 16:
                continue
            # struct sock_extended_err, then the sockaddr of the router that
            # sent the error - SO_EE_OFFENDER in the kernel headers.
            _, origin, icmp_type, icmp_code = struct.unpack_from("=IBBB", data, 0)
            if origin not in (SO_EE_ORIGIN_ICMP, SO_EE_ORIGIN_ICMP6):
                return None
            addr = None
            if len(data) >= 24:
                offender_family, = struct.unpack_from("=H", data, 16)
                if offender_family == socket.AF_INET:
                    addr = socket.inet_ntoa(data[20:24])
                elif offender_family == socket.AF_INET6 and len(data) >= 40:
                    addr = socket.inet_ntop(socket.AF_INET6, data[24:40])
            return addr, icmp_type, icmp_code
        return None


class RawIcmp:
    """macOS, the BSDs, illumos, Solaris. Read the ICMP off a raw socket, as root."""

    def __init__(self, family):
        proto = socket.IPPROTO_ICMP if family == socket.AF_INET else socket.IPPROTO_ICMPV6
        self.sock = socket.socket(family, socket.SOCK_RAW, proto)
        self.sock.setblocking(False)

    def arm(self, sock, family):
        pass

    def extra_readers(self):
        return [self.sock]

    def collect(self, sock, family):
        try:
            packet, peer = self.sock.recvfrom(1500)
        except OSError:
            return None
        if family == socket.AF_INET:
            # BSD raw sockets hand back the IP header too.
            header_len = (packet[0] & 0x0F) * 4
            packet = packet[header_len:]
        if len(packet) < 2:
            return None
        return peer[0], packet[0], packet[1]


def probe(dest, port, proto, family, hop_limit, timeout, listener):
    """One probe at one hop limit. Returns (icmp, socket_state, note)."""
    kind = socket.SOCK_STREAM if proto == "tcp" else socket.SOCK_DGRAM
    sock = socket.socket(family, kind)
    if family == socket.AF_INET:
        sock.setsockopt(socket.IPPROTO_IP, socket.IP_TTL, hop_limit)
    else:
        sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_UNICAST_HOPS, hop_limit)
    listener.arm(sock, family)
    sock.setblocking(False)

    try:
        if kind == socket.SOCK_DGRAM:
            sock.connect((dest, port))
            sock.send(b"\x00" * 32)
        else:
            try:
                sock.connect((dest, port))
            except BlockingIOError:
                pass
    except OSError as exc:
        sock.close()
        return None, None, "local error: %s" % exc.strerror

    readers = [sock] + listener.extra_readers()
    writers = [] if kind == socket.SOCK_DGRAM else [sock]
    deadline = time.time() + timeout
    icmp = state = None

    while time.time() < deadline:
        ready_r, ready_w, ready_x = select.select(
            readers, writers, [sock], max(0.01, deadline - time.time()))
        if not (ready_r or ready_w or ready_x):
            continue
        # Drain the error queue first, always. An ICMP error reaches a TCP
        # socket as a plain errno, so SO_ERROR on its own cannot tell you
        # whether a router spoke or the far end did.
        icmp = listener.collect(sock, family)
        if icmp:
            break
        if ready_w:
            err = sock.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR)
            if err == 0:
                state = "connected"
            elif err == errno.ECONNREFUSED:
                state = "TCP reset"
            else:
                state = os.strerror(err)
            break

    sock.close()
    if icmp or state:
        return icmp, state, None
    return None, None, "no reply"


def walk(dest, port, proto, family, first, last, timeout, listener):
    print("walking to %s  %s/%d  hop limit %d-%d" % (dest, proto.upper(), port, first, last))
    answered = []
    for hop in range(first, last + 1):
        started = time.time()
        icmp, state, _ = probe(dest, port, proto, family, hop, timeout, listener)
        rtt = (time.time() - started) * 1000
        if icmp:
            addr, icmp_type, icmp_code = icmp
            print(" %2d  %-39s %7.1f ms  ICMP %d/%d %s"
                  % (hop, addr or "?", rtt, icmp_type, icmp_code,
                     describe(family, icmp_type, icmp_code)))
            if is_expiry(family, icmp_type):
                answered.append((hop, addr))
            else:
                return answered, hop, "icmp-reject", addr
        elif state:
            print(" %2d  %-39s %7.1f ms  %s" % (hop, dest, rtt, state))
            return answered, hop, state, dest
        else:
            print(" %2d  *" % hop)
    return answered, None, "silent", None


def main():
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument("host")
    parser.add_argument("port", nargs="?", type=int, default=443)
    parser.add_argument("--proto", choices=("tcp", "udp"), default="tcp")
    parser.add_argument("--first", type=int, default=1, help="hop limit to start at")
    parser.add_argument("--max", type=int, default=20, help="hop limit to stop at")
    parser.add_argument("--wait", type=float, default=2.0, help="seconds to wait per hop")
    parser.add_argument("-6", dest="v6", action="store_true", help="force IPv6")
    parser.add_argument("-4", dest="v4", action="store_true", help="force IPv4")
    args = parser.parse_args()

    family = socket.AF_INET6 if args.v6 else socket.AF_INET
    kind = socket.SOCK_STREAM if args.proto == "tcp" else socket.SOCK_DGRAM
    dest = socket.getaddrinfo(args.host, args.port, family, kind)[0][4][0]

    if sys.platform.startswith("linux"):
        listener = ErrorQueue()
    else:
        try:
            listener = RawIcmp(family)
        except PermissionError:
            sys.exit("%s cannot report ICMP errors on a normal socket, so this "
                     "needs a raw one. Run it as root." % sys.platform)

    answered, stop, why, who = walk(dest, args.port, args.proto, family,
                                    args.first, args.max, args.wait, listener)
    what = "%s/%d" % (args.proto.upper(), args.port)
    print()

    if why == "connected":
        print("Verdict: %s is open. It answered at hop %d." % (what, stop))
    elif why == "icmp-reject":
        print("Verdict: %s at hop %d is refusing %s on policy, and is honest "
              "enough to say so." % (who, stop, what))
    elif why == "TCP reset":
        print("Verdict: a reset came back to a probe with a hop limit of %d." % stop)
        print("         Nothing more than %s away can have sent it, so check the reply"
              % ("one hop" if stop == 1 else "%d hops" % stop))
        print("         TTL before you believe the host did.")
    elif answered:
        last_hop, last_addr = answered[-1]
        print("Verdict: answers stop after hop %d (%s)." % (last_hop, last_addr))
        print("         Whatever swallows %s is hop %d." % (what, last_hop + 1))
        print("         Walk a port that works and read off the address at hop %d."
              % (last_hop + 1))
    else:
        print("Verdict: nothing answered at all, not even the first hop. Either the")
        print("         first hop is the one dropping you, or the ICMP errors are being")
        print("         filtered on the way back. Walk a port that works to tell those")
        print("         two apart.")


if __name__ == "__main__":
    main()

Lo que esto no puede decirte

El método es barato y es honesto sobre la mayoría de las cosas, pero no es un escáner de topología. Sé recto en el ticket sobre lo que de verdad mediste.

Quíntuplas distintas pueden tomar caminos distintos. El ECMP hace hash de los puertos de origen y destino en la elección del siguiente salto, así que dos recorridos sobre dos puertos distintos no están garantizados de atravesar los mismos routers en absoluto, que es una de las dos cosas que podrían explicar que el salto 5 de arriba respondiera a un recorrido y se callara en el otro. Repite los dos recorridos. Una frontera que se mueve no está probada.

El MPLS esconde saltos. Un núcleo con conmutación de etiquetas puede presentarse como un solo salto, o como ninguno. Cualquier conteo a través de la red troncal de otro es una cota inferior.

La generación de ICMP está limitada en tasa en casi todas partes. Sondea más rápido de lo que el router quiere responder y te fabricas tus propias estrellas. hopfind.py envía una sonda por salto y espera; eso es deliberado.

El camino de vuelta no tiene por qué coincidir con el de ida. El número de saltos a la ida no es el número de saltos a la vuelta, y la aritmética del TTL inverso solo mide el trayecto de vuelta.

El anycast hace que el host en el salto N pueda no ser la misma caja dos veces. Los resolvers públicos y los CDN, en particular.

Un handshake completado no significa que la sesión sobreviva. Un firewall con estado puede permitir el SYN y descartar lo que sigue en la inspección. Si la conexión se abre y luego muere, este no es el instrumento adecuado — ve y captura.

Has encontrado el primer equipo que descarta, no el que alguien admitirá. En un CGN o una red de operador la dirección en el salto N+1 puede ser una de varias cajas detrás de una sola dirección. Sigue siendo lo correcto que citar, porque es un hecho sobre el camino.

Para qué sirve en realidad

Para acabar con los rebotes. Ese es todo el retorno del ejercicio.

«El puerto 445 está bloqueado en algún sitio» es una invitación a que te devuelvan el ticket. Esto no lo es:

TCP/445 hacia 192.0.2.4 se descarta silenciosamente en el salto 11, dirección 192.0.2.31. El salto 11 responde ICMP Time Exceeded al TCP/443 por el mismo camino en 26 ms y no responde nada en absoluto en el 445, así que el descarte es una decisión de política en ese equipo, no un defecto de enrutado. Los diez saltos que tiene delante están limpios. Reproducido cuatro veces a lo largo de veinte minutos, desde un shell sin privilegios, script adjunto.

Nadie te devuelve eso. Nombra un equipo, dice lo que hizo, dice lo que no hizo, y muestra el trabajo. Que decidan cambiarlo o no sigue siendo cosa suya. Pero la semana de ping-pong se acabó, y costó cuatro comandos.

Todo ello salió de un campo de 8 bits que se especificó como un temporizador en 1981, no se ha usado como tal ni una sola vez, y convierte discretamente «en algún sitio» en una dirección.

Vale la pena aprender a leerlo.