Apuntes · Capítulo 10Transporte y aplicaciones

TCP: conexiones, retransmisión y control de flujo

TCP: conexiones, retransmisión y control de flujo

La semana pasada construimos la entrega confiable desde cero: números de secuencia, ACK, temporizadores y ventana deslizante. Todo eso se quedó en el pizarrón, con paquetes numerados de uno en uno. Esta semana lo vemos funcionando en el protocolo que transporta buena parte del tráfico de Internet. TCP usa esas mismas ideas, con tres diferencias que lo cambian casi todo: numera bytes y no paquetes, tiene que adivinar cuánto dura un RTT que además cambia, y le habla a un receptor que puede ser más lento que el emisor.

En la Sesión A leemos TCP en capturas: el segmento byte por byte, el handshake, el MSS, los ACK acumulativos y retardados, y el cálculo del RTO. En la Sesión B lo hacemos sufrir: descartamos un segmento preciso para ver el fast retransmit, cortamos el enlace para ver el retroceso exponencial, y dejamos a un receptor sin leer para ver la ventana cero. Cerramos con el handshake y el cierre vistos desde sus fallas: qué devuelve connect en cada caso, para qué sirve TIME_WAIT y qué hace un SYN flood.

Vamos a encontrar una tensión que conviene decir desde ahora. Los libros y los RFC explican un TCP, y el kernel de Linux corre otro, compatible pero más afinado: su RTO no se calcula igual, su sonda de ventana cero no lleva datos, y a veces retransmite sin haber visto tres ACK duplicados. En cada caso les damos las dos versiones, la regla general y lo que se midió, para que sepan cuál es cuál.

Todo lo que se muestra salió de una topología de Containerlab que ustedes pueden levantar. Los puertos, los números de secuencia reales y los tiempos cambian en cada ejecución, y las cifras de velocidad dependen de la computadora. El comportamiento no.

Ruta de lectura.

Antes de clase (obligatorio, unos 15 a 20 minutos). Sesión A: La red de esta semana, Qué promete TCP, MSS y MTU, de El segmento TCP, byte por byte, la introducción y Secuencia y ACK cuentan bytes, y la introducción de Estimar el RTT y fijar el RTO. Sesión B: la introducción de Fast retransmit, la de Control de flujo, la de El handshake: abrir una conexión y A medio cerrar.

En clase. Sesión A: Los números relativos de Wireshark, La ventana y su escala, Una conexión completa, ACK acumulativos y ACK retardados, Ejemplo resuelto, Con muestras reales, Por qué no se mide un segmento retransmitido y El temporizador en acción. Sesión B: Por qué tres y no uno, SACK: decir exactamente qué llegó, Retransmisiones: lo que dice Wireshark y lo que pasó, Ventana cero, Quién limita: la ventana o el enlace, Por qué los números iniciales no empiezan en cero, TIME_WAIT, RST: abortar y SYN flood y SYN cookies.

Después de clase. Repasen con el Resumen y los Ejercicios, y preparen El laboratorio y make test. Opcional: Qué hace Linux, Mirar una conexión viva con ss, Qué devuelve connect y El costo de TIME_WAIT. Para profundizar, las lecturas del libro y las gratuitas de la semana:

Este capítulo acompaña a Kurose y Ross, Computer Networking: A Top-Down Approach (9.ª ed.). Para la Sesión A, las secciones 3.5.1 The TCP Connection, 3.5.2 TCP Segment Structure, 3.5.3 Round-Trip Time Estimation and Timeout y 3.5.4 Reliable Data Transfer, y los capítulos TCP Design y TCP Implementation de CS168. Complementarias: RFC 9293, TCP, y RFC 6298, cálculo del RTO. Para la Sesión B, las secciones 3.5.4 Reliable Data Transfer, 3.5.5 Flow Control y 3.5.6 TCP Connection Management, y otra vez TCP Implementation. El código de la semana está en code/week-10/.

La red de esta semana

Dos hosts, a y b, unidos por un router m que es donde estropeamos el camino. Está en code/week-10/tcp/tcp.clab.yml.

    a  10.210.1.10 ---- eth1  m  eth2 ---- 10.210.2.10  b
       10.210.1.20            |
                              +-- aquí se aplican tc netem y nft

El sentido de los datos es casi siempre de a hacia b. El enlace normal tiene 20 ms de retardo en cada sentido, o sea 40 ms de RTT, y 20 Mbit/s de a hacia b con una cola de 200 paquetes. Los escenarios clean, drop y classic usan una cola de 2000, y explicamos por qué más adelante.

¿Por qué un router en el medio, si para el laboratorio bastarían dos hosts unidos por un cable? Por dónde se pierde el paquete. Si tc netem se aplicara en el mismo host que captura, tcpdump no vería el segmento perdido: en la salida, las capturas se toman después de la cola de tc. Con el router en el medio, una captura en a muestra todo lo que a envió, y una captura en b muestra lo que de verdad llegó. Comparar las dos es la herramienta principal de esta semana.

Dentro del contenedor del curso, levanten la red y prueben un escenario:

cd code/week-10
make up
python3 scenarios.py clean
  m# tc qdisc add dev eth2 root netem delay 20ms rate 20mbit limit 2000
  m# tc qdisc add dev eth1 root netem delay 20ms
  a# tcpdump -i eth1 -n -s 128 -B 16384 --immediate-mode -Z root -w /captures/clean-a.pcap tcp port 5001 &
  b# tcpdump -i eth1 -n -s 128 -B 16384 --immediate-mode -Z root -w /captures/clean-b.pcap tcp port 5001 &
  b# /w10/sink 5001 &
  a# /w10/blast 10.210.2.10 5001 2000000
recibidos 2000000 bytes en 0.92 s (17.4 Mbit/s)

Cada escenario imprime los comandos que corre (en las salidas que siguen, los omitimos casi siempre). Léanlos: son lo mismo que harían a mano, y al final quedan en captures/ un archivo .pcap por cada lado. Los programas en C son pocos y cortos:

Programa Qué hace
blast.c Cliente: abre una conexión, envía N bytes y cierra
sink.c Servidor: acepta, lee todo y cuenta; con opciones para no leer, para contestar y para cerrar primero
connect_probe.c Intenta una conexión y dice qué devolvió connect y cuánto demoró
half_open.c Deja conexiones a medio abrir, para la sección de SYN flood
lab/slow_reader.c El receptor lento que ustedes van a completar

También hay herramientas en Python: tcpsum.py resume una captura, timeseq.py dibuja el gráfico tiempo-secuencia y rto_table.py hace la cuenta del RTO. La lista completa de escenarios sale con python3 scenarios.py.

abort            El cliente cierra con RST en vez de FIN; el servidor se entera en su read.
bindfail         El servidor cierra primero y queda en TIME_WAIT: ¿puede volver a abrir ese puerto?
classic          El mismo descarte, con SACK y RACK apagados: TCP con los ACK duplicados de los libros.
clean            Transferencia de 2 MB sin pérdidas, con una cola que no se puede desbordar.
closed           Conectarse a un puerto cerrado: el RST del kernel de b, capturado.
delack           Datos chicos cada 100 ms a un receptor que no lee: ¿cuánto espera antes del ACK?
drop             El router descarta un solo segmento, el número 40. TCP moderno: con SACK y RACK.
halfclose        El cliente cierra su sentido y después todavía recibe la respuesta del servidor.
limit            La misma transferencia con buffers de recepción distintos: ¿quién limita?
loss             2 % de pérdida aleatoria en el sentido de los datos.
mss              Baja el MTU de a a 1400: el MSS que a anuncia cambia, y también el tamaño de los datos.
ports            Qué devuelve connect: puerto cerrado, filtrado en silencio, rechazado, y sin ruta.
reorder          Sin pérdida, sin tope de velocidad y con 3 ms de jitter: los segmentos se desordenan.
reorder_classic  El mismo enlace desordenado, con el TCP de los ACK duplicados.
rto              Un flujo lento y un corte total de 3 s: nadie manda ACK, y queda solo el temporizador.
rtomin           Fija rto_min en la ruta de a: se ve qué parte del RTO de Linux es ese piso.
slow             Su slow_reader del laboratorio: 2 KB cada 100 ms, o sea unos 160 kbit/s.
ss               Una conexión activa vista con ss cada segundo: rtt, rto, cwnd.
syn              Una cola de SYN llena, sin y con SYN cookies.
tail             Se pierden el último segmento y el FIN: no llega ningún ACK que avise. ¿Quién lo arregla?
timewait         Se pierde el último ACK del cierre: ese es el trabajo de quien espera en TIME_WAIT.
zero             El receptor acepta y se queda 3 s sin leer: su buffer se llena y anuncia ventana cero.

Qué promete TCP

Definición. TCP ofrece a dos procesos un stream de bytes confiable, ordenado, con conexión y full duplex. Cada palabra de esa frase dice algo preciso:

  • Stream de bytes. Para la aplicación no hay mensajes. Es lo que vimos en la semana 2: un write de 3000 bytes no corresponde a un read de 3000 bytes. TCP decide cómo cortar el stream en segmentos.
  • Confiable y ordenado. Lo que read entrega son los mismos bytes en el mismo orden, o un error. Nunca entrega un hueco.
  • Con conexión. Antes de enviar datos, los dos extremos crean un estado: números de secuencia, buffers, temporizadores. Ese estado vive solo en los hosts. Los routers no saben que existe la conexión.
  • Full duplex. Son dos streams independientes, uno en cada sentido, y cada uno tiene sus propios números de secuencia. Cada segmento puede llevar datos de un sentido y el ACK del otro.

Ya vimos en la semana 9 que un socket TCP se identifica con cuatro campos (dirección y puerto de cada lado). Por eso un servidor en un solo puerto atiende a miles de clientes.

Respecto a los protocolos de la semana pasada, TCP es un híbrido. Se parece a Go-Back-N en que su ACK es acumulativo y lleva un solo temporizador. Se parece a Selective Repeat en que el receptor guarda lo que llega fuera de orden y el emisor retransmite poco, no toda la ventana.

Idea clave. TCP numera bytes, no paquetes, porque la aplicación le da un stream. Casi todo lo demás de esta semana es consecuencia de eso, o de que la red no le dice nada: TCP deduce lo que pasa solo por los ACK que vuelven y por el tiempo que tardan.

MSS y MTU

El MTU es el tamaño máximo de un paquete IP en un enlace. El MSS (maximum segment size) es la cantidad máxima de datos que TCP pone en un segmento, sin contar encabezados. El nombre confunde: el MSS no mide el segmento, sino lo que lleva adentro.

MSS = MTU - encabezado IP - encabezado TCP = 1500 - 20 - 20 = 1460

Cada extremo anuncia su MSS en el SYN con una opción. No es una negociación: cada uno dice cuánto está dispuesto a recibir, y los dos valores pueden ser distintos. Miremos el resumen de la transferencia limpia:

