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.

  1. Capturar una transferencia sin pérdidas. Encontrar el handshake, el cierre y la opción MSS.
  2. En Wireshark, usar Statistics → TCP Stream Graphs → Time Sequence y explicar la forma de la curva.
  3. Agregar 2 % de pérdida y repetir. Encontrar ACK duplicados y fast retransmit. Para ver un timeout, corten el enlace por completo (escenario rto).
  4. Hacer un receptor lento en C que lea poco a poco. Encontrar la ventana cero en la captura.
  5. Probar a abrir conexiones hacia un puerto cerrado y hacia un puerto filtrado. ¿Qué devuelve connect en cada caso?
  6. Mirar con ss -tin los valores de rtt, rto y cwnd de una conexión activa.
  7. Avanzar con P3.

Algunas actividades se basan en el laboratorio de TCP de Kurose y Ross.

Preguntas de repaso

  1. Un segmento lleva seq=1000 y 500 bytes. ¿Qué ACK espera el emisor? ¿Y si antes se perdió el segmento anterior?
  2. 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.
  3. ¿Por qué el handshake usa números de secuencia iniciales que no son cero?
  4. ¿Qué hace el emisor cuando la ventana anunciada es cero y qué evita que la conexión se trabe para siempre?
  5. ¿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.cDescargar blast.c
// 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.cDescargar connect_probe.c
// 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.cDescargar half_open.c
// 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;
}
netlab.pyDescargar netlab.py
"""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()
pcaptools.pyDescargar pcaptools.py
"""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
rto_table.pyDescargar rto_table.py
"""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())
scenarios.pyDescargar scenarios.py
"""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.cDescargar sink.c
// 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;
}
tcpsum.pyDescargar tcpsum.py
"""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())
timeseq.pyDescargar timeseq.py
"""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())
tcp/tcp.clab.ymlDescargar tcp/tcp.clab.yml
# 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.

test_estimator.pyDescargar test_estimator.py
"""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()
test_lab.pyDescargar test_lab.py
"""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()
test_tcp.pyDescargar test_tcp.py
"""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.

lab/slow_reader.cDescargar lab/slow_reader.c
// 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.

MakefileDescargar Makefile
# 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