Semana 10 — TCP
Objetivos
- Leer un segmento TCP y seguir una conexión completa en una captura.
- Explicar cómo TCP estima el RTT y fija el temporizador de retransmisión.
- Distinguir control de flujo de control de congestión.
- Explicar fast retransmit y el cierre de una conexión.
Sesión A — TCP: segmentos, conexiones y retransmisión
Lecturas:
- Kurose y Ross, secs. 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).
- CS168, TCP Design y TCP Implementation.
Lecturas complementarias:
Temas:
- TCP como stream de bytes confiable, con conexión y full duplex. MSS y MTU.
- Campos del encabezado: secuencia y ACK cuentan bytes, no paquetes.
- Error común: leer los números que muestra Wireshark como reales. Por defecto son relativos al inicio de la conexión.
- Estimación del RTT: SampleRTT, EstimatedRTT, DevRTT y RTO. Ejemplo numérico.
- Por qué no se mide el RTT de un segmento retransmitido.
- ACK acumulativos y ACK retardado.
Sesión B — TCP: control de flujo y recuperación rápida
Lecturas:
- Kurose y Ross, secs. 3.5.4 (Reliable Data Transfer), 3.5.5 (Flow Control) y 3.5.6 (TCP Connection Management).
- CS168, TCP Implementation.
Temas:
- Fast retransmit: tres ACK duplicados. Por qué tres y no uno. Linux detecta pérdidas por defecto con RACK, no solo contando ACK duplicados.
- SACK: decir exactamente qué llegó.
- Control de flujo: la ventana de recepción protege al receptor. Ventana cero.
- Establecimiento en tres pasos: por qué dos no alcanzan, y números de secuencia iniciales.
- Cierre y estado
TIME_WAIT: por qué existe. Conexiones a medio cerrar.RST. - SYN flood y SYN cookies.
- Error común: confundir control de flujo con control de congestión. El primero protege al receptor; el segundo, a la red.
Laboratorio — Leer TCP en capturas, sobre un enlace con pérdidas
Trabajen en code/week-10/, con dos hosts unidos por un router m y un enlace configurado con tc netem. El router permite capturar en el emisor los segmentos que después se pierden.
- Capturar una transferencia sin pérdidas. Encontrar el handshake, el cierre y la opción MSS.
- En Wireshark, usar Statistics → TCP Stream Graphs → Time Sequence y explicar la forma de la curva.
- Agregar 2 % de pérdida y repetir. Encontrar ACK duplicados y fast retransmit. Para ver un timeout, corten el enlace por completo (escenario
rto). - Hacer un receptor lento en C que lea poco a poco. Encontrar la ventana cero en la captura.
- Probar a abrir conexiones hacia un puerto cerrado y hacia un puerto filtrado. ¿Qué devuelve
connecten cada caso? - Mirar con
ss -tinlos valores dertt,rtoycwndde una conexión activa. - Avanzar con P3.
Algunas actividades se basan en el laboratorio de TCP de Kurose y Ross.
Preguntas de repaso
- Un segmento lleva
seq=1000y 500 bytes. ¿Qué ACK espera el emisor? ¿Y si antes se perdió el segmento anterior? - Con EstimatedRTT = 100 ms, DevRTT = 5 ms y un SampleRTT de 120 ms, calculen los nuevos valores y el RTO, con α = 0,125 y β = 0,25. Calculen DevRTT primero, como el RFC 6298.
- ¿Por qué el handshake usa números de secuencia iniciales que no son cero?
- ¿Qué hace el emisor cuando la ventana anunciada es cero y qué evita que la conexión se trabe para siempre?
- ¿Por qué el lado que cierra primero espera en
TIME_WAIT?
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (11)Pruebas (3)Laboratorio (1)Compilación (1)
Ejemplos
Los programas que usan los apuntes y la clase.
// blast: abre una conexión TCP, envía bytes y la cierra.
//
// blast <ip> <puerto> <bytes> [--chunk N] [--gap-ms N] [--nodelay] [--reply] [--abort]
//
// Con --chunk y --gap-ms, envía de a N bytes con una pausa entre cada write. Con --reply,
// cierra solo su sentido con shutdown y se queda leyendo lo que conteste el servidor. Con
// --abort, cierra con SO_LINGER en cero: el kernel envía RST en vez de FIN.
#define _POSIX_C_SOURCE 200809L
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>
#define BUF_SIZE 65536
static void pause_ms(long ms) {
struct timespec ts = {ms / 1000, (ms % 1000) * 1000000L};
while (nanosleep(&ts, &ts) < 0) {
if (errno != EINTR) {
perror("nanosleep");
exit(1);
}
}
}
// Lo que el servidor conteste después de nuestro FIN: el otro sentido sigue abierto.
static void read_reply(int fd) {
char reply[256];
ssize_t got;
while ((got = read(fd, reply, sizeof reply - 1)) > 0) {
reply[got] = '\0';
printf("el servidor contesta: %s", reply);
}
if (got < 0) {
perror("read");
exit(1);
}
}
int main(int argc, char **argv) {
if (argc < 4) {
fprintf(stderr, "uso: %s <ip> <puerto> <bytes> [--chunk N] [--gap-ms N] [--nodelay] [--reply] [--abort]\n",
argv[0]);
return 2;
}
long long total = atoll(argv[3]);
size_t chunk = BUF_SIZE;
long gap_ms = 0;
int nodelay = 0, want_reply = 0, abort_close = 0;
for (int i = 4; i < argc; i++) {
if (strcmp(argv[i], "--chunk") == 0 && i + 1 < argc) {
chunk = (size_t)atol(argv[++i]);
} else if (strcmp(argv[i], "--gap-ms") == 0 && i + 1 < argc) {
gap_ms = atol(argv[++i]);
} else if (strcmp(argv[i], "--nodelay") == 0) {
nodelay = 1;
} else if (strcmp(argv[i], "--reply") == 0) {
want_reply = 1;
} else if (strcmp(argv[i], "--abort") == 0) {
abort_close = 1;
} else {
fprintf(stderr, "opción desconocida: %s\n", argv[i]);
return 2;
}
}
if (chunk == 0 || chunk > BUF_SIZE) {
fprintf(stderr, "--chunk debe estar entre 1 y %d\n", BUF_SIZE);
return 2;
}
struct sockaddr_in addr = {.sin_family = AF_INET, .sin_port = htons((uint16_t)atoi(argv[2]))};
if (inet_pton(AF_INET, argv[1], &addr.sin_addr) != 1) {
fprintf(stderr, "dirección inválida: %s\n", argv[1]);
return 2;
}
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
perror("socket");
return 1;
}
if (nodelay) {
int one = 1;
if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof one) < 0) {
perror("setsockopt TCP_NODELAY");
return 1;
}
}
if (connect(fd, (struct sockaddr *)&addr, sizeof addr) < 0) {
perror("connect");
return 1;
}
static char buf[BUF_SIZE];
memset(buf, 'x', sizeof buf);
long long sent = 0;
while (sent < total) {
size_t want = total - sent < (long long)chunk ? (size_t)(total - sent) : chunk;
ssize_t done = write(fd, buf, want);
if (done < 0) {
if (errno == EINTR) {
continue;
}
perror("write");
return 1;
}
sent += done;
if (gap_ms > 0 && sent < total) {
pause_ms(gap_ms);
}
}
if (want_reply) {
if (shutdown(fd, SHUT_WR) < 0) {
perror("shutdown");
return 1;
}
read_reply(fd);
}
if (abort_close) {
struct linger hard = {.l_onoff = 1, .l_linger = 0};
if (setsockopt(fd, SOL_SOCKET, SO_LINGER, &hard, sizeof hard) < 0) {
perror("setsockopt SO_LINGER");
return 1;
}
}
if (close(fd) < 0) {
perror("close");
return 1;
}
printf("enviados %lld bytes\n", sent);
return 0;
}// connect_probe: intenta una conexión y dice qué devolvió connect y cuánto demoró.
//
// connect_probe <ip> <puerto> [reintentos_de_syn [espera_ms]]
//
// El tercer argumento fija TCP_SYNCNT, para no esperar los dos minutos que Linux tarda por
// defecto en rendirse cuando nadie contesta. El cuarto mantiene abierta la conexión un rato
// antes de cerrarla, para que el otro extremo pueda cerrar primero.
#define _POSIX_C_SOURCE 200809L
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>
static double now_seconds(void) {
struct timespec ts;
if (clock_gettime(CLOCK_MONOTONIC, &ts) < 0) {
perror("clock_gettime");
exit(1);
}
return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
}
static const char *errno_name(int number) {
switch (number) {
case ECONNREFUSED: return "ECONNREFUSED";
case ETIMEDOUT: return "ETIMEDOUT";
case EHOSTUNREACH: return "EHOSTUNREACH";
case ENETUNREACH: return "ENETUNREACH";
default: return "otro";
}
}
int main(int argc, char **argv) {
if (argc < 3) {
fprintf(stderr, "uso: %s <ip> <puerto> [reintentos_de_syn [espera_ms]]\n", argv[0]);
return 2;
}
struct sockaddr_in addr = {.sin_family = AF_INET, .sin_port = htons((uint16_t)atoi(argv[2]))};
if (inet_pton(AF_INET, argv[1], &addr.sin_addr) != 1) {
fprintf(stderr, "dirección inválida: %s\n", argv[1]);
return 2;
}
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
perror("socket");
return 1;
}
if (argc > 3) {
int retries = atoi(argv[3]);
if (setsockopt(fd, IPPROTO_TCP, TCP_SYNCNT, &retries, sizeof retries) < 0) {
perror("setsockopt TCP_SYNCNT");
return 1;
}
}
double start = now_seconds();
int result = connect(fd, (struct sockaddr *)&addr, sizeof addr);
int saved = errno;
double seconds = now_seconds() - start;
if (result == 0) {
printf("connect devolvió 0 en %.3f s\n", seconds);
} else {
printf("connect devolvió -1 en %.3f s: %s (%s)\n", seconds, errno_name(saved),
strerror(saved));
}
if (result == 0 && argc > 4) {
long ms = atol(argv[4]);
struct timespec ts = {ms / 1000, (ms % 1000) * 1000000L};
if (nanosleep(&ts, NULL) < 0) {
perror("nanosleep");
return 1;
}
}
if (close(fd) < 0) {
perror("close");
return 1;
}
return 0;
}// half_open: deja conexiones a medio abrir. Envía SYN desde una dirección propia y nunca
// termina el handshake, porque en este laboratorio el router descarta los SYN-ACK que
// vuelven a esa dirección. Es la forma de ver una cola de SYN llena sin falsificar nada.
//
// half_open <ip_propia> <ip_destino> <puerto> <cantidad> <segundos>
#define _POSIX_C_SOURCE 200809L
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <unistd.h>
int main(int argc, char **argv) {
if (argc != 6) {
fprintf(stderr, "uso: %s <ip_propia> <ip_destino> <puerto> <cantidad> <segundos>\n", argv[0]);
return 2;
}
struct sockaddr_in local = {.sin_family = AF_INET};
struct sockaddr_in peer = {.sin_family = AF_INET, .sin_port = htons((uint16_t)atoi(argv[3]))};
if (inet_pton(AF_INET, argv[1], &local.sin_addr) != 1 ||
inet_pton(AF_INET, argv[2], &peer.sin_addr) != 1) {
fprintf(stderr, "dirección inválida\n");
return 2;
}
int count = atoi(argv[4]);
for (int i = 0; i < count; i++) {
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
perror("socket");
return 1;
}
if (bind(fd, (struct sockaddr *)&local, sizeof local) < 0) {
perror("bind");
return 1;
}
// No bloqueante: connect vuelve de inmediato con EINPROGRESS, y el SYN ya salió.
int flags = fcntl(fd, F_GETFL);
if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) {
perror("fcntl");
return 1;
}
if (connect(fd, (struct sockaddr *)&peer, sizeof peer) < 0 && errno != EINPROGRESS) {
perror("connect");
return 1;
}
}
printf("%d conexiones a medio abrir\n", count);
if (fflush(stdout) != 0) {
perror("fflush");
return 1;
}
sleep((unsigned)atoi(argv[5]));
return 0;
}"""Ayudas de la semana 10: levantar la topología y correr comandos en sus nodos."""
import os
import subprocess
import time
import unittest
HERE = os.path.dirname(os.path.abspath(__file__))
CAPTURES = os.path.join(HERE, "captures")
# La solución no se publica. Sin ella, las pruebas usan lo que ustedes escriben en lab/.
ANSWERS = os.path.join(HERE, "solution")
if not os.path.isdir(ANSWERS):
ANSWERS = os.path.join(HERE, "lab")
def answer_path(name):
"""Devuelve la ruta de un archivo del laboratorio, o salta si sigue con el TODO."""
path = os.path.join(ANSWERS, name)
with open(path, encoding="utf-8") as file:
if "TODO" in file.read():
raise unittest.SkipTest(f"falta completar lab/{name}")
return path
class Lab:
"""La topología de Containerlab, que se levanta y se destruye desde las pruebas."""
def __init__(self, folder="tcp", name="w10-tcp", network="redes-w10-tcp"):
self.folder = os.path.join(HERE, folder)
self.topology = f"{folder}.clab.yml"
self.prefix = f"clab-{name}-"
self.network = network
def deploy(self):
done = subprocess.run(
["containerlab", "deploy", "-t", self.topology, "--reconfigure"],
cwd=self.folder, capture_output=True, text=True, timeout=300)
if done.returncode != 0:
raise RuntimeError(done.stdout + done.stderr)
def destroy(self):
subprocess.run(["containerlab", "destroy", "-t", self.topology, "--cleanup"],
cwd=self.folder, capture_output=True, text=True, timeout=300)
subprocess.run(["docker", "network", "rm", self.network], capture_output=True, text=True)
def sh(self, node, *command, timeout=90):
"""Corre un comando en un nodo y devuelve el resultado completo."""
return subprocess.run(["docker", "exec", self.prefix + node, *command],
capture_output=True, text=True, timeout=timeout)
def out(self, node, *command, timeout=90):
"""Corre un comando en un nodo y devuelve stdout y stderr juntos."""
done = self.sh(node, *command, timeout=timeout)
return done.stdout + done.stderr
def daemon(self, node, command):
"""Deja un comando corriendo en segundo plano dentro de un nodo."""
subprocess.run(["docker", "exec", "-d", self.prefix + node, "sh", "-c", command],
capture_output=True, text=True, check=True)
def counters(self, node):
"""Los contadores TCP del kernel de un nodo, como diccionario."""
table = {}
for line in self.out(node, "nstat", "-azs").splitlines():
parts = line.split()
if len(parts) >= 2 and parts[1].isdigit():
table[parts[0]] = int(parts[1])
return table
def wait_until(predicate, seconds=30, step=0.25):
"""Espera a que una condición se cumpla; devuelve si se cumplió."""
deadline = time.monotonic() + seconds
while time.monotonic() < deadline:
if predicate():
return True
time.sleep(step)
return predicate()"""Lee una captura con tshark y la devuelve como una lista de segmentos."""
import subprocess
FIELDS = {
"frame": "frame.number",
"time": "frame.time_relative",
"src": "ip.src",
"dst": "ip.dst",
"sport": "tcp.srcport",
"dport": "tcp.dstport",
"seq": "tcp.seq",
"seq_raw": "tcp.seq_raw",
"ack": "tcp.ack",
"len": "tcp.len",
"flags": "tcp.flags.str",
"win_field": "tcp.window_size_value",
"win": "tcp.window_size",
"mss": "tcp.options.mss_val",
"wscale": "tcp.options.wscale.shift",
"sack_perm": "tcp.options.sack_perm",
"ts": "tcp.options.timestamp.tsval",
"sack_le": "tcp.options.sack_le",
"sack_re": "tcp.options.sack_re",
"dup_ack": "tcp.analysis.duplicate_ack_num",
"retrans": "tcp.analysis.retransmission",
"fast": "tcp.analysis.fast_retransmission",
"spurious": "tcp.analysis.spurious_retransmission",
"out_of_order": "tcp.analysis.out_of_order",
"zero_window": "tcp.analysis.zero_window",
"probe": "tcp.analysis.keep_alive",
"window_update": "tcp.analysis.window_update",
"ack_rtt": "tcp.analysis.ack_rtt",
"rto": "tcp.analysis.rto",
}
NUMBERS = {"frame", "sport", "dport", "seq", "seq_raw", "ack", "len", "win_field", "win", "mss",
"wscale", "dup_ack"}
FLOATS = {"time", "ack_rtt", "rto"}
MARKS = {"sack_perm", "ts", "retrans", "fast", "spurious", "out_of_order", "zero_window", "probe",
"window_update"}
class Segment:
"""Un segmento de la captura. Los campos de FIELDS se leen como atributos."""
def __init__(self, values):
for name, text in values.items():
if name in MARKS:
value = text != ""
elif name in NUMBERS:
value = int(text) if text else None
elif name in FLOATS:
value = float(text) if text else None
else:
value = text
setattr(self, name, value)
# tshark escribe las banderas con un carácter por bit: ··A·S· y similares.
self.syn = "S" in self.flags
self.has_ack = "A" in self.flags
self.fin = "F" in self.flags
self.rst = "R" in self.flags
self.resent = self.retrans or self.fast or self.spurious
def read_capture(path):
"""Devuelve los segmentos TCP de la captura, en orden."""
command = ["tshark", "-r", path, "-Y", "tcp", "-T", "fields", "-E", "separator=\t"]
for field in FIELDS.values():
command += ["-e", field]
done = subprocess.run(command, capture_output=True, text=True, timeout=120)
if done.returncode != 0:
raise RuntimeError(done.stderr)
segments = []
for line in done.stdout.splitlines():
segments.append(Segment(dict(zip(FIELDS, line.split("\t")))))
return segments"""Calcula EstimatedRTT, DevRTT y el RTO para una lista de muestras, como el RFC 6298.
python3 rto_table.py 100 90 120 95 muestras en ms; la primera inicializa
python3 rto_table.py --start 100 5 120 parte de EstimatedRTT = 100 y DevRTT = 5
python3 rto_table.py --pcap captures/x-a.pcap toma las muestras de una captura
Solo hace la cuenta. No tiene temporizador ni retroceso: no es un emisor.
"""
import sys
from pcaptools import read_capture
ALPHA = 1 / 8
BETA = 1 / 4
def first_sample(sample):
"""Regla 2.2 del RFC 6298: la primera muestra inicializa los dos valores."""
return sample, sample / 2
def next_sample(estimated, deviation, sample):
"""Regla 2.3 del RFC 6298. DevRTT se calcula antes y usa el EstimatedRTT anterior."""
deviation = (1 - BETA) * deviation + BETA * abs(estimated - sample)
estimated = (1 - ALPHA) * estimated + ALPHA * sample
return estimated, deviation
def rto(estimated, deviation):
"""El RTO sin el piso de 1 s, para poder compararlo con las muestras."""
return estimated + 4 * deviation
def table(samples, start=None):
"""Devuelve filas (muestra, EstimatedRTT, DevRTT, RTO)."""
rows = []
state = start
for sample in samples:
state = next_sample(*state, sample) if state else first_sample(sample)
rows.append((sample, state[0], state[1], rto(*state)))
return rows
def samples_from_capture(path):
"""Las muestras de RTT que Wireshark deduce al emparejar cada dato con su ACK, en ms."""
segments = read_capture(path)
client = (segments[0].src, segments[0].sport)
return [s.ack_rtt * 1000 for s in segments
if s.ack_rtt is not None and (s.src, s.sport) != client and not s.syn]
def main():
args = sys.argv[1:]
start = None
if args[:1] == ["--pcap"] and len(args) == 2:
samples = samples_from_capture(args[1])
samples = samples[:: max(1, len(samples) // 12)]
elif args[:1] == ["--start"] and len(args) >= 4:
start = (float(args[1]), float(args[2]))
samples = [float(x) for x in args[3:]]
elif args and not args[0].startswith("-"):
samples = [float(x) for x in args]
else:
print(__doc__)
return 2
print(f"{'muestra':>9} {'EstimatedRTT':>13} {'DevRTT':>8} {'RTO':>8}")
for row in table(samples, start):
print(f"{row[0]:9.2f} {row[1]:13.2f} {row[2]:8.2f} {row[3]:8.2f}")
return 0
if __name__ == "__main__":
sys.exit(main())"""Los experimentos de la semana: cada uno prepara el enlace, transfiere y captura.
python3 scenarios.py lista los escenarios
python3 scenarios.py clean corre uno; las capturas quedan en captures/
Cada escenario imprime los comandos que corre: léanlos, porque son lo que se hace a mano en
el laboratorio. Hace falta la topología levantada (make up).
"""
import os
import sys
import time
from netlab import CAPTURES, Lab, wait_until
LAB = Lab()
SERVER = "10.210.2.10"
ALIAS = "10.210.1.20" # la segunda dirección de a: la de las conexiones a medio abrir
PORT = 5001
LINK = "delay 20ms rate 20mbit limit 200" # 40 ms de RTT y 20 Mbit/s, de a hacia b
# Con una cola de 200, el arranque de una transferencia a veces la desborda. Una cola de 2000
# es mayor que todos los segmentos de 2 MB juntos: aquí nada se pierde si no lo pedimos.
DEEP_LINK = "delay 20ms rate 20mbit limit 2000"
CLASSIC = {"tcp_sack": 0, "tcp_recovery": 0, "tcp_early_retrans": 0}
MODERN = {"tcp_sack": 1, "tcp_recovery": 1, "tcp_early_retrans": 3}
VERBOSE = True
def say(node, command):
if VERBOSE:
print(f" {node}# {command}")
def report(text):
"""Muestra un resultado. Las pruebas no lo imprimen: lo revisan."""
if VERBOSE:
print(text.rstrip())
def run(node, command):
"""Corre un comando visible en un nodo; falla si el comando falla."""
say(node, command)
done = LAB.sh(node, "sh", "-c", command)
if done.returncode != 0:
raise RuntimeError(f"{node}: {command}\n{done.stdout}{done.stderr}")
return done.stdout + done.stderr
def quiet(node, command):
"""Limpieza: corre un comando sin mostrarlo y sin importar si falla."""
return LAB.sh(node, "sh", "-c", command)
def set_link(toward_b=LINK, toward_a="delay 20ms"):
# Se borra y se vuelve a crear: con replace, netem conserva el rate anterior si no se le da
# uno nuevo, y con rate no desordena.
quiet("m", "tc qdisc del dev eth2 root; tc qdisc del dev eth1 root")
run("m", f"tc qdisc add dev eth2 root netem {toward_b}")
run("m", f"tc qdisc add dev eth1 root netem {toward_a}")
def set_tcp(values):
"""Fija sysctls de TCP en a y en b. Cada nodo tiene su propio network namespace."""
settings = " ".join(f"net.ipv4.{name}={value}" for name, value in values.items())
for node in "ab":
run(node, f"sysctl -qw {settings}")
def nft_drop(rule):
run("m", f"nft 'add rule ip w10 forward {rule} drop'")
def reset():
"""Deja todo como recién levantado: enlace bueno, sin filtros, sin programas colgados."""
for node in "ab":
quiet(node, "pkill sink; pkill slow_reader; pkill half_open; pkill tcpdump; "
"pkill blast; true")
quiet(node, "sysctl -qw net.ipv4.tcp_sack=1 net.ipv4.tcp_recovery=1 "
"net.ipv4.tcp_early_retrans=3 net.ipv4.tcp_syncookies=1 "
"net.ipv4.tcp_syn_linear_timeouts=4")
quiet("a", "ip link set eth1 mtu 1500; ip route change default via 10.210.1.1; "
"ip route flush cache")
quiet("m", f"tc qdisc del dev eth2 root; tc qdisc del dev eth1 root; "
f"tc qdisc add dev eth2 root netem {LINK}; "
"tc qdisc add dev eth1 root netem delay 20ms; "
"nft delete table ip w10; nft add table ip w10; "
"nft add chain ip w10 forward '{ type filter hook forward priority 0; }'")
time.sleep(0.2)
class Capture:
"""Captura en a y en b mientras dura el bloque with. Quedan name-a.pcap y name-b.pcap."""
def __init__(self, name, ports=f"tcp port {PORT}", nodes="ab"):
self.name = name
self.ports = ports
self.nodes = nodes
def path(self, node):
return os.path.join(CAPTURES, f"{self.name}-{node}.pcap")
def __enter__(self):
for node in self.nodes:
if os.path.exists(self.path(node)):
os.remove(self.path(node))
# -B agranda el buffer del kernel: sin eso, a toda velocidad, tcpdump pierde segmentos.
# --immediate-mode evita perder lo último al detenerlo con una señal.
command = (f"tcpdump -i eth1 -n -s 128 -B 16384 --immediate-mode -Z root "
f"-w /captures/{self.name}-{node}.pcap {self.ports}")
say(node, command + " &")
LAB.daemon(node, f"{command} >/tmp/tcpdump.log 2>&1")
ready = wait_until(lambda: all("listening" in quiet(n, "cat /tmp/tcpdump.log").stdout
for n in self.nodes), seconds=15)
if not ready:
raise RuntimeError("tcpdump no arrancó")
time.sleep(0.3)
return self
def __exit__(self, *_):
time.sleep(1.0) # lo último todavía viene en camino por el enlace con retardo
for node in self.nodes:
quiet(node, "pkill -INT tcpdump")
for node in self.nodes:
wait_until(lambda n=node: quiet(n, "pidof tcpdump").stdout.strip() == "", seconds=10)
return False
def start_server(args, program="sink"):
"""Arranca el servidor en b. Su salida queda en /tmp/server.out."""
say("b", f"/w10/{program} {args} &")
quiet("b", "rm -f /tmp/server.out")
LAB.daemon("b", f"/w10/{program} {args} >/tmp/server.out 2>&1")
if not wait_until(lambda: f":{args.split()[0]} " in quiet("b", "ss -ltn").stdout, seconds=10):
raise RuntimeError("el servidor no arrancó: " + quiet("b", "cat /tmp/server.out").stdout)
def server_output(seconds=60):
"""Espera a que termine el servidor y devuelve lo que escribió."""
wait_until(lambda: quiet("b", "pidof sink slow_reader").stdout.strip() == "",
seconds=seconds)
return quiet("b", "cat /tmp/server.out").stdout
def send(args=f"{SERVER} {PORT} 2000000"):
return run("a", f"/w10/blast {args}")
def transfer(name, size=2000000, server_args="", blast_args=""):
"""Una transferencia completa dentro de una captura. Devuelve la salida del servidor."""
with Capture(name):
start_server(f"{PORT} {server_args}".strip())
send(f"{SERVER} {PORT} {size} {blast_args}".strip())
result = server_output()
return result
def scenario_clean():
"""Transferencia de 2 MB sin pérdidas, con una cola que no se puede desbordar."""
reset()
set_link(DEEP_LINK)
return transfer("clean")
def scenario_mss():
"""Baja el MTU de a a 1400: el MSS que a anuncia cambia, y también el tamaño de los datos."""
reset()
run("a", "ip link set eth1 mtu 1400")
try:
return transfer("mss", size=100000)
finally:
run("a", "ip link set eth1 mtu 1500")
def scenario_drop():
"""El router descarta un solo segmento, el número 40. TCP moderno: con SACK y RACK."""
reset()
set_link(DEEP_LINK)
nft_drop(f"tcp dport {PORT} numgen inc mod 100000 == 40 counter")
before = LAB.counters("a")
result = transfer("drop")
return result, before, LAB.counters("a")
def scenario_classic():
"""El mismo descarte, con SACK y RACK apagados: TCP con los ACK duplicados de los libros."""
reset()
set_link(DEEP_LINK)
set_tcp(CLASSIC)
nft_drop(f"tcp dport {PORT} numgen inc mod 100000 == 40 counter")
try:
return transfer("classic")
finally:
set_tcp(MODERN)
def scenario_rto():
"""Un flujo lento y un corte total de 3 s: nadie manda ACK, y queda solo el temporizador."""
reset()
before = LAB.counters("a")
samples = {}
with Capture("rto"):
start_server(str(PORT))
LAB.daemon("a", f"/w10/blast {SERVER} {PORT} 60000 --chunk 1000 --gap-ms 100 --nodelay")
say("a", f"/w10/blast {SERVER} {PORT} 60000 --chunk 1000 --gap-ms 100 --nodelay &")
time.sleep(1.5)
samples["before"] = run("a", f"ss -tin dst {SERVER}")
report(samples["before"])
set_link(f"{LINK} loss 100%", "delay 20ms loss 100%")
time.sleep(2.2)
samples["during"] = run("a", f"ss -tin dst {SERVER}")
report(samples["during"])
time.sleep(0.8)
set_link()
server_output()
after = LAB.counters("a")
show_counters((before, after))
return before, after, samples
def scenario_tail():
"""Se pierden el último segmento y el FIN: no llega ningún ACK que avise. ¿Quién lo arregla?"""
reset()
# Hacia el servidor van: SYN (0), ACK (1), cinco de datos (2 a 6) y el FIN (7).
nft_drop(f"tcp dport {PORT} numgen inc mod 100000 {{ 6, 7 }} counter")
before = LAB.counters("a")
result = transfer("tail", size=7000)
return result, before, LAB.counters("a")
def scenario_loss():
"""2 % de pérdida aleatoria en el sentido de los datos."""
reset()
set_link(f"{LINK} loss 2%")
before = LAB.counters("a")
result = transfer("loss", size=3000000)
return result, before, LAB.counters("a")
def scenario_reorder():
"""Sin pérdida, sin tope de velocidad y con 3 ms de jitter: los segmentos se desordenan."""
reset()
set_link("delay 20ms 3ms limit 2000")
before = LAB.counters("a")
result = transfer("reorder", size=3000000)
return result, before, LAB.counters("a")
def scenario_reorder_classic():
"""El mismo enlace desordenado, con el TCP de los ACK duplicados."""
reset()
set_tcp(CLASSIC)
set_link("delay 20ms 3ms limit 2000")
before = LAB.counters("a")
try:
result = transfer("reorder-classic", size=3000000)
finally:
set_tcp(MODERN)
return result, before, LAB.counters("a")
def scenario_zero():
"""El receptor acepta y se queda 3 s sin leer: su buffer se llena y anuncia ventana cero."""
reset()
return transfer("zero", size=300000, server_args="--rcvbuf 16384 --stall-ms 3000")
def scenario_slow():
"""Su slow_reader del laboratorio: 2 KB cada 100 ms, o sea unos 160 kbit/s."""
reset()
with Capture("slow"):
start_server(f"{PORT} 8192 2048 100", program="slow_reader")
send(f"{SERVER} {PORT} 100000")
result = server_output()
return result
def scenario_limit():
"""La misma transferencia con buffers de recepción distintos: ¿quién limita?"""
reset()
results = {}
for rcvbuf in (8192, 32768, 262144):
name = f"limit-{rcvbuf}"
results[rcvbuf] = transfer(name, size=3000000, server_args=f"--rcvbuf {rcvbuf}")
report(f" rcvbuf {rcvbuf}: {results[rcvbuf].strip()}")
return results
def scenario_ports():
"""Qué devuelve connect: puerto cerrado, filtrado en silencio, rechazado, y sin ruta."""
reset()
results = {}
results["closed"] = run("a", f"/w10/connect_probe {SERVER} 5999 2")
nft_drop("tcp dport 5999")
with Capture("filtered", ports="tcp port 5999", nodes="a"):
results["filtered"] = run("a", f"/w10/connect_probe {SERVER} 5999 2")
# Linux reciente reintenta el SYN cada 1 s unas veces antes de empezar a duplicar.
run("a", "sysctl -qw net.ipv4.tcp_syn_linear_timeouts=0")
with Capture("filtered-exp", ports="tcp port 5999", nodes="a"):
results["filtered_exp"] = run("a", f"/w10/connect_probe {SERVER} 5999 2")
run("a", "sysctl -qw net.ipv4.tcp_syn_linear_timeouts=4")
reset()
run("m", "nft 'add rule ip w10 forward tcp dport 5999 reject with icmp type admin-prohibited'")
results["rejected"] = run("a", f"/w10/connect_probe {SERVER} 5999 2")
reset()
results["no_route"] = run("a", "/w10/connect_probe 10.211.0.1 5999 2")
start_server(str(PORT))
results["open"] = run("a", f"/w10/connect_probe {SERVER} {PORT} 2")
server_output()
for name, text in results.items():
report(f" {name}: {text.strip()}")
return results
def half_open_round(cookies):
"""Llena la cola de SYN de un servidor con backlog 16 y prueba a conectarse."""
reset()
run("b", f"sysctl -qw net.ipv4.tcp_syncookies={cookies}")
# El servidor no acepta nunca: importa la cola del handshake, no lo que pasa después.
start_server(f"{PORT} --stay --backlog 16")
nft_drop(f"ip saddr {SERVER} ip daddr {ALIAS} tcp flags & (syn|ack) == syn|ack")
before = LAB.counters("b")
LAB.daemon("a", f"/w10/half_open {ALIAS} {SERVER} {PORT} 100 12")
say("a", f"/w10/half_open {ALIAS} {SERVER} {PORT} 100 12 &")
time.sleep(2)
pending = len(run("b", "ss -tn state syn-recv").splitlines()) - 1
probe = run("a", f"/w10/connect_probe {SERVER} {PORT} 2")
after = LAB.counters("b")
quiet("a", "pkill half_open")
quiet("b", "pkill sink")
return {"pending": pending, "probe": probe, "before": before, "after": after}
def scenario_syn():
"""Una cola de SYN llena, sin y con SYN cookies."""
results = {"off": half_open_round(0), "on": half_open_round(1)}
for name, result in results.items():
grew = {key: result["after"].get(key, 0) - result["before"].get(key, 0)
for key in ("TcpExtTCPReqQFullDrop", "TcpExtSyncookiesSent", "TcpExtSyncookiesRecv")}
report(f" cookies {name}: {result['pending']} en SYN-RECV, {result['probe'].strip()}, {grew}")
reset()
return results
def scenario_delack():
"""Datos chicos cada 100 ms a un receptor que no lee: ¿cuánto espera antes del ACK?"""
reset()
return transfer("delack", size=4000, server_args="--stall-ms 6000",
blast_args="--chunk 100 --gap-ms 100 --nodelay")
def scenario_halfclose():
"""El cliente cierra su sentido y después todavía recibe la respuesta del servidor."""
reset()
return transfer("halfclose", size=3000, server_args="--reply", blast_args="--reply")
def scenario_timewait():
"""Se pierde el último ACK del cierre: ese es el trabajo de quien espera en TIME_WAIT."""
reset()
# Hacia el servidor van: SYN (0), ACK (1), FIN (2) y el último ACK (3).
nft_drop(f"tcp dport {PORT} numgen inc mod 100000 == 3 counter")
result = transfer("timewait", size=0)
states = run("a", f"ss -tano state time-wait dst {SERVER}")
report(states)
return result, states
def close_first_round(port, flags):
"""Un servidor que cierra primero; devuelve lo que dice uno nuevo en el mismo puerto."""
start_server(f"{port} --close-first {flags}".strip())
run("a", f"/w10/connect_probe {SERVER} {port} 2 300")
server_output()
time.sleep(0.2)
states = run("b", f"ss -tano state time-wait sport = :{port}")
report(states)
say("b", f"timeout 1 /w10/sink {port} {flags}")
again = quiet("b", f"timeout 1 /w10/sink {port} {flags}")
return states, again.stdout + again.stderr
def scenario_bindfail():
"""El servidor cierra primero y queda en TIME_WAIT: ¿puede volver a abrir ese puerto?"""
reset()
# Puertos propios: el TIME_WAIT de una ronda no debe estorbar a los otros escenarios.
without = close_first_round(5003, "--no-reuseaddr")
report(f" sin SO_REUSEADDR: {without[1].strip() or 'bind funciona'}")
with_reuse = close_first_round(5004, "")
report(f" con SO_REUSEADDR: {with_reuse[1].strip() or 'bind funciona'}")
return without, with_reuse
def scenario_abort():
"""El cliente cierra con RST en vez de FIN; el servidor se entera en su read."""
reset()
with Capture("abort"):
start_server(str(PORT))
send(f"{SERVER} {PORT} 3000 --abort")
result = server_output()
return result
def scenario_closed():
"""Conectarse a un puerto cerrado: el RST del kernel de b, capturado."""
reset()
with Capture("closed", ports="tcp port 5999"):
result = run("a", f"/w10/connect_probe {SERVER} 5999 2")
return result
def scenario_rtomin():
"""Fija rto_min en la ruta de a: se ve qué parte del RTO de Linux es ese piso."""
reset()
rows = {}
for floor in (None, "500ms"):
if floor:
run("a", f"ip route change default via 10.210.1.1 rto_min {floor}")
start_server(str(PORT))
LAB.daemon("a", f"/w10/blast {SERVER} {PORT} 20000 --chunk 1000 --gap-ms 100 --nodelay")
say("a", f"/w10/blast {SERVER} {PORT} 20000 --chunk 1000 --gap-ms 100 --nodelay &")
time.sleep(1.2)
rows[floor or "default"] = run("a", f"ss -tin dst {SERVER}")
server_output()
run("a", "ip route change default via 10.210.1.1")
for floor, text in rows.items():
report(f" {floor}: {' '.join(w for w in text.split() if w.startswith(('rto:', 'rtt:')))}")
return rows
def scenario_ss():
"""Una conexión activa vista con ss cada segundo: rtt, rto, cwnd."""
reset()
start_server(str(PORT))
LAB.daemon("a", f"/w10/blast {SERVER} {PORT} 8000000")
say("a", f"/w10/blast {SERVER} {PORT} 8000000 &")
rows = []
for _ in range(3):
time.sleep(0.8)
rows.append(run("a", f"ss -tin dst {SERVER}"))
report(rows[-1])
server_output()
return rows
def show_counters(result):
"""Muestra cuánto cambiaron los contadores del kernel entre (antes, después)."""
names = ("TcpRetransSegs", "TcpExtTCPTimeouts", "TcpExtTCPLossProbes", "TcpExtTCPSackRecovery",
"TcpExtTCPRenoRecovery")
before, after = result
report(" ".join(f"{name}={after.get(name, 0) - before.get(name, 0)}" for name in names))
def scenarios():
module = sys.modules[__name__]
return {name[len("scenario_"):]: getattr(module, name)
for name in dir(module) if name.startswith("scenario_")}
def main():
table = scenarios()
if len(sys.argv) != 2 or sys.argv[1] not in table:
width = max(len(name) for name in table)
for name, function in table.items():
print(f"{name:<{width}} {function.__doc__}")
return 2
result = table[sys.argv[1]]()
if isinstance(result, str):
print(result, end="")
elif isinstance(result, tuple) and len(result) == 3 and isinstance(result[0], str):
print(result[0], end="")
show_counters(result[1:])
return 0
if __name__ == "__main__":
sys.exit(main())// sink: acepta una conexión TCP, lee todo lo que llegue y cuenta los bytes.
//
// sink <puerto> [--rcvbuf B] [--stall-ms N] [--reply] [--close-first] [--backlog N]
// [--no-reuseaddr] [--stay]
//
// --rcvbuf fija SO_RCVBUF antes de listen, para que la conexión lo herede
// --stall-ms acepta y se queda N ms sin leer: el buffer de recepción se llena
// --reply cuando el cliente cierra su sentido, le contesta cuántos bytes recibió
// --close-first apenas acepta, cierra él primero (se queda con TIME_WAIT)
// --backlog segundo argumento de listen
// --stay no acepta nunca: sirve para ver la cola de conexiones
#define _POSIX_C_SOURCE 200809L
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>
static double now_seconds(void) {
struct timespec ts;
if (clock_gettime(CLOCK_MONOTONIC, &ts) < 0) {
perror("clock_gettime");
exit(1);
}
return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
}
static void pause_ms(long ms) {
struct timespec ts = {ms / 1000, (ms % 1000) * 1000000L};
while (nanosleep(&ts, &ts) < 0) {
if (errno != EINTR) {
perror("nanosleep");
exit(1);
}
}
}
int main(int argc, char **argv) {
if (argc < 2) {
fprintf(stderr, "uso: %s <puerto> [opciones]\n", argv[0]);
return 2;
}
int rcvbuf = 0, backlog = 16, reply = 0, close_first = 0, reuse = 1, stay = 0;
long stall_ms = 0;
for (int i = 2; i < argc; i++) {
if (strcmp(argv[i], "--rcvbuf") == 0 && i + 1 < argc) {
rcvbuf = atoi(argv[++i]);
} else if (strcmp(argv[i], "--stall-ms") == 0 && i + 1 < argc) {
stall_ms = atol(argv[++i]);
} else if (strcmp(argv[i], "--backlog") == 0 && i + 1 < argc) {
backlog = atoi(argv[++i]);
} else if (strcmp(argv[i], "--reply") == 0) {
reply = 1;
} else if (strcmp(argv[i], "--close-first") == 0) {
close_first = 1;
} else if (strcmp(argv[i], "--no-reuseaddr") == 0) {
reuse = 0;
} else if (strcmp(argv[i], "--stay") == 0) {
stay = 1;
} else {
fprintf(stderr, "opción desconocida: %s\n", argv[i]);
return 2;
}
}
int listener = socket(AF_INET, SOCK_STREAM, 0);
if (listener < 0) {
perror("socket");
return 1;
}
if (reuse && setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof reuse) < 0) {
perror("setsockopt SO_REUSEADDR");
return 1;
}
if (rcvbuf > 0 && setsockopt(listener, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof rcvbuf) < 0) {
perror("setsockopt SO_RCVBUF");
return 1;
}
struct sockaddr_in addr = {.sin_family = AF_INET,
.sin_port = htons((uint16_t)atoi(argv[1])),
.sin_addr.s_addr = htonl(INADDR_ANY)};
if (bind(listener, (struct sockaddr *)&addr, sizeof addr) < 0) {
perror("bind");
return 1;
}
if (listen(listener, backlog) < 0) {
perror("listen");
return 1;
}
if (stay) {
for (;;) {
pause_ms(1000);
}
}
int fd = accept(listener, NULL, NULL);
if (fd < 0) {
perror("accept");
return 1;
}
double start = now_seconds();
if (stall_ms > 0) {
pause_ms(stall_ms);
}
if (close_first) {
if (close(fd) < 0) {
perror("close");
return 1;
}
printf("cerré primero\n");
return 0;
}
static char buf[65536];
long long total = 0;
ssize_t got;
while ((got = read(fd, buf, sizeof buf)) != 0) {
if (got < 0) {
if (errno == EINTR) {
continue;
}
perror("read");
return 1;
}
total += got;
}
if (reply) {
char text[64];
int len = snprintf(text, sizeof text, "%lld bytes\n", total);
if (write(fd, text, (size_t)len) < 0) {
perror("write");
return 1;
}
}
double seconds = now_seconds() - start;
if (close(fd) < 0) {
perror("close");
return 1;
}
// Se mide aquí y no en el emisor: write termina cuando el kernel acepta los bytes en su
// buffer, no cuando llegan.
printf("recibidos %lld bytes en %.2f s (%.1f Mbit/s)\n", total, seconds,
seconds > 0 ? (double)total * 8 / seconds / 1e6 : 0.0);
return 0;
}"""Resume una captura TCP: handshake, datos, ACK duplicados, retransmisiones, cierre.
python3 tcpsum.py captures/clean-a.pcap
Las marcas de retransmisión las pone Wireshark por deducción: mira la captura desde afuera
y no sabe qué pasó dentro del kernel.
"""
import sys
from pcaptools import read_capture
def summarize(segments):
"""Calcula el resumen de la primera conexión de la captura."""
first = segments[0]
client = (first.src, first.sport)
data = [s for s in segments if (s.src, s.sport) == client and s.len]
back = [s for s in segments if (s.src, s.sport) != client]
return {
"client": f"{first.src}:{first.sport}",
"server": f"{first.dst}:{first.dport}",
"syn": next((s for s in segments if s.syn and not s.has_ack), None),
"synack": next((s for s in segments if s.syn and s.has_ack), None),
"data_segments": len(data),
"data_bytes": sum(s.len for s in data),
"new_bytes": max((s.seq + s.len - 1 for s in data), default=0),
"largest": max((s.len for s in data), default=0),
"acks": len([s for s in back if s.has_ack and not s.len and not s.syn]),
"dup_acks": len([s for s in back if s.dup_ack]),
"max_dup": max((s.dup_ack for s in back if s.dup_ack), default=0),
"dup_reached": {level: len([s for s in back if s.dup_ack == level]) for level in (1, 2, 3)},
"resent": len([s for s in data if s.resent]),
"fast": len([s for s in data if s.fast]),
"zero_window": len([s for s in back if s.zero_window]),
"probes": len([s for s in segments if s.probe]),
"max_window": max((s.win for s in back if s.win is not None and not s.syn), default=0),
"fins": len([s for s in segments if s.fin]),
"rsts": len([s for s in segments if s.rst]),
"seconds": segments[-1].time,
}
def describe_options(segment):
return (f"MSS {segment.mss}, ventana {segment.win_field}, escala {segment.wscale}, "
f"SACK {'sí' if segment.sack_perm else 'no'}, "
f"timestamps {'sí' if segment.ts else 'no'}")
def main():
if len(sys.argv) != 2:
print(__doc__)
return 2
segments = read_capture(sys.argv[1])
if not segments:
print("la captura no tiene segmentos TCP")
return 1
info = summarize(segments)
print(f"conexión {info['client']} -> {info['server']}")
if info["syn"]:
print(f"SYN seq real {info['syn'].seq_raw}; {describe_options(info['syn'])}")
if info["synack"]:
print(f"SYN-ACK seq real {info['synack'].seq_raw}; {describe_options(info['synack'])}")
print(f"datos {info['data_segments']} segmentos, {info['data_bytes']} bytes enviados, "
f"{info['new_bytes']} bytes distintos, el mayor de {info['largest']}")
print(f"ACK de vuelta {info['acks']} sin datos, ventana máxima anunciada {info['max_window']}")
reached = info["dup_reached"]
print(f"ACK duplicados {info['dup_acks']} (el mayor, #{info['max_dup']}); rachas que llegaron "
f"a #1: {reached[1]}, a #2: {reached[2]}, a #3: {reached[3]}")
print(f"retransmitidos {info['resent']} segmentos, {info['fast']} de ellos marcados como fast")
print(f"ventana cero {info['zero_window']} avisos, {info['probes']} sondas")
print(f"cierre {info['fins']} FIN, {info['rsts']} RST")
print(f"duración {info['seconds']:.3f} s")
return 0
if __name__ == "__main__":
sys.exit(main())"""Dibuja el gráfico tiempo-secuencia de una captura, como el de Wireshark.
python3 timeseq.py captures/loss-a.pcap captures/loss-a.png
Cada punto es un segmento de datos del cliente. Los retransmitidos van en rojo, y la línea
gris es el último ACK recibido. Wireshark: Statistics > TCP Stream Graphs > Time Sequence.
"""
import sys
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from pcaptools import read_capture
def points(segments):
"""Separa los segmentos en datos nuevos, retransmitidos y ACK: (tiempo, MB)."""
client = (segments[0].src, segments[0].sport)
new, resent, acks = [], [], []
for segment in segments:
if (segment.src, segment.sport) == client:
if segment.len:
(resent if segment.resent else new).append((segment.time, segment.seq / 1e6))
elif segment.has_ack:
acks.append((segment.time, segment.ack / 1e6))
return new, resent, acks
def main():
if len(sys.argv) != 3:
print(__doc__)
return 2
new, resent, acks = points(read_capture(sys.argv[1]))
figure, axes = plt.subplots(figsize=(8, 4.5))
if acks:
axes.step(*zip(*acks), where="post", color="0.6", linewidth=1, label="último ACK")
if new:
axes.scatter(*zip(*new), s=4, color="tab:blue", label="datos")
if resent:
axes.scatter(*zip(*resent), s=18, color="tab:red", marker="x", label="retransmitidos")
axes.set_xlabel("tiempo (s)")
axes.set_ylabel("número de secuencia relativo (MB)")
axes.legend(loc="upper left")
figure.tight_layout()
figure.savefig(sys.argv[2], dpi=130)
print(f"gráfico en {sys.argv[2]}")
return 0
if __name__ == "__main__":
sys.exit(main())# Dos hosts unidos por un enlace que se puede empeorar a propósito. El router m está en el
# medio para que la pérdida ocurra fuera de los hosts: así una captura en a ve el segmento
# antes de perderse, y una captura en b ve lo que de verdad llegó.
#
# a 10.210.1.10 ---- eth1 m eth2 ---- 10.210.2.10 b
#
# La carpeta bin/ tiene los programas de la semana compilados con -static, y captures/ es
# donde quedan los .pcap, visibles también desde el contenedor del curso.
name: w10-tcp
mgmt:
network: redes-w10-tcp
ipv4-subnet: 172.90.10.0/26
ipv6-subnet: 3fff:172:90:10::/64
topology:
defaults:
kind: linux
image: redes-router
binds:
- ../bin:/w10:ro
- ../captures:/captures
nodes:
a:
exec:
- ip route del default
- sysctl -w net.ipv6.conf.all.disable_ipv6=1
- ip addr add 10.210.1.10/24 dev eth1
- ip addr add 10.210.1.20/24 dev eth1
- ip route add default via 10.210.1.1
b:
exec:
- ip route del default
- sysctl -w net.ipv6.conf.all.disable_ipv6=1
- ip addr add 10.210.2.10/24 dev eth1
- ip route add default via 10.210.2.1
m:
exec:
- ip route del default
- sysctl -w net.ipv6.conf.all.disable_ipv6=1
- sysctl -w net.ipv4.ip_forward=1
- ip addr add 10.210.1.1/24 dev eth1
- ip addr add 10.210.2.1/24 dev eth2
# Containerlab usa MTU 9500 por defecto; aquí queremos el valor de Ethernet.
links:
- {endpoints: ["a:eth1", "m:eth1"], mtu: 1500}
- {endpoints: ["m:eth2", "b:eth1"], mtu: 1500}Pruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
"""Pruebas de la cuenta del RTT y del RTO en rto_table.py. No necesitan la red."""
import unittest
from rto_table import first_sample, next_sample, rto, table
class Estimator(unittest.TestCase):
def test_exercise_of_the_guide(self):
estimated, deviation = next_sample(100, 5, 120)
self.assertAlmostEqual(estimated, 102.5)
self.assertAlmostEqual(deviation, 8.75)
self.assertAlmostEqual(rto(estimated, deviation), 137.5)
def test_deviation_uses_the_previous_estimate(self):
# Con el EstimatedRTT ya actualizado saldría 8,125 y no 8,75.
_, deviation = next_sample(100, 5, 120)
self.assertNotAlmostEqual(deviation, 0.75 * 5 + 0.25 * abs(102.5 - 120))
def test_first_sample_initializes(self):
self.assertEqual(first_sample(80), (80, 40))
self.assertAlmostEqual(rto(*first_sample(80)), 240)
def test_steady_samples_bring_the_rto_down_to_the_rtt(self):
last = table([40.0] * 60)[-1]
self.assertAlmostEqual(last[1], 40, places=3)
self.assertLess(last[3], 41)
def test_one_slow_sample_moves_the_rto_more_than_the_estimate(self):
rows = table([40.0] * 60 + [200.0])
self.assertLess(rows[-1][1] - rows[-2][1], rows[-1][3] - rows[-2][3])
if __name__ == "__main__":
unittest.main()"""Prueba del laboratorio: slow_reader. Se salta mientras lab/slow_reader.c tenga TODO."""
import os
import unittest
import scenarios
from netlab import CAPTURES, answer_path, wait_until
from pcaptools import read_capture
from tcpsum import summarize
LAB = scenarios.LAB
scenarios.VERBOSE = False
def setUpModule():
answer_path("slow_reader.c")
unittest.addModuleCleanup(LAB.destroy)
LAB.deploy()
scenarios.reset()
if not wait_until(lambda: LAB.sh("a", "ping", "-c", "1", "-W", "2", scenarios.SERVER).returncode == 0,
seconds=30):
raise RuntimeError("la topología no quedó lista")
class SlowReader(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.output = scenarios.scenario_slow()
cls.segments = read_capture(os.path.join(CAPTURES, "slow-a.pcap"))
cls.info = summarize(cls.segments)
def test_all_bytes_arrive(self):
self.assertIn("recibidos 100000 bytes", self.output)
def test_it_reads_slowly(self):
# 100000 bytes de a 2048 cada 100 ms son unos 4,9 s, menos lo que queda en el buffer.
self.assertGreater(self.info["seconds"], 3.5)
def test_rcvbuf_was_set_before_listen(self):
self.assertLess(self.info["max_window"], 20000)
self.assertEqual(self.info["synack"].wscale, 0)
def test_zero_window_appears(self):
self.assertGreaterEqual(self.info["zero_window"], 2)
if __name__ == "__main__":
unittest.main()"""Pruebas de lo que afirman los apuntes, sobre capturas de la topología tcp/tcp.clab.yml.
Las mediciones son ruidosas: se comprueban tendencias con margen, nunca cifras exactas.
"""
import os
import re
import statistics
import subprocess
import unittest
import scenarios
from netlab import CAPTURES, HERE, wait_until
from pcaptools import read_capture
from tcpsum import summarize
LAB = scenarios.LAB
scenarios.VERBOSE = False
CLIENT = "10.210.1.10"
def capture(name, node="a"):
return read_capture(os.path.join(CAPTURES, f"{name}-{node}.pcap"))
def data_of(segments):
return [s for s in segments if s.src == CLIENT and s.len]
def from_server(segments):
return [s for s in segments if s.src != CLIENT]
def grew(before, after, name):
return after.get(name, 0) - before.get(name, 0)
def setUpModule():
unittest.addModuleCleanup(LAB.destroy)
LAB.deploy()
scenarios.reset()
if not wait_until(lambda: LAB.sh("a", "ping", "-c", "1", "-W", "2", scenarios.SERVER).returncode == 0,
seconds=30):
raise RuntimeError("la topología no quedó lista")
class CleanTransfer(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.output = scenarios.scenario_clean()
cls.segments = capture("clean")
cls.info = summarize(cls.segments)
def test_all_bytes_arrive(self):
self.assertIn("recibidos 2000000 bytes", self.output)
def test_three_way_handshake(self):
syn, synack, third = self.segments[:3]
self.assertTrue(syn.syn and not syn.has_ack)
self.assertTrue(synack.syn and synack.has_ack)
self.assertTrue(third.has_ack and not third.syn)
# El SYN gasta un número de secuencia: el ACK es el número inicial más uno.
self.assertEqual(third.ack, 1)
self.assertEqual(synack.ack, 1)
self.assertEqual(third.seq_raw, syn.seq_raw + 1)
def test_mss_and_size_of_the_data(self):
self.assertEqual(self.segments[0].mss, 1500 - 20 - 20)
self.assertEqual(self.info["largest"], 1460 - 12) # 12 bytes de la opción de timestamps
self.assertTrue(self.segments[0].ts)
def test_sequence_counts_bytes(self):
data = data_of(self.segments)
self.assertEqual(data[0].seq, 1)
for current, following in zip(data, data[1:]):
self.assertEqual(following.seq, current.seq + current.len)
def test_ack_is_the_next_byte_expected(self):
limits = {s.seq + s.len for s in data_of(self.segments)} | {1}
limits.add(max(limits) + 1) # el FIN del cliente también gasta un número
for ack in from_server(self.segments):
if ack.has_ack and not ack.fin and not ack.syn:
self.assertIn(ack.ack, limits)
def test_relative_is_not_the_real_number(self):
first = data_of(self.segments)[0]
self.assertEqual(first.seq, 1)
self.assertEqual(first.seq_raw, self.segments[0].seq_raw + 1)
self.assertNotEqual(first.seq_raw, 1)
def test_window_scale(self):
self.assertEqual(self.segments[0].wscale, 7)
self.assertEqual(self.segments[0].win, self.segments[0].win_field) # el SYN no se escala
later = from_server(self.segments)[5]
self.assertEqual(later.win, later.win_field * 128)
def test_no_loss_means_no_retransmissions(self):
self.assertEqual(self.info["resent"], 0)
self.assertEqual(self.info["dup_acks"], 0)
def test_fewer_acks_than_data_segments(self):
self.assertLess(self.info["acks"], 0.95 * self.info["data_segments"])
self.assertGreater(self.info["acks"], 0.4 * self.info["data_segments"])
def test_closes_with_fin(self):
self.assertEqual(self.info["fins"], 2)
self.assertEqual(self.info["rsts"], 0)
def test_receiver_sees_the_same_bytes(self):
self.assertEqual(len(data_of(capture("clean", "b"))), self.info["data_segments"])
def test_time_sequence_graph(self):
png = os.path.join(CAPTURES, "clean-a.png")
done = subprocess.run(["python3", "timeseq.py", os.path.join(CAPTURES, "clean-a.pcap"), png],
cwd=HERE, capture_output=True, text=True, timeout=120)
self.assertEqual(done.returncode, 0, done.stderr)
with open(png, "rb") as file:
self.assertEqual(file.read(8)[1:4], b"PNG")
def test_initial_sequence_numbers_change(self):
scenarios.scenario_mss()
self.assertNotEqual(capture("mss")[0].seq_raw, self.segments[0].seq_raw)
class SmallerMtu(unittest.TestCase):
def test_mss_follows_the_mtu(self):
scenarios.scenario_mss()
segments = capture("mss")
info = summarize(segments)
self.assertEqual(info["syn"].mss, 1400 - 40)
self.assertEqual(info["synack"].mss, 1500 - 40)
self.assertEqual(info["largest"], 1360 - 12)
class OneDrop(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.output, cls.before, cls.after = scenarios.scenario_drop()
cls.a = capture("drop", "a")
cls.b = capture("drop", "b")
cls.resent = [s for s in data_of(cls.a) if s.resent]
def test_all_bytes_arrive(self):
self.assertIn("recibidos 2000000 bytes", self.output)
def test_lost_segment_goes_twice_and_arrives_once(self):
self.assertGreaterEqual(len(self.resent), 1)
lost = self.resent[0].seq
self.assertEqual(len([s for s in data_of(self.a) if s.seq == lost]), 2)
self.assertEqual(len([s for s in data_of(self.b) if s.seq == lost]), 1)
def test_retransmission_comes_after_duplicate_acks(self):
retransmission = self.resent[0]
earlier = [s for s in from_server(self.a) if s.frame < retransmission.frame and s.dup_ack]
self.assertGreaterEqual(len(earlier), 3)
self.assertTrue(all(s.ack == retransmission.seq for s in earlier))
self.assertTrue(retransmission.fast)
def test_duplicate_acks_carry_sack(self):
duplicates = [s for s in from_server(self.a) if s.dup_ack]
self.assertTrue(duplicates)
lost = self.resent[0].seq
for segment in duplicates[:3]:
self.assertGreater(int(segment.sack_le.split(",")[0]), lost)
def test_ack_jumps_once_the_hole_is_filled(self):
retransmission = self.resent[0]
later = [s for s in from_server(self.a) if s.frame > retransmission.frame
and s.ack > retransmission.seq]
self.assertGreater(later[0].ack - retransmission.seq, 10 * retransmission.len)
def test_no_timeout(self):
self.assertEqual(grew(self.before, self.after, "TcpExtTCPTimeouts"), 0)
self.assertGreaterEqual(grew(self.before, self.after, "TcpExtTCPSackRecovery"), 1)
class ClassicDrop(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.output = scenarios.scenario_classic()
cls.segments = capture("classic")
def test_sack_was_not_negotiated(self):
self.assertFalse(self.segments[0].sack_perm)
self.assertFalse(any(s.sack_le for s in self.segments))
def test_retransmits_on_the_third_duplicate(self):
retransmission = next(s for s in data_of(self.segments) if s.resent)
earlier = [s for s in from_server(self.segments)
if s.frame < retransmission.frame and s.dup_ack]
# El kernel puede atender varios ACK juntos: al menos tres, y no muchos más.
self.assertGreaterEqual(len(earlier), 3)
self.assertLessEqual(len(earlier), 8)
self.assertIn("recibidos 2000000 bytes", self.output)
class RetransmissionTimeout(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.before, cls.after, cls.samples = scenarios.scenario_rto()
cls.segments = capture("rto")
resent = [s for s in data_of(cls.segments) if s.resent]
counts = {}
for segment in resent:
counts[segment.seq] = counts.get(segment.seq, []) + [segment.time]
cls.times = max(counts.values(), key=len)
def test_timer_expired_several_times(self):
self.assertGreaterEqual(grew(self.before, self.after, "TcpExtTCPTimeouts"), 2)
self.assertGreaterEqual(len(self.times), 3)
def test_interval_doubles(self):
gaps = [b - a for a, b in zip(self.times, self.times[1:])]
for shorter, longer in zip(gaps, gaps[1:]):
self.assertGreater(longer / shorter, 1.5)
self.assertLess(longer / shorter, 2.6)
def test_a_probe_goes_first(self):
self.assertGreaterEqual(grew(self.before, self.after, "TcpExtTCPLossProbes"), 1)
def test_rto_of_linux_is_rtt_plus_a_floor(self):
text = self.samples["before"]
rto = float(re.search(r"\brto:([\d.]+)", text).group(1))
rtt = float(re.search(r"\brtt:([\d.]+)", text).group(1))
self.assertGreater(rto - rtt, 150)
self.assertLess(rto - rtt, 320)
def test_backoff_is_visible_in_ss(self):
self.assertRegex(self.samples["during"], r"backoff:\d+")
def test_transfer_recovers(self):
self.assertIn("recibidos 60000 bytes", LAB.out("b", "cat", "/tmp/server.out"))
class RtoFloor(unittest.TestCase):
def test_gap_follows_rto_min(self):
rows = scenarios.scenario_rtomin()
gaps = {}
for name, text in rows.items():
rto = float(re.search(r"\brto:([\d.]+)", text).group(1))
rtt = float(re.search(r"\brtt:([\d.]+)", text).group(1))
gaps[name] = rto - rtt
self.assertLess(gaps["default"], 320)
self.assertGreater(gaps["500ms"], 430)
class TailLoss(unittest.TestCase):
def test_a_probe_goes_out_with_no_ack_to_trigger_it(self):
# Después de la sonda, el dato puede salir por RACK o por timeout: cambia entre corridas.
output, before, after = scenarios.scenario_tail()
self.assertIn("recibidos 7000 bytes", output)
self.assertGreaterEqual(grew(before, after, "TcpExtTCPLossProbes"), 1)
segments = capture("tail")
probe = next(s for s in segments if s.resent)
self.assertTrue(probe.fin) # la sonda repite lo último que se envió: el FIN
self.assertEqual(len([s for s in segments if s.dup_ack and s.frame < probe.frame]), 0)
class RandomLoss(unittest.TestCase):
def test_transfer_survives_and_slows_down(self):
output, before, after = scenarios.scenario_loss()
self.assertIn("recibidos 3000000 bytes", output)
rate = float(re.search(r"\(([\d.]+) Mbit/s\)", output).group(1))
self.assertLess(rate, 12)
info = summarize(capture("loss"))
self.assertGreater(info["resent"], 5)
self.assertGreater(info["dup_acks"], info["resent"])
self.assertGreater(grew(before, after, "TcpRetransSegs"), 5)
class Reordering(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.modern_output, before, after = scenarios.scenario_reorder()
cls.modern_resent = grew(before, after, "TcpRetransSegs")
cls.info = summarize(capture("reorder"))
cls.classic_output, before, after = scenarios.scenario_reorder_classic()
cls.classic_resent = grew(before, after, "TcpRetransSegs")
def test_all_bytes_arrive(self):
self.assertIn("recibidos 3000000 bytes", self.modern_output)
self.assertIn("recibidos 3000000 bytes", self.classic_output)
def test_duplicate_acks_without_loss(self):
self.assertGreater(self.info["dup_acks"], 100)
self.assertGreater(self.info["dup_reached"][3], 10)
def test_classic_tcp_retransmits_more(self):
# Nada se perdió: todo lo retransmitido es trabajo de más, y el TCP clásico hace más.
self.assertGreater(self.classic_resent, self.modern_resent)
self.assertGreater(self.classic_resent, 5)
def test_capture_is_complete(self):
self.assertEqual(self.info["new_bytes"], 3000000)
class ZeroWindow(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.output = scenarios.scenario_zero()
cls.segments = capture("zero")
def test_all_bytes_arrive(self):
self.assertIn("recibidos 300000 bytes", self.output)
def test_receiver_announces_zero(self):
zero = [s for s in from_server(self.segments) if s.zero_window]
self.assertGreaterEqual(len(zero), 2)
self.assertTrue(all(s.win == 0 for s in zero))
def test_probes_have_no_data_and_old_sequence(self):
probes = [s for s in self.segments if s.probe]
self.assertGreaterEqual(len(probes), 2)
zero = next(s for s in self.segments if s.zero_window)
for probe in probes:
self.assertEqual(probe.len, 0)
self.assertEqual(probe.seq, zero.ack - 1)
def test_probe_interval_grows(self):
times = [s.time for s in self.segments if s.probe]
gaps = [b - a for a, b in zip(times, times[1:])]
for shorter, longer in zip(gaps, gaps[1:]):
self.assertGreater(longer, 1.4 * shorter)
def test_sender_never_passes_the_window(self):
limit = None
for segment in self.segments:
if segment.src != CLIENT and segment.has_ack and segment.win is not None:
limit = segment.ack + segment.win
elif segment.src == CLIENT and segment.len and limit is not None:
self.assertLessEqual(segment.seq + segment.len, limit)
def test_transfer_waits_for_the_update(self):
last_zero = [s for s in self.segments if s.zero_window][-1]
update = next(s for s in from_server(self.segments)
if s.frame > last_zero.frame and s.win > 0)
self.assertGreater(update.time, 2.5) # el servidor pasó 3 s sin leer
self.assertTrue(any(s.frame > update.frame for s in data_of(self.segments)))
class WindowLimitsThroughput(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.results = scenarios.scenario_limit()
cls.rates = {size: float(re.search(r"\(([\d.]+) Mbit/s\)", text).group(1))
for size, text in cls.results.items()}
def test_more_buffer_more_throughput(self):
self.assertLess(self.rates[8192] * 2, self.rates[32768])
self.assertLess(self.rates[32768] * 1.5, self.rates[262144])
def test_rate_is_about_window_over_rtt(self):
window = summarize(capture("limit-8192"))["max_window"]
predicted = window * 8 / 0.040 / 1e6
self.assertGreater(self.rates[8192], 0.4 * predicted)
self.assertLess(self.rates[8192], 1.5 * predicted)
def test_large_buffer_is_limited_by_the_link(self):
self.assertLess(self.rates[262144], 22)
self.assertGreater(self.rates[262144], 8)
def test_the_limit_is_not_loss(self):
# Puede salir alguna sonda de más, pero la ventana explica la velocidad, no la pérdida.
info = summarize(capture("limit-8192"))
self.assertLess(info["resent"], 0.01 * info["data_segments"])
class ConnectResults(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.results = scenarios.scenario_ports()
def seconds(self, name):
return float(re.search(r"en ([\d.]+) s", self.results[name]).group(1))
def test_closed_port_is_refused_at_once(self):
self.assertIn("ECONNREFUSED", self.results["closed"])
self.assertLess(self.seconds("closed"), 1)
def test_filtered_port_times_out(self):
self.assertIn("ETIMEDOUT", self.results["filtered"])
self.assertGreater(self.seconds("filtered"), 5)
def syn_gaps(self, name):
times = [s.time for s in read_capture(os.path.join(CAPTURES, f"{name}-a.pcap")) if s.syn]
return [b - a for a, b in zip(times, times[1:])]
def test_syn_retransmissions_start_linear_in_this_kernel(self):
gaps = self.syn_gaps("filtered")
self.assertGreaterEqual(len(gaps), 4)
for gap in gaps[:4]:
self.assertAlmostEqual(gap, 1, delta=0.4)
def test_syn_retransmissions_double_without_the_linear_phase(self):
gaps = self.syn_gaps("filtered-exp")
self.assertEqual(len(gaps), 2)
self.assertAlmostEqual(gaps[0], 1, delta=0.4)
self.assertAlmostEqual(gaps[1], 2, delta=0.6)
self.assertIn("ETIMEDOUT", self.results["filtered_exp"])
def test_rejected_by_the_router(self):
self.assertIn("EHOSTUNREACH", self.results["rejected"])
self.assertLess(self.seconds("rejected"), 1)
def test_no_route(self):
self.assertIn("ENETUNREACH", self.results["no_route"])
def test_open_port(self):
self.assertIn("devolvió 0", self.results["open"])
class SynQueue(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.results = scenarios.scenario_syn()
def test_queue_fills_up_to_the_backlog(self):
for result in self.results.values():
self.assertGreaterEqual(result["pending"], 10)
self.assertLessEqual(result["pending"], 30)
def test_without_cookies_a_legitimate_client_cannot_enter(self):
result = self.results["off"]
self.assertIn("ETIMEDOUT", result["probe"])
self.assertGreater(grew(result["before"], result["after"], "TcpExtTCPReqQFullDrop"), 0)
def test_with_cookies_the_same_client_enters(self):
result = self.results["on"]
self.assertIn("devolvió 0", result["probe"])
self.assertGreater(grew(result["before"], result["after"], "TcpExtSyncookiesSent"), 0)
self.assertGreaterEqual(grew(result["before"], result["after"], "TcpExtSyncookiesRecv"), 1)
class DelayedAck(unittest.TestCase):
def test_ack_waits_when_the_application_is_not_reading(self):
scenarios.scenario_delack()
segments = capture("delack", "b")
pending = {}
delays = []
for segment in segments:
if segment.src == CLIENT and segment.len:
pending[segment.seq + segment.len] = segment.time
elif segment.src != CLIENT and segment.ack in pending:
delays.append(segment.time - pending[segment.ack])
self.assertGreater(len(delays), 20)
self.assertLess(delays[0], 0.01) # al comienzo, Linux responde de inmediato
tail = statistics.median(delays[-10:])
self.assertGreater(tail, 0.02)
self.assertLess(tail, 0.2)
self.assertLess(max(delays), 0.5) # el tope del RFC 9293
class HalfClose(unittest.TestCase):
def test_server_answers_after_the_client_fin(self):
scenarios.scenario_halfclose()
segments = capture("halfclose")
client_fin = next(s for s in segments if s.fin and s.src == CLIENT)
answer = next(s for s in from_server(segments) if s.len)
self.assertGreater(answer.frame, client_fin.frame)
self.assertGreater(next(s for s in from_server(segments) if s.fin).frame, answer.frame)
class TimeWait(unittest.TestCase):
def test_the_last_ack_is_repeated_from_time_wait(self):
_, states = scenarios.scenario_timewait()
segments = capture("timewait")
fins = [s for s in from_server(segments) if s.fin]
self.assertEqual(len(fins), 2) # el servidor no recibió el último ACK y repite su FIN
last_acks = [s for s in segments if s.src == CLIENT and not s.fin and not s.syn
and s.seq == 2]
self.assertEqual(len(last_acks), 2)
self.assertGreater(last_acks[1].frame, fins[1].frame)
port = segments[0].sport
match = re.search(rf":{port}\s.*timewait,(\d+)sec", states)
self.assertIsNotNone(match, states)
self.assertLessEqual(int(match.group(1)), 60)
self.assertGreater(int(match.group(1)), 50)
def test_the_side_that_closes_first_keeps_the_state(self):
without, with_reuse = scenarios.scenario_bindfail()
self.assertIn("timewait", without[0])
self.assertIn("Address already in use", without[1])
self.assertIn("timewait", with_reuse[0])
self.assertNotIn("Address already in use", with_reuse[1])
class Resets(unittest.TestCase):
def test_abort_sends_rst_and_the_peer_sees_an_error(self):
output = scenarios.scenario_abort()
self.assertIn("Connection reset by peer", output)
segments = capture("abort")
self.assertTrue(any(s.rst for s in segments))
self.assertFalse(any(s.fin for s in segments))
def test_closed_port_answers_rst(self):
output = scenarios.scenario_closed()
self.assertIn("ECONNREFUSED", output)
segments = capture("closed")
self.assertEqual(len(segments), 2)
self.assertTrue(segments[1].rst and segments[1].has_ack)
class Statistics(unittest.TestCase):
def test_ss_shows_rtt_rto_and_cwnd(self):
rows = scenarios.scenario_ss()
for text in rows:
self.assertRegex(text, r"\brto:\d+")
self.assertRegex(text, r"\bcwnd:\d+")
rtt = float(re.search(r"\brtt:([\d.]+)", text).group(1))
self.assertGreater(rtt, 38) # nunca menos que el retardo del enlace
minimum = float(re.search(r"\bminrtt:([\d.]+)", rows[-1]).group(1))
self.assertLess(minimum, 60)
if __name__ == "__main__":
unittest.main()Laboratorio
Archivos de partida del laboratorio de la semana.
// slow_reader: un servidor TCP que lee poco a poco.
//
// slow_reader <puerto> <rcvbuf> <bytes_por_lectura> <pausa_ms>
//
// Completen los TODO. Cuando ya no quede ninguno, make test pone a prueba este programa.
#define _POSIX_C_SOURCE 200809L
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>
static void pause_ms(long ms) {
struct timespec ts = {ms / 1000, (ms % 1000) * 1000000L};
while (nanosleep(&ts, &ts) < 0) {
if (errno != EINTR) {
perror("nanosleep");
exit(1);
}
}
}
int main(int argc, char **argv) {
if (argc != 5) {
fprintf(stderr, "uso: %s <puerto> <rcvbuf> <bytes_por_lectura> <pausa_ms>\n", argv[0]);
return 2;
}
int rcvbuf = atoi(argv[2]);
size_t chunk = (size_t)atol(argv[3]);
long pause = atol(argv[4]);
char *buf = malloc(chunk);
if (buf == NULL) {
perror("malloc");
return 1;
}
int listener = socket(AF_INET, SOCK_STREAM, 0);
if (listener < 0) {
perror("socket");
return 1;
}
int one = 1;
if (setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one) < 0) {
perror("setsockopt SO_REUSEADDR");
return 1;
}
// TODO 1: fijen SO_RCVBUF con el valor de rcvbuf. Piensen si debe ir antes o después de
// listen: ¿en qué momento se decide la escala de la ventana?
(void)rcvbuf;
struct sockaddr_in addr = {.sin_family = AF_INET,
.sin_port = htons((uint16_t)atoi(argv[1])),
.sin_addr.s_addr = htonl(INADDR_ANY)};
if (bind(listener, (struct sockaddr *)&addr, sizeof addr) < 0) {
perror("bind");
return 1;
}
if (listen(listener, 1) < 0) {
perror("listen");
return 1;
}
int fd = accept(listener, NULL, NULL);
if (fd < 0) {
perror("accept");
return 1;
}
long long total = 0;
// TODO 2: lean hasta que el otro extremo cierre, a lo sumo chunk bytes por vez, con una
// pausa de pause milisegundos después de cada lectura. Sumen a total lo que lean. Revisen
// los tres casos que puede devolver read.
(void)pause_ms;
(void)chunk;
(void)pause;
if (close(fd) < 0 || close(listener) < 0) {
perror("close");
return 1;
}
free(buf);
printf("recibidos %lld bytes\n", total);
return 0;
}Compilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 10: TCP. Se usa dentro del contenedor del curso.
#
# make compila los programas en bin/ (estáticos, para correr en los nodos)
# make test compila, levanta la topología, corre las pruebas (test_*.py) y siempre
# la destruye. La prueba de slow_reader se salta hasta que completen
# lab/slow_reader.c
# make up levanta la topología (tcp/tcp.clab.yml)
# make down destruye la topología y su red de Docker
# make clean lo anterior, y borra los binarios y las capturas
CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -D_DEFAULT_SOURCE -O2 -g -static
PROGRAMS = bin/blast bin/sink bin/connect_probe bin/half_open bin/slow_reader
# La solución no se publica. Sin ella, se compila lo que ustedes escriben en lab/.
SLOW_READER = $(firstword $(wildcard solution/slow_reader.c) lab/slow_reader.c)
.PHONY: all test up down clean
all: $(PROGRAMS) | captures
captures:
mkdir -p captures
bin:
mkdir -p bin
bin/%: %.c | bin
$(CC) $(CFLAGS) -o $@ $<
bin/slow_reader: $(SLOW_READER) | bin
$(CC) $(CFLAGS) -o $@ $<
test: all
@status=0; PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -p 'test_*.py' -v || status=$$?; \
$(MAKE) --no-print-directory down; exit $$status
up: all
cd tcp && containerlab deploy -t tcp.clab.yml --reconfigure
down:
@cd tcp && containerlab destroy -t tcp.clab.yml --cleanup >/dev/null 2>&1 || true
@docker network rm redes-w10-tcp >/dev/null 2>&1 || true
clean: down
rm -rf bin captures __pycache__ tcp/clab-w10-tcp