$ python3 tcpsum.py captures/clean-a.pcap
conexión       10.210.1.10:33776 -> 10.210.2.10:5001
SYN            seq real 1661972085; MSS 1460, ventana 64240, escala 7, SACK sí, timestamps sí
SYN-ACK        seq real 3960613348; MSS 1460, ventana 65160, escala 7, SACK sí, timestamps sí
datos          1382 segmentos, 2000000 bytes enviados, 2000000 bytes distintos, el mayor de 1448
ACK de vuelta  896 sin datos, ventana máxima anunciada 1035136
ACK duplicados 0 (el mayor, #0); rachas que llegaron a #1: 0, a #2: 0, a #3: 0
retransmitidos 0 segmentos, 0 de ellos marcados como fast
ventana cero   0 avisos, 0 sondas
cierre         2 FIN, 0 RST
duración       1.003 s

Anunciaron 1460, pero el segmento más grande lleva 1448 bytes. ¿Y los otros 12? Son la opción de timestamps, que va en todos los segmentos: 10 bytes de la opción y 2 de relleno. La cuenta completa de un frame lleno es:

14 (Ethernet) + 20 (IP) + 20 (TCP) + 12 (timestamps) + 1448 (datos) = 1514 bytes

Si el MTU cambia, cambia todo. El escenario mss baja a 1400 el MTU de la interfaz de a:

$ python3 tcpsum.py captures/mss-a.pcap
conexión       10.210.1.10:35582 -> 10.210.2.10:5001
SYN            seq real 4265168592; MSS 1360, ventana 65280, escala 7, SACK sí, timestamps sí
SYN-ACK        seq real 2465781842; MSS 1460, ventana 65160, escala 7, SACK sí, timestamps sí
datos          75 segmentos, 100000 bytes enviados, 100000 bytes distintos, el mayor de 1348

a anuncia 1360 y b sigue anunciando 1460: cada uno habla de su propia interfaz. Pero a envía datos de 1348 bytes como máximo (1360 − 12), porque tampoco puede enviar más de lo que su propio enlace deja pasar. Lo que ocurre si el MTU chico está más adelante en el camino lo vimos en la semana 4: el bit DF y el descubrimiento del MTU del camino.

Error común. Decir que “el MSS es 1500” o que “el segmento mide 1460”. 1500 es el MTU, que incluye IP y TCP. 1460 es lo que se anuncia. Y en una captura de Linux, lo que se ve en Len= suele ser 1448.

El segmento TCP, byte por byte

El encabezado tiene 20 bytes fijos, más las opciones:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-------------------------------+-------------------------------+
|         puerto origen         |        puerto destino         |
+-------------------------------+-------------------------------+
|                      número de secuencia                      |
+---------------------------------------------------------------+
|                        número de ACK                          |
+-------+-------+---------------+-------------------------------+
| largo | resv. |    banderas   |            ventana            |
+-------+-------+---------------+-------------------------------+
|           checksum            |        urgent pointer         |
+-------------------------------+-------------------------------+
|                     opciones (0 a 40 bytes)                   |
+---------------------------------------------------------------+

Vamos a leer un SYN real sin ayuda de Wireshark. Es el primer paquete de la captura limpia. Los bytes salieron de tshark -r captures/clean-a.pcap -c 1 -x:

0000  aa c1 ab 4f 0b 7e aa c1 ab 45 b5 1d 08 00 45 00   ...O.~...E....E.
0010  00 3c 55 59 40 00 40 06 cc ab 0a d2 01 0a 0a d2   .<UY@.@.........
0020  02 0a 83 f0 13 89 63 0f ae 75 00 00 00 00 a0 02   ......c..u......
0030  fa f0 b1 64 00 00 02 04 05 b4 04 02 08 0a 82 de   ...d............
0040  57 16 00 00 00 00 01 03 03 07                     W.........

Ethernet ocupa 14 bytes y el encabezado IP, 20. El encabezado TCP empieza en el byte 0x22:

Bytes Campo Valor
83 f0 Puerto origen 33776
13 89 Puerto destino 5001
63 0f ae 75 Número de secuencia 1661972085
00 00 00 00 Número de ACK 0: todavía no confirma nada
a0 Largo del encabezado (4 bits altos) 10 palabras de 4 bytes: 40 bytes
02 Banderas Solo SYN
fa f0 Ventana 64240
b1 64 Checksum Lo revisa el receptor
00 00 Urgent pointer Sin uso
02 04 05 b4 Opción MSS 1460
04 02 Opción SACK permitido
08 0a 82 de 57 16 00 00 00 00 Opción timestamps Valor propio y eco en cero
01 NOP Relleno, para alinear
03 03 07 Opción escala de ventana Desplazamiento de 7 bits

Las banderas que nos importan son SYN (abro una conexión y este es mi número inicial), ACK (el campo de ACK es válido), FIN (no voy a enviar más), RST (aborto) y PSH (entreguen esto a la aplicación sin esperar). Salvo el primer SYN, todos los segmentos de una conexión llevan ACK encendido.

Secuencia y ACK cuentan bytes

Definición. El número de secuencia de un segmento es el número del primer byte que lleva. El número de ACK es el número del siguiente byte que se espera recibir: “tengo todo hasta antes de este”.

Un ejemplo. Un segmento con seq=1000 y 500 bytes lleva los bytes 1000 a 1499. Si llegó ese y todo lo anterior, el ACK es 1500. Si se perdió algo anterior, el ACK se queda donde empieza el hueco, por más que este segmento haya llegado.

Hay un detalle que confunde al inicio: SYN y FIN gastan un número de secuencia, aunque no lleven datos. Si no fuera así, no habría forma de confirmarlos. Por eso, después de un SYN con número X, el primer byte de datos es X + 1. Veámoslo en la captura, con los números tal como aparecen en el paquete. Recortamos las columnas de la derecha:

$ tshark -r captures/clean-a.pcap -c 5 -o tcp.relative_sequence_numbers:FALSE
    1   0.000000  10.210.1.10 → 10.210.2.10  TCP 74 33776 → 5001 [SYN] Seq=1661972085 Win=64240 Len=0 MSS=1460
    2   0.041148  10.210.2.10 → 10.210.1.10  TCP 74 5001 → 33776 [SYN, ACK] Seq=3960613348 Ack=1661972086 Win=
    3   0.041166  10.210.1.10 → 10.210.2.10  TCP 66 33776 → 5001 [ACK] Seq=1661972086 Ack=3960613349 Win=64256
    4   0.041191  10.210.1.10 → 10.210.2.10  TCP 1514 33776 → 5001 [ACK] Seq=1661972086 Ack=3960613349 Win=642
    5   0.041195  10.210.1.10 → 10.210.2.10  TCP 1514 33776 → 5001 [PSH, ACK] Seq=1661973534 Ack=3960613349 Wi

Sigan los números. El SYN de a usa 1661972085, y b responde Ack=1661972086. El paquete 3 no lleva datos, así que no gasta nada: el paquete 4 sale con el mismo número. El paquete 5 tiene Seq=1661973534, que es 1661972086 + 1448. Cuenta bytes.

Los números relativos de Wireshark

La misma captura, sin esa opción de tshark:

$ tshark -r captures/clean-a.pcap -c 6
    1   0.000000  10.210.1.10 → 10.210.2.10  TCP 74 33776 → 5001 [SYN] Seq=0 Win=64240 Len=0 MSS=1460 SACK_PERM TSval=2195609366 TSecr=0 WS=
    2   0.041148  10.210.2.10 → 10.210.1.10  TCP 74 5001 → 33776 [SYN, ACK] Seq=0 Ack=1 Win=65160 Len=0 MSS=1460 SACK_PERM TSval=2105469646 
    3   0.041166  10.210.1.10 → 10.210.2.10  TCP 66 33776 → 5001 [ACK] Seq=1 Ack=1 Win=64256 Len=0 TSval=2195609407 TSecr=2105469646
    4   0.041191  10.210.1.10 → 10.210.2.10  TCP 1514 33776 → 5001 [ACK] Seq=1 Ack=1 Win=64256 Len=1448 TSval=2195609407 TSecr=2105469646
    5   0.041195  10.210.1.10 → 10.210.2.10  TCP 1514 33776 → 5001 [PSH, ACK] Seq=1449 Ack=1 Win=64256 Len=1448 TSval=2195609407 TSecr=21054
    6   0.041200  10.210.1.10 → 10.210.2.10  TCP 1514 33776 → 5001 [ACK] Seq=2897 Ack=1 Win=64256 Len=1448 TSval=2195609407 TSecr=2105469646

Error común. Leer Seq=0 como si fuera el número real. Wireshark y tshark muestran por defecto números relativos al inicio de la conexión, restando el inicial de cada sentido. Es cómodo para leer, pero en el paquete viaja 1661972085. Para ver el real: en Wireshark, Edit → Preferences → Protocols → TCP y desmarquen Relative sequence numbers. tcpdump hace una mezcla que despista más: en el handshake muestra los reales, y después pasa a relativos.

Lo de tcpdump se ve en esta misma captura, con el ACK del tercer paquete:

$ tcpdump -nr captures/clean-a.pcap -c 3
14:52:18.479242 IP 10.210.1.10.33776 > 10.210.2.10.5001: Flags [S], seq 1661972085, win 64240, options [mss 1460,sackOK,TS val 2195609366 ec
14:52:18.520390 IP 10.210.2.10.5001 > 10.210.1.10.33776: Flags [S.], seq 3960613348, ack 1661972086, win 65160, options [mss 1460,sackOK,TS 
14:52:18.520408 IP 10.210.1.10.33776 > 10.210.2.10.5001: Flags [.], ack 1, win 502, options [nop,nop,TS val 2195609407 ecr 2105469646], leng

La ventana y su escala

El campo de ventana tiene 16 bits, así que sin ayuda llega hasta 65535 bytes. En un enlace de 1 Gbit/s con 40 ms de RTT se necesitan 5 MB en vuelo: el campo no alcanza. La opción de escala de ventana dice: “multipliquen por 2^N lo que yo anuncie en esta conexión”.

Fíjense en dos detalles del tcpdump de arriba. El tercer paquete dice win 502, mientras que Wireshark dice Win=64256. Son lo mismo: 502 × 128 = 64256, con la escala 7 que a anunció en su SYN. tcpdump muestra el campo crudo, y Wireshark hace la multiplicación por ustedes si vio el handshake. El otro detalle: la ventana del SYN no se escala nunca, porque la escala recién se está acordando.

Error común. Abrir en Wireshark una captura que empezó a la mitad de la conexión y creer que la ventana es diminuta. Sin el handshake, Wireshark no conoce la escala y muestra el campo crudo.

Una conexión completa

Volvamos a la captura limpia. Son 2282 segmentos, y la forma general es esta:

Animación de la conexión de la captura limpia, sin escala de tiempo, con x = 1661972085 e y = 3960613348. Primero el handshake: SYN con seq=x; SYN, ACK con seq=y y ack=x+1; ACK con seq=x+1 y ack=y+1. Luego los datos: un segmento con seq=x+1 y 1448 bytes, otro con seq=x+1449, y 896 ACK de vuelta. Por último el cierre: FIN con seq=x+2000001; ACK con ack=x+2000002; FIN, ACK con seq=y+1; y ACK con ack=y+2Animación de la conexión de la captura limpia, sin escala de tiempo, con x = 1661972085 e y = 3960613348. Primero el handshake: SYN con seq=x; SYN, ACK con seq=y y ack=x+1; ACK con seq=x+1 y ack=y+1. Luego los datos: un segmento con seq=x+1 y 1448 bytes, otro con seq=x+1449, y 896 ACK de vuelta. Por último el cierre: FIN con seq=x+2000001; ACK con ack=x+2000002; FIN, ACK con seq=y+1; y ACK con ack=y+2
La misma conexión, con sus números. SYN y FIN gastan un número, y por eso el último ACK de b dice x+2000002 después de 2 000 000 bytes.

Y estos son sus últimos paquetes:

 2280   1.003134  10.210.2.10 → 10.210.1.10  TCP 66 5001 → 33776 [ACK] Seq=1 Ack=2000002 Win=1035136 Len=0 TSval=2105470608 TSecr=2195610236
 2281   1.003145  10.210.2.10 → 10.210.1.10  TCP 66 5001 → 33776 [FIN, ACK] Seq=1 Ack=2000002 Win=1035136 Len=0 TSval=2105470608 TSecr=21956
 2282   1.003152  10.210.1.10 → 10.210.2.10  TCP 66 33776 → 5001 [ACK] Seq=2000002 Ack=2 Win=64256 Len=0 TSval=2195610369 TSecr=2105470608

Cuenten: el Ack=2000002 son 2 000 000 bytes, más 1 porque el stream empieza en 1, más 1 del FIN de a. Y el cierre tiene que leerse con cuidado: aquí b confirmó el FIN en el paquete 2280 y envió el suyo en el 2281, un cierre de cuatro segmentos, como en la figura. Pero si el programa de b cierra justo a tiempo, el kernel junta el ACK y el FIN en uno solo, y son tres. Las dos formas aparecen en nuestras corridas, y la captura de la sección de TIME_WAIT tiene la de tres. Las dos son correctas.

En la práctica. ¿Por qué estos escenarios usan una cola de 2000 y no de 200? Porque con 200 nos falló la palabra “limpia”. En dos de cinco corridas, una transferencia de 2 MB desbordó la cola al arrancar, y salieron más de cien retransmisiones (103 y 102) sin ningún netem loss. Una cola más grande que todos los segmentos juntos no se puede desbordar. Esa pérdida no es un defecto del laboratorio: es congestión, y es el tema de la próxima semana.

El gráfico tiempo-secuencia

Una captura de 2000 líneas no se lee línea por línea. La herramienta para eso es el gráfico tiempo-secuencia: en el eje X va el tiempo, en el eje Y el número de secuencia, y cada segmento es un punto. En Wireshark está en Statistics → TCP Stream Graphs → Time Sequence. Nosotros también lo generamos en el contenedor:

$ python3 timeseq.py captures/clean-a.pcap captures/clean-a.png
gráfico en captures/clean-a.png

Ábranlo. Sin pérdidas, se ven tres cosas:

  1. Al comienzo, ráfagas separadas por un RTT. Hasta unos 0,25 s, los puntos forman escalones. Cada ráfaga es más grande que la anterior: es slow start, que estudiaremos la próxima semana. Los primeros escalones están a 40 ms entre sí, un RTT.
  2. Después, una recta. Su pendiente es la velocidad del enlace: 2 MB en cerca de 0,8 s son unos 20 Mbit/s. La pendiente del gráfico es el throughput.
  3. La línea de los ACK corre paralela, a la derecha. La distancia horizontal entre un punto y su ACK es el RTT. Empieza en 40 ms y crece hasta más de 100 ms, porque a envía más rápido de lo que el enlace deja pasar, y se forma una cola en m. Es el mismo retardo de cola de la semana 1, visto desde TCP.
Gráfico tiempo-secuencia con una muestra de los puntos de la captura limpia: 2 MB en cerca de 0,9 s. Los datos forman escalones hasta unos 0,2 s, una ráfaga por RTT, y luego una recta con pendiente de unos 20 Mbit/s. La línea de los ACK corre paralela a la derecha, y la distancia horizontal entre los datos y los ACK, que es el RTT, pasa de 40 ms a más de 100 msGráfico tiempo-secuencia con una muestra de los puntos de la captura limpia: 2 MB en cerca de 0,9 s. Los datos forman escalones hasta unos 0,2 s, una ráfaga por RTT, y luego una recta con pendiente de unos 20 Mbit/s. La línea de los ACK corre paralela a la derecha, y la distancia horizontal entre los datos y los ACK, que es el RTT, pasa de 40 ms a más de 100 ms
Lo que van a ver en Wireshark, con la captura limpia. La pendiente es el throughput y la distancia horizontal a los ACK es el RTT.

En la práctica. Cuando alguien dice que “la red está lenta”, el gráfico tiempo-secuencia separa los casos rápido. Si la curva es una recta sin escalones, TCP está usando lo que hay. Si tiene mesetas, algo lo detiene, y la pregunta es qué: una pérdida, o un receptor que no lee. Las dos cosas se ven en este capítulo.

ACK acumulativos y ACK retardados

TCP usa ACK acumulativo: el número dice hasta dónde está todo completo, y un ACK posterior vuelve innecesarios a los anteriores. Si se pierde un ACK no pasa nada, siempre que llegue otro con un número mayor.

El receptor además no tiene por qué confirmar cada segmento. El RFC 9293 le pide que, si implementa ACK retardado (delayed ACK), genere un ACK al menos cada dos segmentos completos, y que nunca espere más de 0,5 s. En la captura limpia hay 1382 segmentos de datos y 896 ACK de vuelta, y en la parte estable se ve el patrón de uno cada dos. Aquí, desde b, con las columnas tiempo, origen, secuencia, ACK y largo:

0.417020000	10.210.1.10	693593	1	1448
0.417027000	10.210.1.10	695041	1	1448
0.417041000	10.210.2.10	1	696489	0
0.418035000	10.210.1.10	696489	1	1448
0.418042000	10.210.1.10	697937	1	1448
0.418045000	10.210.2.10	1	699385	0
0.419021000	10.210.1.10	699385	1	1448
0.420033000	10.210.1.10	700833	1	1448
0.420042000	10.210.2.10	1	702281	0

Los dos últimos segmentos de datos se confirman juntos: 702281 = 699385 + 2 × 1448. El total no da exactamente uno cada dos porque al comienzo Linux confirma todo de inmediato, para no frenar el arranque de la conexión.

Si llega un solo segmento y nada más, el ACK retardado se nota de verdad. El escenario delack envía 100 bytes cada 100 ms a un receptor que no está leyendo:

1.438350000	10.210.1.10	1301	1	100
1.438362000	10.210.2.10	1	1401	0
1.538742000	10.210.1.10	1401	1	100
1.538761000	10.210.2.10	1	1501	0
1.638989000	10.210.1.10	1501	1	100
1.638997000	10.210.2.10	1	1601	0
1.745987000	10.210.1.10	1601	1	100
1.787576000	10.210.2.10	1	1701	0
1.846984000	10.210.1.10	1701	1	100
1.894997000	10.210.2.10	1	1801	0
1.955985000	10.210.1.10	1801	1	100
2.004004000	10.210.2.10	1	1901	0

Hasta el segmento de Seq=1501, el ACK sale en microsegundos. Desde el de Seq=1601, sale unos 40 a 50 ms después. Esa espera es el ACK retardado. Dos precisiones que salen de lo que medimos:

  • Los 500 ms son un tope, no lo que se usa. Aquí se ven unos 40 ms.
  • Depende de lo que haga la aplicación. Si sink está leyendo, Linux responde en microsegundos a cada segmento chico: cuando probamos ese caso, ningún ACK se retrasó. Por eso el escenario usa un receptor que no lee.

¿Para qué esperar? Porque si la aplicación va a responder pronto, el ACK puede viajar en el mismo segmento que la respuesta (se llama piggybacking), y además se ahorra un paquete de cada dos en una transferencia larga. El costo es que un RTT medido incluye esa espera.

Error común. Creer que un ACK confirma un segmento. Confirma todos los bytes anteriores a ese número. Por eso no hay que contar “un ACK por paquete” al leer una captura, ni asustarse si faltan.

Estimar el RTT y fijar el RTO

Si un segmento se pierde, el emisor se entera porque no llega su ACK. ¿Cuánto debe esperar? Si espera muy poco, retransmite cosas que no se perdieron. Si espera mucho, una pérdida detiene la conexión sin motivo. La semana pasada eso era una constante. En Internet no se puede: el RTT cambia de una conexión a otra, y dentro de una misma conexión. En nuestra captura limpia, sin cambiar nada del enlace, pasa de 41 ms a más de 130 ms.

Definición. TCP mantiene dos promedios móviles exponenciales. Con cada nueva medición, SampleRTT:

DevRTT       = (1 - β) × DevRTT + β × |SampleRTT - EstimatedRTT|
EstimatedRTT = (1 - α) × EstimatedRTT + α × SampleRTT
RTO          = EstimatedRTT + 4 × DevRTT

con α = 1/8 y β = 1/4. El RFC 6298 los llama SRTT y RTTVAR, y agrega tres reglas:

  1. Antes de la primera medición, el RTO es de 1 s.
  2. La primera medición R inicializa todo: EstimatedRTT = R y DevRTT = R / 2.
  3. Si el RTO calculado es menor que 1 s, debería subirse a 1 s.

La intuición: EstimatedRTT sigue el valor típico, y es lento a propósito, para que una medición rara no lo mueva mucho. DevRTT mide cuánto se aparta el RTT de ese valor. El margen de seguridad es grande cuando la red es irregular y chico cuando es estable.

Ejemplo resuelto

Con EstimatedRTT = 100 ms y DevRTT = 5 ms, llega un SampleRTT de 120 ms:

DevRTT       = 0,75 × 5 + 0,25 × |100 - 120| = 3,75 + 5 = 8,75 ms
EstimatedRTT = 0,875 × 100 + 0,125 × 120     = 87,5 + 15 = 102,5 ms
RTO          = 102,5 + 4 × 8,75              = 137,5 ms

rto_table.py hace esta misma cuenta:

$ python3 rto_table.py --start 100 5 120
  muestra  EstimatedRTT   DevRTT      RTO
   120.00        102.50     8.75   137.50

Una muestra 20 % más lenta movió el promedio solo 2,5 ms, pero el RTO pasó de 120 ms a 137,5 ms. El margen reacciona más rápido que el promedio. Ahora, con una lista de muestras en que una es rara:

$ python3 rto_table.py 100 90 120 95 400 100
  muestra  EstimatedRTT   DevRTT      RTO
   100.00        100.00    50.00   300.00
    90.00         98.75    40.00   258.75
   120.00        101.41    35.31   242.66
    95.00        100.61    28.09   212.95
   400.00        138.03    95.91   521.68
   100.00        133.28    81.44   459.05

Error común. Usar el EstimatedRTT nuevo para calcular DevRTT. Da 8,125 y un RTO de 135 ms, y es fácil caer ahí porque muchos textos presentan primero la fórmula de EstimatedRTT. El RFC 6298 es explícito: DevRTT se calcula primero, con el valor anterior. En este curso usamos el orden del RFC.

Con muestras reales

La misma cuenta, sobre mediciones tomadas de la captura limpia (Wireshark empareja cada dato con su ACK; la herramienta toma una de cada tantas):

$ python3 rto_table.py --pcap captures/clean-a.pcap
  muestra  EstimatedRTT   DevRTT      RTO
    41.80         41.80    20.90   125.40
    48.13         42.59    17.26   111.62
    52.13         43.78    15.33   105.09
    74.83         47.66    19.26   124.69
    98.77         54.05    27.22   162.93
   103.03         60.18    32.66   190.81
   106.18         65.93    36.00   209.91
   111.03         71.56    38.27   224.65
   115.01         76.99    39.57   235.26
   117.19         82.02    39.72   240.91
   122.86         87.12    40.00   247.14
   127.80         92.21    40.17   252.90
   131.89         97.17    40.05   257.37

Las muestras suben porque la cola en m se está llenando. Fíjense en que EstimatedRTT va siempre detrás de las muestras, mientras que el RTO queda holgadamente por encima de todas. Esa es la idea. (La herramienta solo hace la cuenta; un emisor real toma muchas más muestras, así que sus valores no serán estos).

Gráfico de las 13 filas de la tabla anterior. Las muestras suben de 41,80 a 131,89 ms. EstimatedRTT va por debajo y termina en 97,17 ms. El RTO arranca en 125,40 ms, baja hasta 105,09 y sube hasta 257,37 ms, siempre por encima de las muestras. La distancia entre EstimatedRTT y el RTO es el margen, 4 × DevRTTGráfico de las 13 filas de la tabla anterior. Las muestras suben de 41,80 a 131,89 ms. EstimatedRTT va por debajo y termina en 97,17 ms. El RTO arranca en 125,40 ms, baja hasta 105,09 y sube hasta 257,37 ms, siempre por encima de las muestras. La distancia entre EstimatedRTT y el RTO es el margen, 4 × DevRTT
La tabla anterior, dibujada. El promedio se queda atrás de un RTT que sube, y el margen compensa esa demora.

Por qué no se mide un segmento retransmitido

Supongan que un segmento se envía, vence el temporizador, se retransmite y llega un ACK. ¿De cuál de los dos envíos es ese ACK?

Dos diagramas de tiempo. En el caso 1 se pierde el envío 1, llega el envío 2 y vuelve un ACK: medir desde el envío 1 da una muestra demasiado grande. En el caso 2 el envío 1 llega, y su ACK viene tarde; el envío 2 sale antes de que ese ACK vuelva, y medir desde el envío 2 da una muestra demasiado chicaDos diagramas de tiempo. En el caso 1 se pierde el envío 1, llega el envío 2 y vuelve un ACK: medir desde el envío 1 da una muestra demasiado grande. En el caso 2 el envío 1 llega, y su ACK viene tarde; el envío 2 sale antes de que ese ACK vuelva, y medir desde el envío 2 da una muestra demasiado chica
El emisor ve lo mismo en los dos casos: un envío, otro envío y un ACK. No tiene cómo saber desde cuál medir.

Si el emisor mide desde el envío 1 y fue el caso 1, la muestra sale demasiado grande. Si mide desde el envío 2 y fue el caso 2, sale demasiado chica. Peor aún: una muestra chica baja el RTO, lo que causa más retransmisiones innecesarias, y así más muestras malas. La regla, llamada algoritmo de Karn, es no tomar muestras de segmentos retransmitidos.

El RFC 6298 admite una excepción: con la opción de timestamps no hay ambigüedad, porque el ACK devuelve en TSecr el valor del envío que lo provocó, y el RFC 7323 dice lo mismo. Sin timestamps, la regla de Karn es obligatoria.

Qué hace Linux

Hagan la cuenta del RFC con lo que muestra ss -tin en una conexión de nuestra red, que indica el RTT como rtt:promedio/desviación:

cubic wscale:7,7 rto:249 rtt:48.23/11.866 mss:1448 pmtu:1500 ... cwnd:10 ...

Según la fórmula, el RTO sería 48,23 + 4 × 11,866 = 95,7 ms, y según la regla del piso, 1 s. Lo que aparece es 249 ms. No coincide ni con una cosa ni con la otra. Mirando varias conexiones se ve un patrón:

rto rtt rto − rtt
249 48,23 200,8
244 43,46 200,5
324 123,18 200,8
306 105,57 200,4

Siempre unos 200 ms sobre el RTT. Linux toma como margen el mayor entre 4 × DevRTT y un piso de 200 ms, en vez de subir todo el RTO a 1 s. Lo podemos comprobar: ese piso se puede cambiar por ruta, con rto_min. Es lo que hace el escenario rtomin:

$ python3 scenarios.py rtomin
    default: rto:244 rtt:43.462/3.019
    500ms: rto:542 rtt:41.792/0.805

Con rto_min 500ms, la diferencia sigue al valor configurado. La regla del RFC es conservadora; Linux prefiere recuperarse antes de una pérdida, a cambio de arriesgar alguna retransmisión de más.

En la práctica. En un examen, y en P3, se aplica la fórmula del RFC 6298. Al leer ss en un Linux real, recuerden que el rto que aparece incluye ese piso. Ninguno de los dos está equivocado: el RFC usa “SHOULD” para el mínimo de 1 s, lo que deja lugar a esta decisión.

El temporizador en acción

Una vez que vence el RTO, TCP retransmite el segmento más antiguo sin confirmar y duplica el RTO antes de armar el temporizador de nuevo. Se llama retroceso exponencial (exponential backoff). Si vence, probablemente la red tiene un problema serio, y lo último que debe hacer TCP es insistir más rápido.

El escenario rto lo aísla. Un flujo lento envía 1000 bytes cada 100 ms y, a la mitad, el router corta los dos sentidos durante unos 3 s, con netem loss 100%. Sin ACK de ningún tipo, TCP solo tiene su reloj. Estas son las retransmisiones que vio la captura en a: tiempo, secuencia y largo.

1.828041000	17001	1000
2.386997000	15001	1000
2.887999000	15001	1000
3.920219000	15001	1000
5.968019000	15001	1000
6.052147000	16001	1000
6.052152000	17001	1000
6.094050000	18001	1000

Lean la columna de la secuencia. La primera línea no es del temporizador: es una sonda sobre lo último enviado, que explicamos en la Sesión B. Luego 15001, el byte más antiguo sin confirmar, sale varias veces, a intervalos de 0,50 s, 1,03 s y 2,05 s: cada uno es el doble del anterior. La última vez sí pasó, porque la red ya había vuelto. Llegó el ACK, y TCP siguió de inmediato con lo demás. Esto mismo, desde ss, antes y durante el corte:

cubic wscale:7,7 rto:249 rtt:48.23/11.866 mss:1448 pmtu:1500 rcvmss:536 advmss:1448 cwnd:10 bytes_sent:15000 bytes_acked:14001 ...
cubic wscale:7,7 rto:992 backoff:2 rtt:47.455/10.45 mss:1448 pmtu:1500 rcvmss:536 advmss:1448 cwnd:1 ssthresh:7 bytes_sent:26000 bytes_retrans:3000 bytes_acked:15001 ...

backoff:2 dice que se duplicó dos veces: 992 ≈ 248 × 4. Y cwnd:1 anticipa la próxima semana: después de un timeout, TCP vuelve a empezar casi de cero. Este es el resumen de los contadores del kernel para ese corte, que no adivinan nada:

TcpRetransSegs=12  TcpExtTCPTimeouts=4  TcpExtTCPLossProbes=1  TcpExtTCPSackRecovery=0  TcpExtTCPRenoRecovery=0

Cuatro veces venció el temporizador, una vez salió una sonda. Y hay un costo oculto: como el temporizador ya está en retroceso, TCP puede quedarse callado un buen rato después de que la red volvió. Una caída de 3 s se puede sentir como bastante más.

Idea clave. Hay dos preguntas distintas. ¿Cuánto esperar la primera vez? Lo responde la estimación del RTT. ¿Y si tampoco esa vez llega nada? Lo responde el retroceso exponencial, que no mira el RTT: solo duplica.

Fast retransmit

Con esto empieza la Sesión B. Esperar el RTO es caro: aquí son más de 200 ms, unas seis veces el RTT. Pero en una transferencia larga, una pérdida da señales mucho antes. Si se pierde un segmento en medio de otros, los siguientes siguen llegando, y por cada uno el receptor manda un ACK con el mismo número. El emisor recibe ACK duplicados.

Para verlo sin azar, el escenario drop hace que m descarte exactamente un paquete: el número 40 de los que van hacia el puerto 5001. Lo hace con esta regla, que vale la pena leer:

m# nft 'add rule ip w10 forward tcp dport 5001 numgen inc mod 100000 == 40 counter drop'

Empecemos por el TCP de los libros: el escenario classic apaga SACK y RACK, dos mejoras que vemos enseguida, con sysctl en cada host.

$ tshark -r captures/classic-a.pcap -Y tcp.analysis.flags
  123   0.172808  10.210.2.10 → 10.210.1.10  TCP 66 [TCP Dup ACK 120#1] 5001 → 37986 [ACK] Seq=1 Ack=55025 Win=99840 Len=0 TS
  125   0.173835  10.210.2.10 → 10.210.1.10  TCP 66 [TCP Dup ACK 120#2] 5001 → 37986 [ACK] Seq=1 Ack=55025 Win=99840 Len=0 TS
  127   0.173840  10.210.2.10 → 10.210.1.10  TCP 66 [TCP Dup ACK 120#3] 5001 → 37986 [ACK] Seq=1 Ack=55025 Win=99840 Len=0 TS
  128   0.173843  10.210.1.10 → 10.210.2.10  TCP 1514 [TCP Fast Retransmission] 37986 → 5001 [ACK] Seq=55025 Ack=1 Win=64256 
  129   0.173844  10.210.2.10 → 10.210.1.10  TCP 66 [TCP Dup ACK 120#4] 5001 → 37986 [ACK] Seq=1 Ack=55025 Win=99840 Len=0 TS
  130   0.173845  10.210.2.10 → 10.210.1.10  TCP 66 [TCP Dup ACK 120#5] 5001 → 37986 [ACK] Seq=1 Ack=55025 Win=99840 Len=0 TS

Definición. Con fast retransmit, al recibir el tercer ACK duplicado, el emisor retransmite el segmento que empieza en ese número, sin esperar el temporizador. Eso está en el RFC 5681.

Aquí pasa exactamente eso: a los 3 microsegundos del tercer duplicado sale la retransmisión de Seq=55025. Con el mismo descarte, en la captura drop de la próxima sección, eso ocurre unos 45 ms después del envío original, mucho menos que los 240 ms de RTO. Los ACK duplicados 4 y 5 siguen llegando porque ya venían en camino.

Por qué tres y no uno

Un ACK duplicado no significa pérdida. Significa que llegó algo fuera de orden. Y eso también ocurre cuando un paquete solo se atrasó, por ejemplo si tomó otro camino. Con un umbral de uno, cualquier pequeño desorden causaría una retransmisión inútil. Con tres, hace falta que tres segmentos posteriores hayan adelantado al que falta, lo que ya es poco probable si solo fue desorden. El tres es un valor empírico, elegido para que el desorden leve no dispare nada, y no sale de ningún teorema.

Eso sí, el valor no alcanza para todo. El escenario reorder no pierde nada: solo agrega 3 ms de jitter, sin tope de velocidad, y los segmentos se adelantan unos a otros. Nada se pierde, así que toda retransmisión está de más:

$ python3 scenarios.py reorder_classic
recibidos 3000000 bytes en 0.52 s (46.3 Mbit/s)
TcpRetransSegs=34  TcpExtTCPTimeouts=0  TcpExtTCPLossProbes=0  TcpExtTCPSackRecovery=0  TcpExtTCPRenoRecovery=35

$ python3 scenarios.py reorder
recibidos 3000000 bytes en 0.34 s (71.0 Mbit/s)
TcpRetransSegs=4  TcpExtTCPTimeouts=0  TcpExtTCPLossProbes=0  TcpExtTCPSackRecovery=1  TcpExtTCPRenoRecovery=0

Y estos son los ACK duplicados de la segunda corrida, la moderna:

ACK duplicados 1595 (el mayor, #89); rachas que llegaron a #1: 162, a #2: 133, a #3: 115
retransmitidos 4 segmentos, 4 de ellos marcados como fast

Es decir, 115 veces hubo tres duplicados seguidos sin ninguna pérdida, y aun así este TCP retransmitió 4 segmentos. El clásico, en el mismo enlace, 34 y con menor velocidad. En otras corridas vimos entre 0 y 4 para el moderno y entre 34 y 40 para el clásico: comparen el orden de magnitud. ¿Qué hace distinto el moderno? Lo vemos a continuación.

Idea clave. Un ACK duplicado es una pista, no una prueba. El umbral de tres es un compromiso entre reaccionar rápido a una pérdida y no confundir desorden con pérdida.

SACK: decir exactamente qué llegó

El ACK acumulativo tiene un límite: solo dice dónde empieza el primer hueco. Si se perdieron dos segmentos, el emisor no sabe nada del segundo hasta arreglar el primero. SACK (selective acknowledgment) agrega una opción a los ACK con los bloques recibidos más allá del hueco. Se acuerda en el handshake (es el SACK_PERM del SYN) y es lo que usa Linux por defecto. Este es el mismo descarte, con el TCP normal. Las columnas son número de duplicado, ACK, y el comienzo y el final del bloque:

$ tshark -r captures/drop-a.pcap -Y tcp.analysis.duplicate_ack -T fields -e tcp.analysis.duplicate_ack_num \
    -e tcp.ack -e tcp.options.sack_le -e tcp.options.sack_re
1	55025	56473	57921
2	55025	56473	59369
3	55025	56473	60817
4	55025	56473	62265

Léanlo así: “espero el byte 55025, pero ya tengo desde el 56473 hasta el 57921”. Falta exactamente un segmento (1448 bytes), y el bloque crece con cada uno que llega. Ahora comparemos las dos capturas, usando el truco de esta red. En a, el segmento sale dos veces:

   57   0.128398  10.210.1.10 → 10.210.2.10  TCP 1514 37978 → 5001 [ACK] Seq=55025 Ack=1 Win=64256 Len=14
  126   0.172518  10.210.1.10 → 10.210.2.10  TCP 1514 [TCP Fast Retransmission] 37978 → 5001 [ACK] Seq=55

Y en b llega una sola vez: la retransmisión, en la posición de la captura en que ya había bastantes otros detrás.

  169   0.178087  10.210.1.10 → 10.210.2.10  TCP 1514 [TCP Fast Retransmission] 37978 → 5001 [ACK] Seq=55

El primer ACK de b después de eso salta de golpe:

  202   0.219291  10.210.2.10 → 10.210.1.10  TCP 66 5001 → 37978 [ACK] Seq=1 Ack=125977 Win=214656 Len=0 TSval

De 55025 a 125977: 70 952 bytes confirmados con un solo ACK. Es el ACK acumulativo haciendo su trabajo, sobre lo que b guardó mientras esperaba. Y los contadores del kernel confirman la historia completa:

$ python3 scenarios.py drop
recibidos 2000000 bytes en 1.48 s (10.8 Mbit/s)
TcpRetransSegs=1  TcpExtTCPTimeouts=0  TcpExtTCPLossProbes=0  TcpExtTCPSackRecovery=1  TcpExtTCPRenoRecovery=0

Un solo segmento retransmitido, cero timeouts. Pero noten la velocidad: cerca de 11 Mbit/s, contra los 17 de la captura limpia, por un paquete perdido en 1383. Lo que costó no fue reenviar 1448 bytes, sino que TCP interpretó la pérdida como congestión y bajó su velocidad. Ese es el tema de la próxima semana.

Animación del espacio de bytes del receptor, sin escala de tiempo, con la captura drop. Falta el segmento 55025. Los ACK duplicados 1, 2 y 3 repiten ack=55025 y el bloque SACK crece: 56473-57921, 56473-59369 y 56473-60817. Al tercero, el emisor hace fast retransmit de Seq=55025. El duplicado 4, con el bloque hasta 62265, ya venía en camino. La retransmisión llena el hueco y un ACK salta a 125977Animación del espacio de bytes del receptor, sin escala de tiempo, con la captura drop. Falta el segmento 55025. Los ACK duplicados 1, 2 y 3 repiten ack=55025 y el bloque SACK crece: 56473-57921, 56473-59369 y 56473-60817. Al tercero, el emisor hace fast retransmit de Seq=55025. El duplicado 4, con el bloque hasta 62265, ya venía en camino. La retransmisión llena el hueco y un ACK salta a 125977
El ACK acumulativo solo dice dónde está el hueco, y SACK dice qué hay después. Un segmento retransmitido destraba 70 952 bytes.

Linux ya no cuenta hasta tres

Hoy, en Linux, el mecanismo por defecto no es contar duplicados. Es RACK (RFC 8985), que usa el tiempo: si ya se confirmó (por SACK) un segmento que salió después de otro, y pasó un pequeño margen, el anterior se da por perdido. Por eso tolera mejor el desorden que vimos arriba, y por eso el sistema reacciona aunque no alcancen a llegar tres duplicados. En nuestra captura con un solo descarte también salió justo tras el tercero, pero no es una garantía: la regla de los tres duplicados sigue siendo lo que hay que entender, y lo que hace el modo classic, pero un Linux real puede retransmitir antes o después.

Cuando no hay quién avise

Fast retransmit necesita que lleguen segmentos después del perdido. ¿Y si se pierde el último? No habrá ACK duplicados, y solo quedaría esperar el RTO. Linux tiene una respuesta: TLP (tail loss probe), la otra mitad del RFC 8985. Si pasan unos dos RTT sin ACK, reenvía lo último que mandó para provocar una respuesta. El escenario tail envía 7000 bytes y descarta el último segmento de datos y también el FIN:

    8   0.041812  10.210.1.10 → 10.210.2.10  TCP 1274 43294 → 5001 [PSH, ACK] Seq=5793 Ack=1 Win=642
    9   0.041816  10.210.1.10 → 10.210.2.10  TCP 66 43294 → 5001 [FIN, ACK] Seq=7001 Ack=1 Win=64256
   10   0.083735  10.210.2.10 → 10.210.1.10  TCP 66 5001 → 43294 [ACK] Seq=1 Ack=1449 Win=68096 Len=
   11   0.084735  10.210.2.10 → 10.210.1.10  TCP 66 5001 → 43294 [ACK] Seq=1 Ack=2897 Win=71040 Len=
   12   0.084738  10.210.2.10 → 10.210.1.10  TCP 66 5001 → 43294 [ACK] Seq=1 Ack=4345 Win=73856 Len=
   13   0.085730  10.210.2.10 → 10.210.1.10  TCP 66 5001 → 43294 [ACK] Seq=1 Ack=5793 Win=76800 Len=
   14   0.177741  10.210.1.10 → 10.210.2.10  TCP 66 [TCP Retransmission] 43294 → 5001 [FIN, ACK] Seq
   15   0.219737  10.210.2.10 → 10.210.1.10  TCP 78 [TCP Dup ACK 13#1] 5001 → 43294 [ACK] Seq=1 Ack=
   16   0.219753  10.210.1.10 → 10.210.2.10  TCP 1274 [TCP Retransmission] 43294 → 5001 [PSH, ACK] S
   17   0.261739  10.210.2.10 → 10.210.1.10  TCP 66 5001 → 43294 [ACK] Seq=1 Ack=7002 Win=79104 Len=
   18   0.261754  10.210.2.10 → 10.210.1.10  TCP 66 5001 → 43294 [FIN, ACK] Seq=1 Ack=7002 Win=79104
   19   0.261762  10.210.1.10 → 10.210.2.10  TCP 66 43294 → 5001 [ACK] Seq=7002 Ack=2 Win=64256 Len=

Entre el paquete 13 y el 14 no llegó nada. A los 92 ms, sin ninguna señal, a reenvía lo último que había enviado: el FIN. Esa es la sonda. Esta vez sí llega, y su ACK (el paquete 15) lleva un bloque SACK que revela el hueco. De inmediato sale el dato que faltaba (16). El kernel lo confirma:

TcpRetransSegs=2  TcpExtTCPTimeouts=0  TcpExtTCPLossProbes=1  TcpExtTCPSackRecovery=1  TcpExtTCPRenoRecovery=0

Es una mejora que no siempre alcanza: en otra corrida del mismo escenario el dato faltante esperó al RTO (unos 0,23 s más), con TcpExtTCPTimeouts=1. Por eso la prueba de esta sección exige la sonda y no pide nada sobre el timeout.

Retransmisiones: lo que dice Wireshark y lo que pasó

Ahora el caso del laboratorio: 2 % de pérdida aleatoria, en el sentido de los datos.

$ python3 scenarios.py loss
recibidos 3000000 bytes en 11.53 s (2.1 Mbit/s)
TcpRetransSegs=43  TcpExtTCPTimeouts=0  TcpExtTCPLossProbes=0  TcpExtTCPSackRecovery=35  TcpExtTCPRenoRecovery=0
$ python3 tcpsum.py captures/loss-a.pcap
datos          2116 segmentos, 3062264 bytes enviados, 3000000 bytes distintos, el mayor de 1448
ACK de vuelta  1142 sin datos, ventana máxima anunciada 609408
ACK duplicados 304 (el mayor, #37); rachas que llegaron a #1: 41, a #2: 40, a #3: 38
retransmitidos 43 segmentos, 24 de ellos marcados como fast

Tres conclusiones:

  • El archivo llegó entero. 3 000 000 bytes distintos, con 62 264 de sobra por retransmitir.
  • La pérdida es de 2 %, pero la velocidad cayó casi 90 %. De unos 18 Mbit/s a unos 2 Mbit/s. La pérdida no cuesta los bytes reenviados, cuesta la reacción de TCP. Las cifras varían bastante de una corrida a otra (de 1,6 a 3,4 Mbit/s en las nuestras).
  • Wireshark marcó 24 como fast y 19 como otra cosa. ¿Esas 19 son timeouts? No: el kernel dice TcpExtTCPTimeouts=0.

Error común. Leer [TCP Retransmission] como “venció el temporizador”. Wireshark está mirando desde afuera y clasifica con reglas: si vio varios duplicados y fue reciente, dice fast; si no, dice retransmission. No sabe por qué el kernel tomó esa decisión. Un segmento que Wireshark marca así puede ser una sonda TLP, una decisión de RACK o un timeout real. En el corte de 3 s del escenario rto también le puso fast a lo que siguió a la recuperación. La fuente confiable son los contadores: nstat -az TcpExtTCPTimeouts.

En las ocho corridas de este escenario en que miramos ese contador, ni una vez venció el RTO. Con SACK, RACK y TLP, 2 % de pérdida aleatoria rara vez deja a TCP sin señales. Para encontrar un timeout real, usen el escenario rto. Lo mismo vale en el laboratorio.

El gráfico muestra una curva sin mesetas largas, con pendiente que cambia, y con cruces rojos donde hubo retransmisiones:

$ python3 timeseq.py captures/loss-a.pcap captures/loss-a.png
gráfico en captures/loss-a.png

Compárenlo con el de la captura limpia: aquella era una recta a 20 Mbit/s que tardaba un segundo, y esta tarda varios. Donde hay retransmisiones, la pendiente baja. Ahí TCP redujo su ventana.

Control de flujo

Hasta aquí, TCP protegía los datos de la red. Ahora lo protegemos de otra cosa: del receptor. Los datos que llegan esperan en un buffer del kernel hasta que la aplicación hace read. Si la aplicación es lenta, ese buffer se llena, y lo que llegue después se descartaría.

Definición. Con control de flujo, cada segmento lleva en el campo de ventana cuántos bytes más puede recibir ese extremo (la ventana de recepción, rwnd). El emisor garantiza que:

último byte enviado - último byte confirmado <= ventana anunciada

Es la ventana deslizante de la semana 9 con una novedad: su tamaño cambia, y lo decide el receptor.

Ventana cero

El escenario zero usa un servidor con un buffer chico que acepta la conexión y se queda 3 s sin leer. Lo ajustamos con SO_RCVBUF, y tiene que hacerse antes de listen, porque la escala de la ventana se decide en el handshake:

$ tshark -r captures/zero-a.pcap -Y "frame.number>=30 and frame.number<=41"
   30   0.212588  10.210.2.10 → 10.210.1.10  TCP 66 5001 → 56426 [ACK] Seq=1 Ack=26065 Win=2896 Len=0 TSval=21
   31   0.212613  10.210.1.10 → 10.210.2.10  TCP 1514 [TCP Window Full] 56426 → 5001 [ACK] Seq=27513 Ack=1 Win
   32   0.254582  10.210.2.10 → 10.210.1.10  TCP 66 [TCP ZeroWindow] 5001 → 56426 [ACK] Seq=1 Ack=28961 Win=0 
   33   0.518604  10.210.1.10 → 10.210.2.10  TCP 66 [TCP Keep-Alive] 56426 → 5001 [ACK] Seq=28960 Ack=1 Win=64
   34   0.560608  10.210.2.10 → 10.210.1.10  TCP 66 [TCP ZeroWindow] 5001 → 56426 [ACK] Seq=1 Ack=28961 Win=0 
   35   1.133607  10.210.1.10 → 10.210.2.10  TCP 66 [TCP Keep-Alive] 56426 → 5001 [ACK] Seq=28960 Ack=1 Win=64
   36   1.175605  10.210.2.10 → 10.210.1.10  TCP 66 [TCP ZeroWindow] 5001 → 56426 [ACK] Seq=1 Ack=28961 Win=0 
   37   2.221656  10.210.1.10 → 10.210.2.10  TCP 66 [TCP Keep-Alive] 56426 → 5001 [ACK] Seq=28960 Ack=1 Win=64
   38   2.263588  10.210.2.10 → 10.210.1.10  TCP 66 [TCP ZeroWindow] 5001 → 56426 [ACK] Seq=1 Ack=28961 Win=0 
   39   3.084589  10.210.2.10 → 10.210.1.10  TCP 66 [TCP Window Update] 5001 → 56426 [ACK] Seq=1 Ack=28961 Win
   40   3.084611  10.210.1.10 → 10.210.2.10  TCP 1514 56426 → 5001 [ACK] Seq=28961 Ack=1 Win=64256 Len=1448 TSval=
   41   3.084612  10.210.1.10 → 10.210.2.10  TCP 1514 56426 → 5001 [PSH, ACK] Seq=30409 Ack=1 Win=64256 Len=14

La historia, paso a paso:

  • Paquete 30. La ventana ya está en 2896 bytes: dos segmentos.
  • Paquete 31. a envía el último que cabe. Wireshark avisa Window Full: con eso se acabó.
  • Paquete 32. b confirma todo, con Win=0. Es un mensaje importante: “lo recibí bien, pero no me mandes más”.
  • Paquetes 33 a 38. a espera, y cada cierto tiempo pregunta. A estas preguntas las llamamos sondas de ventana (zero-window probes). Salen a 0,26 s, 0,62 s y 1,09 s entre sí: el intervalo crece.
  • Paquete 39. Al cumplirse los 3 s, el programa lee, y b avisa por su cuenta con una ventana nueva. Un instante después, a retoma donde quedó.
Animación a escala de la captura zero, con dos líneas horizontales, a y b. La ventana baja a Win=2896, a envía lo último que cabe y a los 0,25 s b confirma con Win=0. Luego a manda sondas a los 0,52, 1,13 y 2,22 s, separadas 0,26, 0,62 y 1,09 s, y b contesta Win=0 a cada una. A los 3,08 s el programa de b lee, b avisa con una ventana nueva, y a retoma en Seq=28961Animación a escala de la captura zero, con dos líneas horizontales, a y b. La ventana baja a Win=2896, a envía lo último que cabe y a los 0,25 s b confirma con Win=0. Luego a manda sondas a los 0,52, 1,13 y 2,22 s, separadas 0,26, 0,62 y 1,09 s, y b contesta Win=0 a cada una. A los 3,08 s el programa de b lee, b avisa con una ventana nueva, y a retoma en Seq=28961
Los tres segundos de la captura, a escala. El emisor no se rinde, pero pregunta cada vez menos seguido.

¿Por qué hacen falta sondas, si b ya avisa cuando se abre la ventana? Porque ese aviso del paquete 39 es un ACK sin datos, y los ACK sin datos no se retransmiten ni se confirman. Si se pierde, a espera un aviso que no llegará, y b espera datos que no vendrán. La conexión quedaría trabada para siempre. Las sondas evitan eso: el emisor pregunta, y mientras reciba respuesta la conexión sigue viva, por todo el tiempo que haga falta (el RFC 9293 lo exige).

Error común. Tomar al pie de la letra las descripciones de la sonda. El RFC 9293 habla de enviar al menos un byte de datos nuevos si hay, o de retransmitir, y muchos textos hablan de “una sonda de un byte”. Lo que capturamos aquí es otra cosa: Len=0 con Seq=28960, uno menos que el siguiente byte. Es un segmento con un número ya confirmado, que obliga al receptor a contestar con su estado actual. Por eso, además, Wireshark lo etiqueta TCP Keep-Alive: tiene el aspecto de un keep-alive. Búsquenlo con ese nombre.

Quién limita: la ventana o el enlace

Lo que transfiere TCP en un RTT no puede pasar de lo que permita la ventana. Por eso:

throughput <= ventana / RTT

El escenario limit lo mide, con un receptor que lee todo lo rápido que puede, pero con buffers de distinto tamaño. El enlace es el de siempre: 20 Mbit/s, con 40 ms de RTT.

SO_RCVBUF Ventana máxima anunciada Ventana / RTT Medido
8192 8 688 bytes 1,7 Mbit/s 1,2 Mbit/s
32768 39 096 bytes 7,8 Mbit/s 6,8 Mbit/s
262144 261 848 bytes 52 Mbit/s 17,9 Mbit/s

Cifras de una corrida; cambian un poco en otras, y la tendencia no. En las dos primeras filas manda la ventana: lo medido anda algo por debajo de ventana / RTT, que es un techo. En la tercera, la ventana alcanzaría para 52 Mbit/s, y por eso quien limita es el enlace. Para llenar un enlace, la ventana debe ser al menos el producto ancho de banda × retardo: aquí, 20 Mbit/s × 40 ms = 100 kB. Es la misma cuenta de la semana 9.

Las capturas de las primeras filas no muestran una pérdida que explique la lentitud: en la corrida de la tabla, 0 segmentos retransmitidos. Ese es el diagnóstico: lento, sin pérdidas, con la ventana chica. No es la red.

Error común. Confundir control de flujo con control de congestión. Los dos limitan al emisor, pero el control de flujo protege al receptor, usa rwnd, y el receptor la anuncia en cada segmento. El control de congestión protege a la red, usa cwnd, y el emisor la deduce, porque la red no le dice nada. El emisor respeta el menor de los dos: min(rwnd, cwnd). En ss, son snd_wnd y cwnd. La próxima semana vemos la segunda.

En la práctica. Si una transferencia lenta sobre un enlace largo no sube de cierta velocidad sin importar el enlace, revisen la ventana. Linux agranda solo el buffer de recepción: la captura limpia llegó a anunciar 1 035 136 bytes, sin que nadie se lo pidiera. Fijar SO_RCVBUF a mano apaga ese ajuste, y lo pagan quienes lo copian de un ejemplo viejo.

Mirar una conexión viva con ss

Todo lo anterior el kernel lo sabe de cada conexión, y ss -tin lo muestra. El escenario ss envía 8 MB y consulta ss cada cierto tiempo. Recortamos los campos menos relevantes:

a# ss -tin dst 10.210.2.10
ESTAB 0      738480   10.210.1.10:50756  10.210.2.10:5001
	 cubic wscale:7,7 rto:324 rtt:123.177/0.523 mss:1448 pmtu:1500 ... cwnd:215 ssthresh:168 bytes_sent:1911360 bytes_acked:1600041 ...
ESTAB 0      944096   10.210.1.10:50756  10.210.2.10:5001
	 cubic wscale:7,7 rto:306 rtt:105.569/0.502 mss:1448 pmtu:1500 ... cwnd:178 ssthresh:171 bytes_sent:3884984 bytes_retrans:17376 ... retrans:0/12 ...
ESTAB 0      871696   10.210.1.10:50756  10.210.2.10:5001
	 cubic wscale:7,7 rto:322 rtt:121.325/0.597 mss:1448 pmtu:1500 ... cwnd:204 ssthresh:171 bytes_sent:5949832 bytes_retrans:17376 ... retrans:0/12 ...
Campo Qué es
Tercera columna (Send-Q) Bytes aceptados por el kernel y aún no confirmados
rtt EstimatedRTT y DevRTT, en ms
rto El temporizador actual, en ms
cwnd Ventana de congestión, en segmentos. El 215 son unos 311 kB
ssthresh Umbral de slow start: próxima semana
retrans:0/12 Retransmitidos: 0 ahora mismo, 12 en total
snd_wnd La ventana que anunció el otro lado (la recortamos en esta salida)

Hagamos dos cuentas. Con 215 segmentos en vuelo y 123 ms de RTT: 215 × 1448 × 8 / 0,123 = 20,2 Mbit/s, igual a lo que da el enlace. Y un RTT de 123 ms, en un camino de 40 ms: los otros 80 ms son cola en m. Esa cola además se llena: los 12 retransmitidos no los causó ningún netem loss, sino un buffer de 200 paquetes rebasado. Es pérdida por congestión, la de la próxima semana.

Otra consecuencia de esto: write devuelve cuando el kernel acepta los bytes en su buffer, no cuando llegan. Hay 738 480 bytes en Send-Q. Por eso sink mide la velocidad en el receptor y no en blast.

El handshake: abrir una conexión

Ya vimos el handshake de tres pasos. Ahora la pregunta interesante: ¿por qué tres?

Cada lado debe saber dos cosas: el número inicial del otro, y que el otro recibió el suyo. Con el número inicial de a y el de b, cada uno confirmado, salen cuatro mensajes: SYN, ACK, SYN, ACK. El segundo y el tercero se pueden juntar en un solo SYN-ACK. Quedan tres.

¿Y por qué no dos? Porque el servidor no puede distinguir un SYN nuevo de uno viejo. Es la razón principal que da el RFC 9293: evitar que SYN duplicados antiguos abran conexiones. Imaginen dos pasos:

  a                                    b
  | -- SYN x (viejo, de hace rato) ->   |  llega tarde
  |                                     |  con dos pasos, b ya abre la conexión
  | <---------- SYN-ACK y, ack x+1 ---  |  y puede esperar datos o enviarlos
  | a no reconoce esa conexión

Con tres pasos, b no considera abierta la conexión hasta que vea un ACK de su número y, que no existía cuando se envió el SYN viejo. Y a, al ver un SYN-ACK que confirma algo que no pidió, responde con RST. El tercer paso prueba que el otro lado está aquí y ahora.

Idea clave. El handshake no es una cortesía. Es lo que convierte “alguien me envió un paquete” en “alguien está ahí, recibe lo que le envío y sabe en qué número voy”.

Por qué los números iniciales no empiezan en cero

Las capturas de arriba abren con 1661972085, 4265168592, 3960613348: cambian en cada conexión. Hay dos motivos:

  1. Confusión con conexiones anteriores. Un segmento retrasado de una conexión anterior, con los mismos cuatro campos, podría caer dentro de la ventana de la nueva. Si todas empezaran en cero, sería muy probable.
  2. Seguridad. Quien pueda adivinar los números de secuencia sin ver el tráfico puede inyectar segmentos falsos en una conexión ajena, o simular una conexión. El número inicial impredecible lo dificulta.

CS168 resume la historia así: originalmente el motivo era el primero, y hoy, en la práctica, es el segundo. El número inicial no es un mecanismo criptográfico: quien pueda capturar el tráfico lo lee. Protege contra quien no puede. Para protegerse de quien puede, está TLS, y lo veremos más adelante.

Qué devuelve connect

Lo que recibe la aplicación depende de lo que le pase al SYN. Las respuestas son fáciles de confundir, y son las que van a ver en sus programas. Usamos connect_probe, con TCP_SYNCNT en 2 para no esperar dos minutos:

$ python3 scenarios.py ports
    closed: connect devolvió -1 en 0.042 s: ECONNREFUSED (Connection refused)
    filtered: connect devolvió -1 en 7.203 s: ETIMEDOUT (Connection timed out)
    filtered_exp: connect devolvió -1 en 7.098 s: ETIMEDOUT (Connection timed out)
    rejected: connect devolvió -1 en 0.021 s: EHOSTUNREACH (No route to host)
    no_route: connect devolvió -1 en 0.020 s: ENETUNREACH (Network is unreachable)
    open: connect devolvió 0 en 0.041 s
Caso Qué pasó Qué ve connect Tiempo
Puerto abierto SYN-ACK 0 Un RTT
Puerto cerrado El kernel de b responde RST, ACK ECONNREFUSED Un RTT
Puerto filtrado (drop) Nadie responde nada ETIMEDOUT Segundos
Filtrado con reject El router manda un ICMP EHOSTUNREACH La mitad del RTT
Sin ruta El router manda un ICMP ENETUNREACH La mitad del RTT

El puerto cerrado, en la captura:

    1   0.000000  10.210.1.10 → 10.210.2.10  TCP 74 55194 → 5999 [SYN] Seq=0 Win=64240 Len=0 MS
    2   0.041334  10.210.2.10 → 10.210.1.10  TCP 54 5999 → 55194 [RST, ACK] Seq=1 Ack=1 Win=0 L

Los dos últimos tardan 20 ms y no 40, porque responde m, a medio camino. Cerrado y filtrado se distinguen fácil: “rechazado al instante” significa que el host existe y su kernel respondió. “Se queda colgado” significa que algo se tragó el paquete, y puede ser el firewall, o que el host está apagado.

Un resultado curioso salió aquí. Esperábamos reintentos a 1, 2 y 4 s, y el total de 7 s parecía confirmarlo. Pero veamos los tiempos de los SYN en la captura:

filtered (por defecto)        filtered_exp (tcp_syn_linear_timeouts=0)
0.000000000                   0.000000000
1.059143000                   1.018746000
2.083651000                   3.067214000
3.107122000
4.131391000
5.155128000

Con la configuración por defecto de este kernel, los primeros reintentos van cada 1 s, sin duplicarse, por el valor net.ipv4.tcp_syn_linear_timeouts=4, y recién luego empieza el retroceso. Si lo ponemos en 0, aparece la duplicación del libro: 1 s, 2 s. La primera espera, de 1 s, sí es la del RFC 6298 en los dos casos.

Lo otro que sorprende es que con TCP_SYNCNT en 2 salieron seis SYN, y no tres, y que el tiempo total no cambió. Medimos más valores, con la configuración por defecto:

TCP_SYNCNT Tiempos de los SYN (s) connect se rinde a los
1 0, 1, 2 3,1 s
2 0, 1, 2, 3, 4, 5 7,2 s
3 0, 1, 2, 3, 4, 5, 7,2, 11,2 19,4 s

Nuestra lectura de estos datos: en este kernel, el valor no funciona como un número de SYN sino como un plazo, el que tomarían n reintentos si cada uno duplicara la espera: 3 s, 7 s y 15 s. Y ese plazo se revisa solo cuando vence el temporizador: con 3, el temporizador venció a los 11,2 s, todavía dentro del plazo, y luego a los 19,4 s, ya fuera. Muchos textos hablan de “cantidad de reintentos”, y aquí no es eso. Otra razón para comprobar, y no suponer.

El cierre: FIN, conexiones a medio cerrar, TIME_WAIT y RST

Como TCP es full duplex, cerrar es cosa de dos: cada sentido se cierra por separado con su propio FIN. El FIN dice “no voy a enviar más”, no “no quiero recibir más”.

A medio cerrar

Eso permite una conexión a medio cerrar (half-close). Un cliente envía todo su pedido y hace shutdown(fd, SHUT_WR). Ahora el servidor ve el fin del archivo en read, y todavía puede contestar. Es útil cuando el servidor necesita saber que el pedido terminó. El escenario halfclose lo hace:

    7   0.055415  10.210.1.10 → 10.210.2.10  TCP 66 48000 → 5001 [FIN, ACK] Seq=3001 Ack=1
    8   0.097507  10.210.2.10 → 10.210.1.10  TCP 66 5001 → 48000 [ACK] Seq=1 Ack=1449 Win=
    9   0.097514  10.210.2.10 → 10.210.1.10  TCP 66 5001 → 48000 [ACK] Seq=1 Ack=2897 Win=
   10   0.097516  10.210.2.10 → 10.210.1.10  TCP 66 5001 → 48000 [ACK] Seq=1 Ack=3001 Win=
   11   0.098352  10.210.2.10 → 10.210.1.10  TCP 77 5001 → 48000 [PSH, ACK] Seq=1 Ack=3002
   12   0.098372  10.210.1.10 → 10.210.2.10  TCP 66 48000 → 5001 [ACK] Seq=3002 Ack=12 Win
   13   0.098373  10.210.2.10 → 10.210.1.10  TCP 66 5001 → 48000 [FIN, ACK] Seq=12 Ack=300
   14   0.098375  10.210.1.10 → 10.210.2.10  TCP 66 48000 → 5001 [ACK] Seq=3002 Ack=13 Win

El cliente envió su FIN en el paquete 7. En el 11, después, el servidor le manda 11 bytes (3000 bytes\n), y el cliente los confirma en el 12. Luego vienen el FIN del servidor y su ACK. Aquí el cierre tiene otra vez cuatro pasos: FIN (7), ACK (10), FIN (13), ACK (14).

TIME_WAIT

El lado que cierra primero no libera la conexión al terminar. Se queda en el estado TIME_WAIT. El RFC 9293 pide esperar 2 MSL, donde MSL (maximum segment lifetime) es el tiempo máximo que un segmento puede andar por la red, fijado en 2 minutos: cuatro minutos en total. Linux usa 60 s fijos. Se ve en ss:

$ ss -tano state time-wait sport = :5003
Recv-Q Send-Q Local Address:Port Peer Address:Port 
0      0        10.210.2.10:5003  10.210.1.10:38724 timer:(timewait,59sec,0)

Existe por dos motivos:

  1. Para poder repetir el último ACK. Si se pierde, el otro lado va a retransmitir su FIN, y alguien tiene que contestar. Sin TIME_WAIT, la conexión ya no existiría, y el FIN recibiría un RST: el otro lado vería un error en una conexión que terminó bien.
  2. Para que los segmentos viejos se mueran. Mientras dure, no se puede crear otra conexión con esos mismos cuatro campos, así que un segmento atrasado no cae dentro de la siguiente.

El primer motivo lo podemos provocar. El escenario timewait hace que m descarte el último ACK:

    1   0.000000  10.210.1.10 → 10.210.2.10  TCP 74 54188 → 5001 [SYN] Seq=0 Win=64240 Len=0 MS
    2   0.046466  10.210.2.10 → 10.210.1.10  TCP 74 5001 → 54188 [SYN, ACK] Seq=0 Ack=1 Win=651
    3   0.046496  10.210.1.10 → 10.210.2.10  TCP 66 54188 → 5001 [ACK] Seq=1 Ack=1 Win=64256 Le
    4   0.046540  10.210.1.10 → 10.210.2.10  TCP 66 54188 → 5001 [FIN, ACK] Seq=1 Ack=1 Win=642
    5   0.090426  10.210.2.10 → 10.210.1.10  TCP 66 5001 → 54188 [FIN, ACK] Seq=1 Ack=2 Win=652
    6   0.090445  10.210.1.10 → 10.210.2.10  TCP 66 54188 → 5001 [ACK] Seq=2 Ack=2 Win=64256 Le
    7   0.367620  10.210.2.10 → 10.210.1.10  TCP 66 [TCP Retransmission] 5001 → 54188 [FIN, ACK
    8   0.367712  10.210.1.10 → 10.210.2.10  TCP 66 [TCP Dup ACK 6#1] 54188 → 5001 [ACK] Seq=2 

El paquete 6 nunca llegó a b. Unos 277 ms después (un RTO de Linux, más o menos), b repite su FIN (paquete 7). Del lado de a ya no hay ningún programa: blast terminó hace rato. Quien contesta el paquete 8 es el estado que TIME_WAIT conservó en el kernel. Sin eso, ahí habría un RST.

El costo de TIME_WAIT

Lo tiene quien cierra primero, y normalmente es el cliente, que no lo nota. Si es el servidor, aparece un error famoso. El escenario bindfail lo reproduce:

$ python3 scenarios.py bindfail
  b# /w10/sink 5003 --close-first --no-reuseaddr &
  a# /w10/connect_probe 10.210.2.10 5003 2 300
  b# ss -tano state time-wait sport = :5003
Recv-Q Send-Q Local Address:Port Peer Address:Port 
0      0        10.210.2.10:5003  10.210.1.10:38724 timer:(timewait,59sec,0)
  b# timeout 1 /w10/sink 5003 --no-reuseaddr
    sin SO_REUSEADDR: bind: Address already in use
  b# /w10/sink 5004 --close-first &
  a# /w10/connect_probe 10.210.2.10 5004 2 300
  b# ss -tano state time-wait sport = :5004
Recv-Q Send-Q Local Address:Port Peer Address:Port 
0      0        10.210.2.10:5004  10.210.1.10:48858 timer:(timewait,59sec,0)
  b# timeout 1 /w10/sink 5004 
    con SO_REUSEADDR: bind funciona

Esto explica por qué todo servidor del curso pone SO_REUSEADDR, y por qué sin él, reiniciar un servidor falla durante un minuto. Hay un detalle poco conocido: la opción debe estar en los dos sockets, el que dejó el TIME_WAIT y el nuevo. Lo probamos a mano: si solo la pone el nuevo, también da Address already in use. Por eso el escenario usa dos puertos, uno por ronda.

Error común. Creer que TIME_WAIT es un error, o una conexión que quedó mal, porque aparecen muchas en ss. Es una conexión que terminó bien, en su último estado normal. Y no ocupa al servidor: un TIME_WAIT no impide atender a otros clientes en ese puerto.

RST: abortar

RST no es una forma de cerrar sino de abortar: dice “esta conexión no existe” o “no sigo”. No se confirma, y el otro lado se entera con un error en su siguiente read. Ya vimos uno, en respuesta a un puerto cerrado. El otro caso es un programa que aborta: con SO_LINGER en cero, close manda RST y tira lo que no se haya enviado. El escenario abort lo hace:

    4   0.041581  10.210.1.10 → 10.210.2.10  TCP 1514 56884 → 5001 [ACK] Seq=1 Ack=1 Win=6
    5   0.041581  10.210.1.10 → 10.210.2.10  TCP 1514 56884 → 5001 [PSH, ACK] Seq=1449 Ack
    6   0.041585  10.210.1.10 → 10.210.2.10  TCP 170 56884 → 5001 [PSH, ACK] Seq=2897 Ack=
    7   0.041594  10.210.1.10 → 10.210.2.10  TCP 66 56884 → 5001 [RST, ACK] Seq=3001 Ack=1
    8   0.083510  10.210.2.10 → 10.210.1.10  TCP 66 5001 → 56884 [ACK] Seq=1 Ack=1449 Win=
    9   0.083525  10.210.1.10 → 10.210.2.10  TCP 54 56884 → 5001 [RST] Seq=1449 Win=0 Len=

Del lado del servidor, read falló con Connection reset by peer. Los ACK de b estaban en camino cuando llegó el RST, y a, que ya no conoce esa conexión, contesta cada uno con otro RST: paquetes 8 y 9. Cualquier segmento que llegue a una conexión que no existe recibe esta respuesta. También por eso un tercero que logre falsificar un RST válido puede cortar una conexión ajena, como menciona CS168: es una forma real de censura.

SYN flood y SYN cookies

Abrir una conexión le cuesta memoria al servidor antes de saber si el cliente es real. Al recibir un SYN, guarda una entrada en la cola de SYN (estado SYN_RECV), responde SYN-ACK y espera el ACK. Esa cola es finita.

Definición. En un SYN flood, el atacante envía muchos SYN y nunca completa el handshake, normalmente con direcciones de origen falsas, para que las respuestas se pierdan. La cola se llena, y los clientes legítimos no pueden entrar. Enviar un SYN cuesta muy poco, y por eso el ataque es barato. Es un ataque de denegación de servicio descrito en el RFC 4987, junto con las defensas.

En nuestra red lo vemos sin falsificar nada, y solo entre nuestros nodos. Un servidor con backlog 16 recibe 100 SYN de half_open desde la segunda dirección de a, y m descarta los SYN-ACK que vuelven a esa dirección. Luego un cliente legítimo, desde la otra dirección, intenta entrar. Primero, con SYN cookies apagadas:

$ python3 scenarios.py syn
    cookies off: 17 en SYN-RECV, connect devolvió -1 en 7.154 s: ETIMEDOUT (Connection timed out), {'TcpExtTCPReqQFullDrop': 587, 'TcpExtSyncookiesSent': 0, 'TcpExtSyncookiesRecv': 0}
    cookies on: 17 en SYN-RECV, connect devolvió 0 en 0.041 s, {'TcpExtTCPReqQFullDrop': 0, 'TcpExtSyncookiesSent': 250, 'TcpExtSyncookiesRecv': 1}

La primera línea es el ataque funcionando. La cola quedó con 17 entradas, casi el valor de backlog. Y se descartaron 587 SYN, contando los reintentos, entre ellos los del cliente legítimo, que se quedó sin conexión. La segunda es la misma situación con la defensa puesta: el cliente entra en un RTT.

Definición. Con SYN cookies, cuando la cola está llena, el servidor deja de guardar estado. Codifica lo necesario en el número de secuencia del SYN-ACK: un hash de los cuatro campos de la conexión y de un secreto, un contador de tiempo y el MSS. El cliente legítimo responde con ese número más uno, y solo entonces el servidor reconstruye el estado, tras comprobar el hash. El atacante, con direcciones falsas, nunca recibió el SYN-ACK y no puede responder con ese número.

Es un buen truco, porque no cambia nada en el cliente, y porque aprovecha justo lo que hace al número inicial una buena idea: se ve como un número cualquiera. La línea lo confirma: TcpExtSyncookiesSent=250 y TcpExtSyncookiesRecv=1, el cliente legítimo. El costo es que no hay dónde guardar todo lo que trae el SYN: el RFC 4987 advierte que los esquemas comunes son incompatibles con algunas opciones de TCP. Por eso se activa solo cuando la cola se llena. En Linux es lo que hace el valor por defecto tcp_syncookies=1.

En la práctica. Un SYN flood real llega con millones de paquetes por segundo. Las SYN cookies protegen la memoria del servidor, pero no el ancho de banda de su enlace, y para eso hay que filtrar antes de llegar. Esto lo veremos en la parte de seguridad.

El laboratorio

Trabajen en code/week-10/, dentro del contenedor del curso. Empiecen por make up. Para abrir una captura en Wireshark, abran el archivo de captures/ desde su computadora: la carpeta es la misma. Algunas actividades se basan en el laboratorio de TCP de Kurose y Ross.

  1. Sin pérdidas. python3 scenarios.py clean. Abran captures/clean-a.pcap y encuentren el handshake, el cierre y la opción MSS. Busquen los números reales, y calculen a mano un par de ACK.

  2. Tiempo-secuencia. En Wireshark, seleccionen un segmento de datos y vayan a Statistics → TCP Stream Graphs → Time Sequence. O bien corran python3 timeseq.py. Expliquen la forma: escalones, recta y distancia a los ACK. ¿Cuánto da la pendiente?

  3. 2 % de pérdida. python3 scenarios.py loss. Encuentren ACK duplicados y un fast retransmit. Filtros útiles: tcp.analysis.duplicate_ack y tcp.analysis.fast_retransmission. Un timeout real no va a aparecer aquí, y conviene comprobarlo: revisen TcpExtTCPTimeouts. Para verlo, corran python3 scenarios.py rto y midan los intervalos.

  4. Receptor lento. Completen lab/slow_reader.c, que lee de a N bytes con una pausa después de cada lectura. Compilen con make. Corran python3 scenarios.py slow, y encuentren en la captura la ventana cero y las sondas: filtros tcp.analysis.zero_window y tcp.analysis.keep_alive. Calculen cuánto debería tardar y compárenlo con lo medido. Con la configuración de ese escenario (8192 2048 100), su resumen se debería parecer a este:

    SYN-ACK        seq real 4113039008; MSS 1460, ventana 7240, escala 0, SACK sí, timestamps sí
    datos          70 segmentos, 100000 bytes enviados, 100000 bytes distintos, el mayor de 1448
    ACK de vuelta  56 sin datos, ventana máxima anunciada 7240
    ventana cero   12 avisos, 6 sondas
    duración       5.655 s
  5. Puerto cerrado y puerto filtrado. python3 scenarios.py ports. ¿Qué devuelve connect en cada caso, y cuánto tarda? Hagan la tabla por su cuenta y comparen con la de este capítulo. Miren captures/filtered-a.pcap.

  6. ss -tin. python3 scenarios.py ss, y después por su cuenta: dejen correr una transferencia larga y consulten con docker exec clab-w10-tcp-a ss -tin. Ubiquen rtt, rto y cwnd, y comprueben si rto − rtt da unos 200 ms.

  7. P3. Sigan con rimay en projects/p3-rimay-confiable. Lo de hoy les sirve para saber qué mirar en sus capturas. No se lleva hecho nada: rto_table.py no es un emisor, y su protocolo va sobre UDP, con su propio formato. Cuando duden de su RTO, compárenlo con la fórmula del RFC 6298, no con lo que muestra ss, porque ya vimos que Linux la cambia.

make test

cd code/week-10
make test

make test compila, levanta la topología, corre los escenarios y destruye todo al final, incluso si algo falla. Tarda unos tres minutos, más si la computadora está ocupada. Lo que comprueba son las afirmaciones de este capítulo, sobre capturas reales: que seq y ack cuentan bytes, que SYN y FIN gastan un número, que 1448 = 1460 − 12, que el segmento descartado sale dos veces en a y llega una en b, que el intervalo del RTO se duplica, que rto − rtt sigue a rto_min, que el emisor nunca pasa de la ventana, y varias más. Las mediciones se revisan con márgenes amplios, porque dependen de la máquina.

La prueba de slow_reader se salta mientras lab/slow_reader.c tenga un TODO. Una vez que lo completen, comprueba que los bytes lleguen completos, que el programa tarde lo que debe, que SO_RCVBUF se haya puesto antes de listen y que aparezca la ventana cero.

Resumen

  • TCP es un stream de bytes confiable, ordenado, con conexión y full duplex. Numera bytes. El estado vive en los hosts.
  • El MSS no incluye encabezados, cada extremo anuncia el suyo, y en Linux con timestamps los segmentos llevan 1448 bytes y no 1460.
  • seq es el primer byte que lleva el segmento, ack es el siguiente byte que se espera. SYN y FIN gastan un número. Wireshark muestra números relativos.
  • El ACK es acumulativo, y suele ir retardado: uno cada dos segmentos, o a los 40 ms aprox. en Linux, con tope de 0,5 s.
  • RTO = EstimatedRTT + 4 × DevRTT, con α = 1/8 y β = 1/4, calculando DevRTT primero. Linux usa un piso de 200 ms sobre el RTT en lugar del mínimo de 1 s del RFC. Los segmentos retransmitidos no se miden, salvo con timestamps.
  • Al vencer el RTO se retransmite lo más antiguo y se duplica el RTO.
  • Fast retransmit actúa con tres ACK duplicados. Tres, porque el desorden también causa duplicados. SACK dice qué llegó exactamente, RACK decide por tiempo, y TLP hace probing cuando no hay quién avise.
  • Las etiquetas de Wireshark son deducciones. Los contadores del kernel (nstat) dicen la verdad.
  • El control de flujo protege al receptor. La ventana cero detiene al emisor, y las sondas evitan que la conexión se trabe. Throughput ≤ ventana / RTT.
  • El control de flujo protege al receptor, y el de congestión, a la red. El emisor respeta min(rwnd, cwnd).
  • El handshake tiene tres pasos para descartar SYN viejos y confirmar los dos números iniciales, que no son predecibles.
  • Puerto cerrado: ECONNREFUSED, de inmediato. Filtrado: ETIMEDOUT, tras varios segundos.
  • Cada sentido se cierra por separado. Quien cierra primero se queda en TIME_WAIT (60 s en Linux), para repetir el último ACK y dejar que mueran los segmentos viejos.
  • Un SYN flood llena la cola de SYN. Las SYN cookies guardan el estado en el número de secuencia.

Ejercicios

  1. Un segmento lleva seq=1000 y 500 bytes. ¿Qué ACK espera el emisor? Si antes se perdió el segmento que empieza en 500 y este llegó bien, ¿qué ACK manda el receptor? ¿Y con SACK, qué agrega?
  2. Con EstimatedRTT = 100 ms y DevRTT = 5 ms, llegan tres muestras seguidas de 100 ms. Calculen a mano los valores y el RTO después de cada una. ¿A qué valor tiende el RTO si todas las muestras son iguales? Comprueben con rto_table.py --start. ¿Por qué un RTO calculado así es peligroso sin un piso?
  3. Una captura muestra un SYN con Seq=0, un SYN-ACK con Seq=0, Ack=1 y un ACK con Seq=1, Ack=1. Un compañero dice que TCP siempre empieza en cero, y que eso es inseguro. ¿Qué le responden? ¿Qué harían para comprobarlo en esa misma captura?
  4. Un host con MTU de 9000 abre una conexión con otro de MTU de 1500. ¿Qué MSS anuncia cada uno, sin timestamps? ¿De qué tamaño serán los datos en cada sentido? ¿Cambia con timestamps? Comprueben su razonamiento en la red, con ip link set y el escenario mss como guía.
  5. Con drop, cambien el número de paquete descartado (de 40 a 1000, por ejemplo). Antes de correrlo, predigan si habrá más o menos ACK duplicados y por qué. Comprueben con tcpsum.py.
  6. Modifiquen el escenario tail para que descarte solo el último segmento de datos (posición 6) y no el FIN. Predigan qué señal le llega a a antes de la sonda, y qué se va a retransmitir. Comprueben con TcpExtTCPLossProbes.
  7. Se transfiere sobre un enlace de 1 Gbit/s con 100 ms de RTT, y el receptor anuncia una ventana de 64 KB sin escala. ¿Cuál es el throughput máximo? ¿Qué ventana se necesita para llenar el enlace? ¿Cuál es la menor escala de ventana que permite anunciarla? En una captura, ¿cómo distinguirían este caso de un enlace saturado?
  8. Con slow_reader, varíen bytes_por_lectura y pausa_ms (cambien el escenario slow). Predigan el throughput a partir de esos dos valores y comprueben. ¿De qué depende que aparezca la ventana cero? ¿Con qué valores desaparece?
  9. Dibujen un diagrama de tiempo de un cierre de cuatro segmentos en que se pierde el primer FIN, y otro en que se pierde el segundo. ¿Quién retransmite en cada caso? ¿Quién termina en TIME_WAIT? Luego modifiquen la regla de timewait para descartar el FIN de a (posición 2) y comparen con su diagrama.
  10. Un servidor web muestra miles de conexiones en TIME_WAIT, y un compañero propone bajar ese tiempo a 1 s. ¿Qué podría fallar? ¿Por qué aparecen en el servidor y no en los clientes? ¿Qué cambio en quién cierra primero las movería de lugar?
  11. El router m hace reject with tcp reset en vez de reject with icmp. Predigan qué devuelve connect. ¿Se puede distinguir, desde el cliente, entre este caso y un puerto cerrado? ¿Y si miran el TTL en la captura? Compruébenlo.
  12. (Desafío) Las SYN cookies guardan el estado en los pocos bits del número de secuencia. ¿Qué le puede faltar a una conexión establecida así, si el SYN pidió opciones como la escala de ventana? Lean la sección 3.6 del RFC 4987 y discutan por qué las cookies se activan solo cuando la cola se llena.