11
Transporte y aplicaciones

Control de congestión

Transporte y aplicaciones

Semana 11 — Control de congestión

Objetivos

  • Explicar qué es la congestión y cuánto cuesta.
  • Describir slow start, congestion avoidance y fast recovery, y dibujar cwnd en el tiempo.
  • Explicar por qué AIMD converge hacia un reparto justo.
  • Comparar Reno, CUBIC y BBR, y explicar el bufferbloat.
  • Medir el comportamiento real de Reno y CUBIC.

Sesión A — Control de congestión: principios, AIMD y Reno

Lecturas:

Lecturas complementarias:

Temas:

  • Costos de la congestión: colas, retransmisiones inútiles y colapso. Historia: el colapso de 1986.
  • Enfoque end-to-end o con ayuda de la red.
  • La ventana de congestión: enviar min(cwnd, rwnd). Throughput aproximado: cwnd / RTT.
  • Slow start, ssthresh, congestion avoidance y fast recovery. Ejemplo resuelto: cwnd por RTT con una pérdida.
  • Error común: creer que slow start es lento. Es exponencial; se llama así porque antes se empezaba enviando toda la ventana.
  • Por qué AIMD: el diagrama de Chiu y Jain. Reparto justo y eficiente.

Sesión B — CUBIC, BBR, bufferbloat y equidad

Lecturas:

Lecturas complementarias:

Temas:

  • Fórmula del throughput de TCP frente a la pérdida: por qué Reno sufre en enlaces rápidos y largos.
  • CUBIC: la ventana crece según el tiempo y no según el RTT.
  • Bufferbloat: colas enormes que esconden la congestión y suben el retardo.
  • BBR: medir ancho de banda y RTT mínimo en vez de reaccionar a la pérdida. Lo vemos en clase; el kernel de Docker Desktop no siempre lo trae, así que no se mide en el laboratorio.
  • ECN: la red avisa sin botar paquetes.
  • Equidad: flujos con RTT distinto, y varias conexiones paralelas.

Laboratorio — Reno y CUBIC bajo distintos enlaces y buffers

Trabajen en code/week-11/, con dos emisores, un cuello de botella y un receptor.

  1. Limitar el enlace con tc: 50 Mbps y 40 ms de retardo. Medir un solo flujo con iperf3.
  2. Elegir el algoritmo con iperf3 -C reno y iperf3 -C cubic. Registrar cwnd con ss -tin y graficarlo con el script de Python del laboratorio.
  3. Repetir con 1 % y con 0,01 % de pérdida. ¿Cuándo se separan y por qué?
  4. Con un buffer grande y un ping en paralelo, medir cuánto sube el RTT durante la transferencia: es el bufferbloat. Repetir con un buffer pequeño.
  5. Correr dos flujos juntos: mismo algoritmo y luego Reno contra CUBIC. Calcular el índice de equidad de Jain.
  6. Dos flujos con distinto RTT: comprobar cuál gana.
  7. Entregar P3 y leer el enunciado de P4.

Las cifras dependen de cada computadora: comparen tendencias, no valores exactos.

Entregas

  • P3 entrega.
  • P4 publicado: rimay, congestión (control de congestión, recuperación rápida y mediciones). Entrega en semana 13. Ver projects/README.md.

Preguntas de repaso

  1. Con MSS de 1 unidad, ssthresh 8 y sin pérdidas, escriban cwnd en los primeros 8 RTT. Hay tres ACK duplicados cuando cwnd = 12: ¿cómo sigue Reno?
  2. Un enlace de 100 Mbps tiene 50 ms de RTT. ¿Qué cwnd, en paquetes de 1500 bytes, se necesita para llenarlo?
  3. ¿Por qué un buffer más grande puede empeorar una videollamada?
  4. ¿Por qué un flujo con RTT corto le gana a uno con RTT largo en Reno? ¿CUBIC cambia eso?
  5. ¿Por qué BBR no usa la pérdida como señal principal? ¿Qué problema puede traer convivir con flujos que sí la usan?

Código de la semana

Compilar y probar dentro del contenedor del curso con make y make test.

Ejemplos (8)Pruebas (3)Laboratorio (3)Compilación (1)

Ejemplos

Los programas que usan los apuntes y la clase.

bench.pyDescargar bench.py
#!/usr/bin/env python3
"""Mediciones de la semana 11 sobre la topología de dumbbell/.

    python3 bench.py link --rate 50 --delay 40 --loss 0 --buffer 250
    python3 bench.py flow --cc reno --seconds 20 --csv reno.csv
    python3 bench.py bloat --cc cubic --seconds 15
    python3 bench.py pair --cc reno --cc2 cubic --seconds 30
    python3 bench.py pair --cc cubic --parallel2 4 --seconds 30
    python3 bench.py queue

Todo corre dentro de los nodos con docker exec. Las cifras dependen de cada computadora.
"""
import argparse
import json
import re
import statistics
import time

from netlab import RECEIVER, sh, spawn

BASE_PORT = 5201
DATA_PORT = 40001  # puerto local del stream de datos, para no confundirlo con el de control
CWND = re.compile(r"\bcwnd:(\d+)")
SSTHRESH = re.compile(r"\bssthresh:(\d+)")
RTT = re.compile(r"\brtt:([\d.]+)/")


def set_link(rate=50, delay=40, loss=0.0, buffer_kb=250, extra_s1=0, extra_s2=0,
             queue="fifo", ecn=False):
    """Configura el enlace. rate en Mbit/s, delay y extra_* en ms, loss en %, buffer en KiB."""
    # 1: el emisor pide ECN al conectarse. 2: el valor por defecto; lo acepta si se lo piden.
    for node in ("s1", "s2"):
        sh(node, f"sysctl -qw net.ipv4.tcp_ecn={1 if ecn else 2}")
    if queue == "red-ecn":
        # RED marca paquetes con ECN entre min y max, antes de que la cola se llene.
        sh("r1", "tc qdisc del dev eth3 root 2>/dev/null; "
                 f"tc qdisc add dev eth3 root handle 1: tbf rate {rate}mbit burst 15kb "
                 f"limit {buffer_kb}kb && "
                 f"tc qdisc add dev eth3 parent 1:1 handle 10: red limit {buffer_kb}kb "
                 f"min 30kb max 90kb avpkt 1500 burst 30 bandwidth {rate}mbit "
                 "probability 0.1 ecn")
    else:
        sh("r1", "tc qdisc del dev eth3 root 2>/dev/null; "
                 f"tc qdisc add dev eth3 root tbf rate {rate}mbit burst 15kb "
                 f"limit {buffer_kb}kb")
    netem = f"netem delay {delay}ms limit 20000"
    if loss > 0:
        netem += f" loss {loss}%"
    sh("r2", f"tc qdisc replace dev eth2 root {netem}")
    # El retardo extra va en el sentido de los ACK: hacia cada emisor, nunca en su propia
    # salida, donde TCP lo notaría antes de que el paquete salga de la máquina.
    for device, extra in (("eth1", extra_s1), ("eth2", extra_s2)):
        if extra > 0:
            sh("r1", f"tc qdisc replace dev {device} root netem delay {extra}ms limit 20000")
        else:
            sh("r1", f"tc qdisc del dev {device} root 2>/dev/null; true")


def start_servers():
    sh("rx", "pkill iperf3; true", check=False)
    time.sleep(0.2)
    for port in (BASE_PORT, BASE_PORT + 1):
        sh("rx", f"iperf3 -s -D -p {port}")
    time.sleep(0.5)


def stop_servers():
    sh("rx", "pkill iperf3; true", check=False)


def iperf_command(cc, seconds, port=BASE_PORT, cport=DATA_PORT, parallel=1):
    return (f"iperf3 -c {RECEIVER} -p {port} --cport {cport} -t {seconds} -C {cc} "
            f"-P {parallel} -i 1 -J")


def sampler_command(cport, seconds, interval=0.1):
    """Un ciclo dentro del nodo: es la única forma de muestrear cada 100 ms."""
    return (
        'read start rest </proc/uptime; limit=${start%.*}; limit=$((limit+' f"{seconds + 2}" '));'
        ' while :; do read now rest </proc/uptime; [ "${now%.*}" -ge "$limit" ] && break;'
        ' echo "@ $now"; ss -tinH "sport = :' f"{cport}" '";'
        f' sleep {interval}; done')


def parse_ss(text):
    """Convierte la salida del muestreo en filas (t, cwnd, ssthresh o None, rtt en ms)."""
    rows, now, first = [], None, None
    for line in text.splitlines():
        if line.startswith("@ "):
            now = float(line[2:])
            continue
        cwnd, ssthresh, rtt = CWND.search(line), SSTHRESH.search(line), RTT.search(line)
        if cwnd and rtt and now is not None:
            if first is None:
                first = now
            rows.append((round(now - first, 2), int(cwnd.group(1)),
                         int(ssthresh.group(1)) if ssthresh else None, float(rtt.group(1))))
    return rows


def write_csv(path, rows):
    with open(path, "w", encoding="utf-8") as file:
        file.write("t_s,cwnd_segments,ssthresh_segments,rtt_ms\n")
        for time_s, cwnd, ssthresh, rtt in rows:
            file.write(f"{time_s},{cwnd},{'' if ssthresh is None else ssthresh},{rtt}\n")


def summary(report):
    """Saca del JSON de iperf3 lo que nos importa."""
    end = report["end"]
    return {
        "cc": end.get("sender_tcp_congestion", "?"),
        "mbps": end["sum_received"]["bits_per_second"] / 1e6,
        "retrans": end["sum_sent"]["retransmits"],
    }


def run_flow(cc, seconds, node="s1", cport=DATA_PORT, sample=True):
    """Un flujo de iperf3 con su muestreo de ss. Devuelve (resumen, filas)."""
    sampler = spawn(node, sampler_command(cport, seconds)) if sample else None
    time.sleep(0.05)
    output = sh(node, iperf_command(cc, seconds, cport=cport), timeout=seconds + 40)
    rows = []
    if sampler:
        text, _ = sampler.communicate(timeout=30)
        rows = parse_ss(text)
    return summary(json.loads(output)), rows


def run_bloat(cc, seconds):
    """Mide el RTT de un ping antes y durante una transferencia que cruza la misma cola."""
    idle = sh("s2", f"ping -c 10 -i 0.2 {RECEIVER}", timeout=30)
    base = [float(x) for x in re.findall(r"time=([\d.]+)", idle)]
    pinger = spawn("s2", f"sleep 4; ping -c {(seconds - 5) * 5} -i 0.2 {RECEIVER}")
    result, rows = run_flow(cc, seconds)
    text, _ = pinger.communicate(timeout=30)
    during = [float(x) for x in re.findall(r"time=([\d.]+)", text)]
    return {
        "mbps": result["mbps"],
        "retrans": result["retrans"],
        "ping_idle_ms": statistics.median(base),
        "ping_load_ms": statistics.median(during) if during else float("nan"),
        "ping_max_ms": max(during) if during else float("nan"),
        "ping_sent": (seconds - 5) * 5,
        "ping_received": len(during),
        "rows": rows,
    }


def queue_stats():
    """Los contadores de la cola del cuello de botella."""
    return sh("r1", "tc -s qdisc show dev eth3")


def run_pair(cc1, cc2, seconds, parallel2=1):
    """Dos emisores a la vez, s1 y s2. Devuelve los dos resúmenes."""
    first = spawn("s1", iperf_command(cc1, seconds, BASE_PORT, DATA_PORT))
    second = spawn("s2", iperf_command(cc2, seconds, BASE_PORT + 1, DATA_PORT, parallel2))
    out1, _ = first.communicate(timeout=seconds + 40)
    out2, _ = second.communicate(timeout=seconds + 40)
    return summary(json.loads(out1)), summary(json.loads(out2))


def jain(values):
    """Índice de equidad de Jain: (suma x)^2 / (n * suma x^2)."""
    return sum(values) ** 2 / (len(values) * sum(x * x for x in values))


def main():
    parser = argparse.ArgumentParser(description=__doc__,
                                     formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument("what", choices=["link", "flow", "bloat", "pair", "queue"])
    parser.add_argument("--rate", type=int, default=50, help="Mbit/s del cuello de botella")
    parser.add_argument("--delay", type=int, default=40, help="ms de retardo en un sentido")
    parser.add_argument("--loss", type=float, default=0.0, help="%% de pérdida aleatoria")
    parser.add_argument("--buffer", type=int, default=250, help="KiB de cola en el cuello")
    parser.add_argument("--extra-s1", type=int, default=0, help="ms extra de RTT para s1")
    parser.add_argument("--extra-s2", type=int, default=0, help="ms extra de RTT para s2")
    parser.add_argument("--queue", choices=["fifo", "red-ecn"], default="fifo")
    parser.add_argument("--ecn", action="store_true", help="los emisores piden ECN")
    parser.add_argument("--cc", default="cubic", help="algoritmo: reno o cubic")
    parser.add_argument("--cc2", default=None, help="algoritmo del segundo flujo en pair")
    parser.add_argument("--parallel2", type=int, default=1,
                        help="conexiones paralelas del segundo emisor en pair")
    parser.add_argument("--seconds", type=int, default=20)
    parser.add_argument("--csv", default=None, help="archivo para guardar cwnd en el tiempo")
    args = parser.parse_args()

    if args.what == "link":
        set_link(args.rate, args.delay, args.loss, args.buffer, args.extra_s1, args.extra_s2,
                 args.queue, args.ecn)
        print(sh("r1", "tc qdisc show dev eth3"), end="")
        print(sh("r2", "tc qdisc show dev eth2"), end="")
        return
    if args.what == "queue":
        print(queue_stats(), end="")
        return

    start_servers()
    try:
        if args.what == "flow":
            result, rows = run_flow(args.cc, args.seconds)
            print(f"{result['cc']}: {result['mbps']:.1f} Mbit/s, "
                  f"{result['retrans']} retransmisiones")
            if args.csv:
                write_csv(args.csv, rows)
                print(f"{len(rows)} muestras en {args.csv}")
        elif args.what == "bloat":
            found = run_bloat(args.cc, args.seconds)
            print(f"{args.cc}: {found['mbps']:.1f} Mbit/s, {found['retrans']} retransmisiones")
            print(f"ping sin carga: {found['ping_idle_ms']:.1f} ms; con carga: mediana "
                  f"{found['ping_load_ms']:.1f} ms, máximo {found['ping_max_ms']:.1f} ms; "
                  f"recibidos {found['ping_received']} de {found['ping_sent']}")
        else:
            one, two = run_pair(args.cc, args.cc2 or args.cc, args.seconds, args.parallel2)
            print(f"s1 {one['cc']}: {one['mbps']:.1f} Mbit/s, {one['retrans']} retransmisiones")
            print(f"s2 {two['cc']}: {two['mbps']:.1f} Mbit/s, {two['retrans']} retransmisiones")
            print(f"Jain: {jain([one['mbps'], two['mbps']]):.3f}")
    finally:
        stop_servers()


if __name__ == "__main__":
    main()
cubic_curve.pyDescargar cubic_curve.py
#!/usr/bin/env python3
"""La curva de CUBIC según el RFC 9438: W(t) = C (t - K)^3 + Wmax, frente a Reno.

Solo evalúa la fórmula con C = 0,4 y beta = 0,7, sin la región amigable con Reno ni la
convergencia rápida. Compara cuánto tarda cada uno en volver a Wmax después de una pérdida.

    python3 cubic_curve.py --wmax 167 --rtt 40
    python3 cubic_curve.py --recovery
"""
import argparse

C = 0.4
BETA = 0.7


def k_seconds(wmax):
    """Tiempo que tarda W(t) en volver a Wmax, con la ventana recién reducida a BETA * Wmax."""
    return (wmax * (1 - BETA) / C) ** (1 / 3)


def window(t, wmax):
    return C * (t - k_seconds(wmax)) ** 3 + wmax


def reno_recovery_seconds(wmax, rtt_ms):
    """Reno baja a Wmax / 2 y sube un paquete por RTT."""
    return (wmax / 2) * rtt_ms / 1000


def packets_for(rate_mbps, rtt_ms, packet=1500):
    return rate_mbps * 1e6 * (rtt_ms / 1000) / (8 * packet)


def recovery_table():
    print(f"{'enlace':>10} {'RTT':>7} {'BDP (paq.)':>11} {'Reno':>12} {'CUBIC':>10}")
    for rate, rtt in ((50, 40), (50, 200), (1000, 40), (1000, 200), (10000, 100)):
        wmax = packets_for(rate, rtt)
        print(f"{rate:>5} Mbps {rtt:>4} ms {wmax:>11.0f} "
              f"{reno_recovery_seconds(wmax, rtt):>10.1f} s {k_seconds(wmax):>8.1f} s")


def main():
    parser = argparse.ArgumentParser(description=__doc__,
                                     formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument("--wmax", type=float, default=167)
    parser.add_argument("--rtt", type=float, default=40)
    parser.add_argument("--recovery", action="store_true")
    args = parser.parse_args()
    if args.recovery:
        recovery_table()
        return
    k = k_seconds(args.wmax)
    print(f"Wmax = {args.wmax:.0f} paquetes, K = {k:.2f} s")
    print(f"{'t (s)':>6} {'W cubic':>8} {'W reno':>8}")
    reno_slope = 1000 / args.rtt  # paquetes por segundo
    for tenth in range(0, int(k * 2.2 * 2) + 1):
        t = tenth / 2
        reno = args.wmax / 2 + reno_slope * t
        print(f"{t:>6.1f} {window(t, args.wmax):>8.0f} {reno:>8.0f}")


if __name__ == "__main__":
    main()
netlab.pyDescargar netlab.py
"""Ayudas de la semana 11: 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__))
FOLDER = os.path.join(HERE, "dumbbell")
TOPOLOGY = "dumbbell.clab.yml"
PREFIX = "clab-w11-dumbbell-"
NETWORK = "redes-w11"
RECEIVER = "10.211.4.10"


def deploy():
    done = subprocess.run(["containerlab", "deploy", "-t", TOPOLOGY, "--reconfigure"],
                          cwd=FOLDER, capture_output=True, text=True, timeout=300)
    if done.returncode != 0:
        raise RuntimeError(done.stdout + done.stderr)


def destroy():
    subprocess.run(["containerlab", "destroy", "-t", TOPOLOGY, "--cleanup"],
                   cwd=FOLDER, capture_output=True, text=True, timeout=300)
    subprocess.run(["docker", "network", "rm", NETWORK], capture_output=True, text=True)


def sh(node, command, timeout=120, check=True):
    """Corre un comando de shell en un nodo y devuelve su stdout."""
    done = subprocess.run(["docker", "exec", PREFIX + node, "sh", "-c", command],
                          capture_output=True, text=True, timeout=timeout)
    if check and done.returncode != 0:
        raise RuntimeError(f"{node}: {command}\n{done.stdout}{done.stderr}")
    return done.stdout


def spawn(node, command):
    """Arranca un comando en un nodo sin esperarlo; su stdout queda en un pipe."""
    return subprocess.Popen(["docker", "exec", PREFIX + node, "sh", "-c", command],
                            stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)


def wait_until(predicate, seconds=30, step=0.5):
    """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()


def load_metrics():
    """Devuelve lab/metrics.py (o la solución, si existe); salta si sigue con el TODO."""
    import importlib.util
    folder = os.path.join(HERE, "solution")
    if not os.path.isdir(folder):
        folder = os.path.join(HERE, "lab")
    path = os.path.join(folder, "metrics.py")
    with open(path, encoding="utf-8") as file:
        if "TODO" in file.read():
            raise unittest.SkipTest("falta completar lab/metrics.py")
    spec = importlib.util.spec_from_file_location("metrics", path)
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module
phase.pyDescargar phase.py
#!/usr/bin/env python3
"""El modelo de Chiu y Jain: dos flujos, un enlace y una señal común de congestión.

En cada paso ambos reciben la misma señal: si x1 + x2 supera la capacidad, bajan; si no,
suben. Se comparan las cuatro combinaciones de aumento y reducción.

    python3 phase.py aimd --x1 10 --x2 70 --steps 12
    python3 phase.py all --png fases.png
"""
import argparse

RULES = {
    # nombre: (aumento, reducción)
    "aimd": (lambda x: x + 5, lambda x: x * 0.5),
    "aiad": (lambda x: x + 5, lambda x: max(x - 20, 0)),
    "mimd": (lambda x: x * 1.25, lambda x: x * 0.5),
    "miad": (lambda x: x * 1.25, lambda x: max(x - 20, 0)),
}


def walk(rule, x1, x2, capacity=100, steps=200):
    """Devuelve la lista de puntos (x1, x2) por los que pasa el sistema."""
    increase, decrease = RULES[rule]
    points = [(x1, x2)]
    for _ in range(steps):
        change = decrease if x1 + x2 > capacity else increase
        x1, x2 = change(x1), change(x2)
        points.append((x1, x2))
    return points


def jain(x1, x2):
    if x1 == 0 and x2 == 0:
        return 0.0
    return (x1 + x2) ** 2 / (2 * (x1 * x1 + x2 * x2))


def plot(path, x1, x2, capacity):
    import matplotlib
    matplotlib.use("Agg")
    import matplotlib.pyplot as plt
    figure, axes = plt.subplots(1, 4, figsize=(14, 3.6), sharex=True, sharey=True)
    for ax, rule in zip(axes, RULES):
        points = walk(rule, x1, x2, capacity, steps=60)
        ax.plot([0, capacity], [capacity, 0], color="gray", label="eficiencia")
        ax.plot([0, capacity], [0, capacity], color="gray", linestyle="--", label="equidad")
        ax.plot(*zip(*points), marker=".", linewidth=0.7)
        ax.set_title(rule.upper())
        ax.set_xlim(0, capacity * 1.3)
        ax.set_ylim(0, capacity * 1.3)
        ax.set_xlabel("x1")
    axes[0].set_ylabel("x2")
    axes[0].legend()
    figure.tight_layout()
    figure.savefig(path, dpi=150)


def main():
    parser = argparse.ArgumentParser(description=__doc__,
                                     formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument("rule", choices=[*RULES, "all"])
    parser.add_argument("--x1", type=float, default=10)
    parser.add_argument("--x2", type=float, default=70)
    parser.add_argument("--capacity", type=float, default=100)
    parser.add_argument("--steps", type=int, default=12)
    parser.add_argument("--png", default=None)
    args = parser.parse_args()
    if args.png:
        plot(args.png, args.x1, args.x2, args.capacity)
        print(f"gráfico en {args.png}")
        return
    if args.rule == "all":
        print(f"{'regla':<6} {'x1':>7} {'x2':>7} {'suma':>7} {'|x1-x2|':>8} {'Jain':>6}")
        for rule in RULES:
            # El último punto: el sistema oscila alrededor de la capacidad, pero x1/x2 se ve bien.
            last = walk(rule, args.x1, args.x2, args.capacity)[-1]
            print(f"{rule:<6} {last[0]:>7.1f} {last[1]:>7.1f} {last[0] + last[1]:>7.1f} "
                  f"{abs(last[0] - last[1]):>8.1f} {jain(*last):>6.3f}")
        return
    print(f"{'paso':>4} {'x1':>7} {'x2':>7} {'suma':>7} {'|x1-x2|':>8} {'Jain':>6}")
    for number, (x1, x2) in enumerate(walk(args.rule, args.x1, args.x2, args.capacity,
                                           args.steps)):
        print(f"{number:>4} {x1:>7.1f} {x2:>7.1f} {x1 + x2:>7.1f} {abs(x1 - x2):>8.1f} "
              f"{jain(x1, x2):>6.3f}")


if __name__ == "__main__":
    main()
plot_cwnd.pyDescargar plot_cwnd.py
#!/usr/bin/env python3
"""Grafica cwnd en el tiempo a partir de los CSV que deja bench.py flow --csv.

    python3 plot_cwnd.py cwnd.png reno.csv cubic.csv
"""
import csv
import os
import sys

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt  # noqa: E402


def read_rows(path):
    with open(path, encoding="utf-8") as file:
        return list(csv.DictReader(file))


def main():
    if len(sys.argv) < 3:
        print(__doc__)
        return 1
    target, *sources = sys.argv[1:]
    figure, (top, bottom) = plt.subplots(2, 1, sharex=True, figsize=(9, 5.5))
    for source in sources:
        rows = read_rows(source)
        if not rows:
            print(f"{source}: no tiene muestras", file=sys.stderr)
            return 1
        times = [float(row["t_s"]) for row in rows]
        label = os.path.splitext(os.path.basename(source))[0]
        top.plot(times, [int(row["cwnd_segments"]) for row in rows], label=label)
        bottom.plot(times, [float(row["rtt_ms"]) for row in rows], label=label)
    top.set_ylabel("cwnd (segmentos)")
    bottom.set_ylabel("RTT suavizado (ms)")
    bottom.set_xlabel("tiempo (s)")
    top.legend()
    figure.tight_layout()
    figure.savefig(target, dpi=150)
    print(f"gráfico en {target}")
    return 0


if __name__ == "__main__":
    sys.exit(main())
reno_trace.pyDescargar reno_trace.py
#!/usr/bin/env python3
"""cwnd de Reno, una fila por RTT, con las reglas del libro y del RFC 5681.

Es un modelo para hacer la cuenta en papel: cuenta en paquetes y por RTT. No tiene ACK, ni
temporizadores, ni datos; no es un transporte.

    python3 reno_trace.py --ssthresh 8 --rounds 12 --dup-at-cwnd 12
    python3 reno_trace.py --ssthresh 16 --rounds 26 --dup-at-round 9 --timeout-at-round 17
"""
import argparse


def trace(rounds, ssthresh, initial=1, dup_rounds=(), timeout_rounds=(), dup_at_cwnd=None):
    """Devuelve filas (ronda, cwnd, ssthresh, fase, evento al final de esa ronda)."""
    rows = []
    cwnd = initial
    fired = False
    for number in range(1, rounds + 1):
        phase = "slow start" if cwnd < ssthresh else "congestion avoidance"
        dup = number in dup_rounds
        if dup_at_cwnd is not None and cwnd == dup_at_cwnd and not fired:
            dup, fired = True, True
        timeout = number in timeout_rounds
        event = "3 ACK dup." if dup else ("timeout" if timeout else "")
        rows.append((number, cwnd, ssthresh, phase, event))
        if dup or timeout:
            ssthresh = max(cwnd // 2, 2)
            cwnd = ssthresh if dup else 1
        elif cwnd < ssthresh:
            cwnd = min(cwnd * 2, ssthresh)
        else:
            cwnd += 1
    return rows


def main():
    parser = argparse.ArgumentParser(description=__doc__,
                                     formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument("--rounds", type=int, default=20)
    parser.add_argument("--ssthresh", type=int, default=8)
    parser.add_argument("--initial", type=int, default=1)
    parser.add_argument("--dup-at-round", type=int, action="append", default=[])
    parser.add_argument("--timeout-at-round", type=int, action="append", default=[])
    parser.add_argument("--dup-at-cwnd", type=int, default=None)
    args = parser.parse_args()
    print(f"{'RTT':>3} {'cwnd':>5} {'ssthresh':>8}  {'fase':<21} evento")
    for number, cwnd, ssthresh, phase, event in trace(
            args.rounds, args.ssthresh, args.initial, args.dup_at_round,
            args.timeout_at_round, args.dup_at_cwnd):
        print(f"{number:>3} {cwnd:>5} {ssthresh:>8}  {phase:<21} {event}".rstrip())


if __name__ == "__main__":
    main()
tcp_info.cDescargar tcp_info.c
// Pide al kernel el estado de control de congestión de un socket TCP mientras envía.
//
//   ./tcp_info recv <puerto>
//   ./tcp_info send <ip> <puerto> <reno|cubic> <segundos> [ms entre muestras]
//
// El control de congestión lo hace el kernel: este programa solo lo elige con
// TCP_CONGESTION y lo observa con TCP_INFO, que es de donde saca `ss` sus números.
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <poll.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>

#define BUFFER_SIZE 65536
#define NO_SSTHRESH 0x7fffffffu  // el kernel usa este valor mientras no hubo ninguna pérdida

static double now_ms(void) {
    struct timespec ts;
    if (clock_gettime(CLOCK_MONOTONIC, &ts) < 0) {
        perror("clock_gettime");
        exit(EXIT_FAILURE);
    }
    return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1e6;
}

static const char *state_name(uint8_t state) {
    // Los nombres del kernel: enum tcp_ca_state en include/uapi/linux/tcp.h.
    switch (state) {
    case 0: return "open";
    case 1: return "disorder";
    case 2: return "cwr";
    case 3: return "recovery";
    case 4: return "loss";
    default: return "?";
    }
}

static int print_sample(int fd, double elapsed) {
    struct tcp_info info;
    socklen_t size = sizeof info;
    if (getsockopt(fd, IPPROTO_TCP, TCP_INFO, &info, &size) < 0) {
        perror("getsockopt TCP_INFO");
        return -1;
    }
    char ssthresh[16] = "-";
    if (info.tcpi_snd_ssthresh < NO_SSTHRESH) {
        snprintf(ssthresh, sizeof ssthresh, "%u", info.tcpi_snd_ssthresh);
    }
    printf("%8.1f %6u %8s %8.1f %7u %s\n", elapsed, info.tcpi_snd_cwnd, ssthresh,
           info.tcpi_rtt / 1000.0, info.tcpi_total_retrans, state_name(info.tcpi_ca_state));
    return 0;
}

static int run_sender(const char *ip, int port, const char *algorithm, int seconds,
                      int interval_ms) {
    struct sockaddr_in address = {.sin_family = AF_INET, .sin_port = htons((uint16_t)port)};
    if (inet_pton(AF_INET, ip, &address.sin_addr) != 1) {
        fprintf(stderr, "dirección no válida: %s\n", ip);
        return -1;
    }
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd < 0) {
        perror("socket");
        return -1;
    }
    // Se elige antes de conectar, para que también valga durante el slow start.
    if (setsockopt(fd, IPPROTO_TCP, TCP_CONGESTION, algorithm, (socklen_t)strlen(algorithm)) < 0) {
        perror("setsockopt TCP_CONGESTION");
        close(fd);
        return -1;
    }
    if (connect(fd, (struct sockaddr *)&address, sizeof address) < 0) {
        perror("connect");
        close(fd);
        return -1;
    }
    char in_use[16] = "";
    socklen_t in_use_size = sizeof in_use;
    if (getsockopt(fd, IPPROTO_TCP, TCP_CONGESTION, in_use, &in_use_size) < 0) {
        perror("getsockopt TCP_CONGESTION");
        close(fd);
        return -1;
    }
    printf("# algoritmo: %s\n", in_use);
    printf("# %6s %6s %8s %8s %7s %s\n", "t_ms", "cwnd", "ssthresh", "rtt_ms", "retrans", "estado");

    static char buffer[BUFFER_SIZE];
    double start = now_ms();
    double next_sample = start;
    double end = start + seconds * 1000.0;
    int status = 0;
    for (;;) {
        double now = now_ms();
        if (now >= end) {
            break;
        }
        if (now >= next_sample) {
            if (print_sample(fd, now - start) < 0) {
                status = -1;
                break;
            }
            next_sample += interval_ms;
        }
        // Esperamos con poll, sin bloquear en send: la muestra debe salir a su hora
        // aunque la ventana esté llena.
        struct pollfd ready = {.fd = fd, .events = POLLOUT};
        int wait = (int)(next_sample - now) + 1;
        int events = poll(&ready, 1, wait);
        if (events < 0) {
            if (errno == EINTR) {
                continue;
            }
            perror("poll");
            status = -1;
            break;
        }
        if (events == 0) {
            continue;
        }
        if (send(fd, buffer, sizeof buffer, MSG_DONTWAIT) < 0
            && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) {
            perror("send");
            status = -1;
            break;
        }
    }
    if (close(fd) < 0) {
        perror("close");
        return -1;
    }
    return status;
}

static int run_receiver(int port) {
    int listener = socket(AF_INET, SOCK_STREAM, 0);
    if (listener < 0) {
        perror("socket");
        return -1;
    }
    int yes = 1;
    if (setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes) < 0) {
        perror("setsockopt SO_REUSEADDR");
        close(listener);
        return -1;
    }
    struct sockaddr_in address = {.sin_family = AF_INET, .sin_port = htons((uint16_t)port),
                                  .sin_addr.s_addr = htonl(INADDR_ANY)};
    if (bind(listener, (struct sockaddr *)&address, sizeof address) < 0) {
        perror("bind");
        close(listener);
        return -1;
    }
    if (listen(listener, 1) < 0) {
        perror("listen");
        close(listener);
        return -1;
    }
    int fd = accept(listener, NULL, NULL);
    if (fd < 0) {
        perror("accept");
        close(listener);
        return -1;
    }
    static char buffer[BUFFER_SIZE];
    unsigned long long total = 0;
    int status = 0;
    for (;;) {
        ssize_t got = recv(fd, buffer, sizeof buffer, 0);
        if (got < 0) {
            if (errno == EINTR) {
                continue;
            }
            perror("recv");
            status = -1;
            break;
        }
        if (got == 0) {
            break;
        }
        total += (unsigned long long)got;
    }
    printf("recibidos %llu bytes\n", total);
    if (close(fd) < 0 || close(listener) < 0) {
        perror("close");
        return -1;
    }
    return status;
}

int main(int argc, char *argv[]) {
    if (argc == 3 && strcmp(argv[1], "recv") == 0) {
        return run_receiver(atoi(argv[2])) == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
    }
    if ((argc == 6 || argc == 7) && strcmp(argv[1], "send") == 0) {
        int interval_ms = argc == 7 ? atoi(argv[6]) : 100;
        if (interval_ms <= 0) {
            fprintf(stderr, "el intervalo debe ser positivo\n");
            return EXIT_FAILURE;
        }
        return run_sender(argv[2], atoi(argv[3]), argv[4], atoi(argv[5]), interval_ms) == 0
                   ? EXIT_SUCCESS : EXIT_FAILURE;
    }
    fprintf(stderr, "uso: %s recv <puerto>\n"
                    "     %s send <ip> <puerto> <reno|cubic> <segundos> [ms entre muestras]\n",
            argv[0], argv[0]);
    return EXIT_FAILURE;
}
dumbbell/dumbbell.clab.ymlDescargar dumbbell/dumbbell.clab.yml
# Dos emisores, un cuello de botella y un receptor.
#
#   s1 --\
#         r1 ==== cuello de botella ==== r2 --- rx
#   s2 --/
#
# La cola se forma en la salida de r1 hacia r2 (eth3). El retardo y la pérdida se aplican en
# la salida de r2 hacia rx (eth2). Las reglas de tc las pone bench.py link.
name: w11-dumbbell
mgmt:
  network: redes-w11
  ipv4-subnet: 172.90.11.0/26
  ipv6-subnet: 3fff:172:90:11::/64
topology:
  defaults:
    kind: linux
    image: redes-router
    binds: [..:/lab]
  nodes:
    s1:
      exec:
        - ip route del default
        - sysctl -w net.ipv6.conf.all.disable_ipv6=1
        - sysctl -w net.ipv4.tcp_no_metrics_save=1
        - ip addr add 10.211.1.10/24 dev eth1
        - ip route add default via 10.211.1.1
    s2:
      exec:
        - ip route del default
        - sysctl -w net.ipv6.conf.all.disable_ipv6=1
        - sysctl -w net.ipv4.tcp_no_metrics_save=1
        - ip addr add 10.211.2.10/24 dev eth1
        - ip route add default via 10.211.2.1
    r1:
      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.211.1.1/24 dev eth1
        - ip addr add 10.211.2.1/24 dev eth2
        - ip addr add 10.211.3.1/24 dev eth3
        - ip route add 10.211.4.0/24 via 10.211.3.2
    r2:
      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.211.3.2/24 dev eth1
        - ip addr add 10.211.4.1/24 dev eth2
        - ip route add 10.211.1.0/24 via 10.211.3.1
        - ip route add 10.211.2.0/24 via 10.211.3.1
    rx:
      exec:
        - ip route del default
        - sysctl -w net.ipv6.conf.all.disable_ipv6=1
        - ip addr add 10.211.4.10/24 dev eth1
        - ip route add default via 10.211.4.1
  links:
    - endpoints: ["s1:eth1", "r1:eth1"]
      mtu: 1500
    - endpoints: ["s2:eth1", "r1:eth2"]
      mtu: 1500
    - endpoints: ["r1:eth3", "r2:eth1"]
      mtu: 1500
    - endpoints: ["r2:eth2", "rx:eth1"]
      mtu: 1500

Pruebas

Comprueban lo que afirman los apuntes; se ejecutan con make test.

test_dumbbell.pyDescargar test_dumbbell.py
"""Las afirmaciones de los apuntes, medidas de verdad sobre dumbbell/dumbbell.clab.yml.

Todo mide tendencias con márgenes amplios: las cifras cambian según la computadora y
según lo que esté corriendo en ella.
"""
import csv
import os
import re
import statistics
import tempfile
import time
import unittest

import bench
import netlab
from netlab import RECEIVER, sh


def setUpModule():
    unittest.addModuleCleanup(netlab.destroy)
    netlab.deploy()
    if not netlab.wait_until(lambda: "bytes from" in sh("s1", f"ping -c 1 -W 2 {RECEIVER}", check=False)
                             and "bytes from" in sh("s2", f"ping -c 1 -W 2 {RECEIVER}", check=False),
                             seconds=60):
        raise RuntimeError("la topología no quedó lista")


def ratios(rows):
    """ssthresh nuevo sobre el cwnd más alto desde el evento anterior."""
    found, last, peak = [], None, 0
    for _, cwnd, ssthresh, _ in rows:
        peak = max(peak, cwnd)
        if ssthresh is not None and ssthresh != last:
            found.append(ssthresh / peak)
            last, peak = ssthresh, cwnd
    return found


class Measured(unittest.TestCase):
    def setUp(self):
        bench.start_servers()
        self.addCleanup(bench.stop_servers)

    def test_kernel_offers_reno_and_cubic(self):
        offered = sh("s1", "cat /proc/sys/net/ipv4/tcp_available_congestion_control").split()
        self.assertIn("reno", offered)
        self.assertIn("cubic", offered)

    def test_single_flow_fills_the_link_and_shows_its_decrease(self):
        bench.set_link()
        for cc, low, high in (("reno", 0.30, 0.62), ("cubic", 0.60, 0.85)):
            result, rows = bench.run_flow(cc, 20)
            self.assertEqual(result["cc"], cc)
            self.assertGreater(result["mbps"], 30, cc)
            self.assertLess(result["mbps"], 52, cc)
            self.assertGreater(len(rows), 50, cc)
            self.assertGreater(min(row[3] for row in rows), 38, "el RTT no puede bajar de 40 ms")
            found = ratios(rows)
            self.assertGreater(len(found), 1, f"{cc}: casi no se vieron reducciones")
            self.assertGreater(statistics.median(found), low, f"{cc}: {found}")
            self.assertLess(statistics.median(found), high, f"{cc}: {found}")

    def test_plot_script_makes_a_png(self):
        bench.set_link()
        _, rows = bench.run_flow("cubic", 3)
        with tempfile.TemporaryDirectory() as folder:
            source = os.path.join(folder, "cubic.csv")
            target = os.path.join(folder, "cwnd.png")
            bench.write_csv(source, rows)
            with open(source, encoding="utf-8") as file:
                self.assertEqual(next(csv.reader(file))[1], "cwnd_segments")
            done = os.system(f"python3 {netlab.HERE}/plot_cwnd.py {target} {source} >/dev/null")
            self.assertEqual(done, 0)
            self.assertGreater(os.path.getsize(target), 1000)

    def test_tcp_info_shows_exponential_slow_start(self):
        bench.set_link()
        receiver = netlab.spawn("rx", "/lab/tcp_info recv 6000")
        time.sleep(0.5)
        output = sh("s1", f"/lab/tcp_info send {RECEIVER} 6000 reno 2 10", timeout=30)
        receiver.communicate(timeout=20)
        self.assertIn("algoritmo: reno", output)
        rows = [line.split() for line in output.splitlines() if not line.startswith("#")]
        self.assertEqual(rows[0][1], "10", "Linux empieza con 10 segmentos")
        self.assertEqual(rows[0][2], "-", "todavía no hay ssthresh")
        # Antes del primer RTT no ha vuelto ningún ACK de datos: la ventana sigue en 10.
        early = [int(row[1]) for row in rows if float(row[0]) < 30]
        self.assertTrue(early and all(value == 10 for value in early), early)
        # Una muestra puede caer a mitad de un RTT, así que no se exigen valores exactos. A los
        # 300 ms, unos 7 RTT, sumar uno por RTT daría 17; doblar da cientos.
        later = [int(row[1]) for row in rows if float(row[0]) >= 300]
        self.assertGreater(later[0], 80, "slow start debe crecer de forma exponencial")
        self.assertTrue(any(row[2] != "-" for row in rows), "nunca hubo pérdida")
        self.assertTrue(any(row[5] == "recovery" for row in rows), "nunca entró a recovery")

    def test_random_loss_hurts_and_matches_the_formula(self):
        bench.set_link(loss=1)
        for cc in ("reno", "cubic"):
            result, _ = bench.run_flow(cc, 15, sample=False)
            self.assertLess(result["mbps"], 15, f"{cc}: con 1 % de pérdida ya no llena el enlace")
            if cc == "reno":
                expected = 1.22 * 1448 * 8 / (0.040 * 0.1) / 1e6
                self.assertGreater(result["mbps"], expected / 3)
                self.assertLess(result["mbps"], expected * 3)

    def test_large_buffer_means_large_delay(self):
        bench.set_link(buffer_kb=2000)
        large = bench.run_bloat("reno", 20)
        bench.set_link(buffer_kb=50)
        small = bench.run_bloat("reno", 20)
        for found in (large, small):
            self.assertLess(found["ping_idle_ms"], 60)
            self.assertGreater(found["ping_received"], 0.6 * found["ping_sent"])
        self.assertGreater(large["ping_load_ms"], large["ping_idle_ms"] + 80)
        self.assertLess(small["ping_load_ms"], small["ping_idle_ms"] + 40)
        self.assertGreater(large["ping_load_ms"], small["ping_load_ms"] * 2)

    def test_same_algorithm_shares_the_link(self):
        bench.set_link()
        one, two = bench.run_pair("reno", "reno", 30)
        self.assertGreater(bench.jain([one["mbps"], two["mbps"]]), 0.7, (one, two))
        self.assertGreater(one["mbps"] + two["mbps"], 30)

    def test_short_rtt_beats_long_rtt_with_reno(self):
        bench.set_link(extra_s2=80)
        short, long = bench.run_pair("reno", "reno", 30)
        self.assertGreater(short["mbps"], 1.5 * long["mbps"], (short, long))

    def test_parallel_connections_take_a_larger_share(self):
        bench.set_link()
        one, many = bench.run_pair("cubic", "cubic", 30, parallel2=4)
        self.assertGreater(many["mbps"], 1.3 * one["mbps"], (one, many))

    def counters(self):
        stats = bench.queue_stats()
        marked = re.search(r"marked (\d+) early (\d+) pdrop (\d+)", stats)
        self.assertIsNotNone(marked, stats)
        return [int(value) for value in marked.groups()]

    def test_ecn_marks_where_red_would_drop(self):
        bench.set_link(queue="red-ecn", buffer_kb=1000, ecn=True)
        self.addCleanup(bench.set_link)
        bench.run_flow("cubic", 12, sample=False)
        marked, early, pdrop = self.counters()
        self.assertGreater(marked, 0, "con ECN, RED marca")
        self.assertEqual(early, 0, "con ECN, RED no bota por anticipado")

        bench.set_link(queue="red-ecn", buffer_kb=1000, ecn=False)
        bench.run_flow("cubic", 12, sample=False)
        marked, early, pdrop = self.counters()
        self.assertEqual(marked, 0, "sin ECN, no hay a quién marcar")
        self.assertGreater(early + pdrop, 0, "sin ECN, la misma cola bota")


class Compiled(unittest.TestCase):
    def test_compiles_without_warnings(self):
        with tempfile.TemporaryDirectory() as folder:
            target = os.path.join(folder, "tcp_info")
            source = os.path.join(netlab.HERE, "tcp_info.c")
            done = os.system(f"gcc -Wall -Wextra -Werror -std=c17 -D_DEFAULT_SOURCE -o {target} {source}")
            self.assertEqual(done, 0)


if __name__ == "__main__":
    unittest.main()
test_lab.pyDescargar test_lab.py
"""Pruebas de lab/metrics.py, que se saltan mientras quede algún TODO."""
import csv
import os
import statistics
import unittest

from netlab import HERE, load_metrics


def read_trace(name):
    rows = []
    with open(os.path.join(HERE, "lab", name), encoding="utf-8") as file:
        for row in csv.DictReader(file):
            ssthresh = int(row["ssthresh_segments"]) if row["ssthresh_segments"] else None
            rows.append((float(row["t_s"]), int(row["cwnd_segments"]), ssthresh,
                         float(row["rtt_ms"])))
    return rows


class Metrics(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.metrics = load_metrics()

    def test_bdp(self):
        self.assertAlmostEqual(self.metrics.bdp_packets(50, 40), 166.67, places=1)
        self.assertAlmostEqual(self.metrics.bdp_packets(100, 50), 416.67, places=1)
        self.assertAlmostEqual(self.metrics.bdp_packets(100, 50, packet_bytes=1000), 625, places=1)

    def test_queue_delay(self):
        self.assertAlmostEqual(self.metrics.queue_delay_ms(250_000, 50), 40, places=3)
        self.assertAlmostEqual(self.metrics.queue_delay_ms(1_000_000, 8), 1000, places=3)

    def test_mathis(self):
        self.assertAlmostEqual(self.metrics.mathis_mbps(1448, 40, 0.01), 3.55, places=1)
        # Cuadruplicar la pérdida reduce el throughput a la mitad, y lo mismo duplicar el RTT.
        self.assertAlmostEqual(self.metrics.mathis_mbps(1448, 40, 0.04),
                               self.metrics.mathis_mbps(1448, 80, 0.01), places=6)

    def test_decrease_ratios_on_a_small_trace(self):
        rows = [(0, 10, None, 40), (1, 100, None, 40), (2, 50, 50, 40), (3, 80, 50, 40),
                (4, 40, 40, 40), (5, 41, 40, 40)]
        self.assertEqual(self.metrics.decrease_ratios(rows), [0.5, 0.5])

    def test_the_sample_of_the_event_counts_for_the_peak(self):
        rows = [(0, 10, None, 40), (1, 100, 60, 40)]
        self.assertEqual(self.metrics.decrease_ratios(rows), [0.6])

    def test_no_events_no_ratios(self):
        self.assertEqual(self.metrics.decrease_ratios([(0, 10, None, 40), (1, 20, None, 40)]), [])

    def test_real_traces_show_the_two_factors(self):
        reno = self.metrics.decrease_ratios(read_trace("trace-reno.csv"))
        cubic = self.metrics.decrease_ratios(read_trace("trace-cubic.csv"))
        self.assertGreater(len(reno), 2)
        self.assertGreater(len(cubic), 2)
        self.assertLess(statistics.median(reno), 0.62)
        self.assertGreater(statistics.median(cubic), 0.62)


if __name__ == "__main__":
    unittest.main()
test_models.pyDescargar test_models.py
"""Pruebas de los modelos en papel de los apuntes: no levantan ninguna red."""
import unittest

import cubic_curve
import phase
import reno_trace


def windows(rows):
    return [row[1] for row in rows]


class RenoTrace(unittest.TestCase):
    def test_worked_example_of_the_notes(self):
        rows = reno_trace.trace(26, 16, dup_rounds=[10], timeout_rounds=[19])
        self.assertEqual(windows(rows)[:10], [1, 2, 4, 8, 16, 17, 18, 19, 20, 21])
        # Tres ACK duplicados en cwnd = 21: ssthresh baja a 10 y se sigue en esa ventana.
        self.assertEqual(rows[10][1:3], (10, 10))
        self.assertEqual(windows(rows)[10:19], [10, 11, 12, 13, 14, 15, 16, 17, 18])
        # Timeout en cwnd = 18: ssthresh baja a 9 y se vuelve a un paquete.
        self.assertEqual(rows[19][1:3], (1, 9))
        self.assertEqual(windows(rows)[19:], [1, 2, 4, 8, 9, 10, 11])

    def test_review_question_of_the_guide(self):
        rows = reno_trace.trace(10, 8, dup_at_cwnd=12)
        self.assertEqual(windows(rows)[:8], [1, 2, 4, 8, 9, 10, 11, 12])
        self.assertEqual(rows[8][1:3], (6, 6))
        self.assertEqual(rows[9][1], 7)

    def test_slow_start_does_not_pass_ssthresh(self):
        self.assertEqual(windows(reno_trace.trace(4, 6))[:4], [1, 2, 4, 6])

    def test_ssthresh_has_a_floor_of_two(self):
        rows = reno_trace.trace(3, 8, timeout_rounds=[1])
        self.assertEqual(rows[1][1:3], (1, 2))


class ChiuJain(unittest.TestCase):
    START = (10.0, 70.0)

    def test_only_aimd_converges_to_fair(self):
        x1, x2 = phase.walk("aimd", *self.START)[-1]
        self.assertAlmostEqual(x1, x2, places=3)
        self.assertGreater(phase.jain(x1, x2), 0.999)

    def test_aiad_keeps_the_gap(self):
        for x1, x2 in phase.walk("aiad", *self.START)[:3]:
            self.assertAlmostEqual(abs(x1 - x2), 60)

    def test_mimd_keeps_the_ratio(self):
        for x1, x2 in phase.walk("mimd", *self.START):
            self.assertAlmostEqual(x2 / x1, 7)

    def test_miad_starves_one_flow(self):
        x1, x2 = phase.walk("miad", *self.START)[-1]
        self.assertEqual(x1, 0)
        self.assertGreater(x2, 0)

    def test_all_stay_around_capacity(self):
        for rule in phase.RULES:
            totals = [x1 + x2 for x1, x2 in phase.walk(rule, *self.START)[100:]]
            self.assertGreater(max(totals), 100, rule)
            self.assertLess(min(totals), 100, rule)


class CubicCurve(unittest.TestCase):
    def test_starts_at_beta_and_returns_at_k(self):
        wmax = 1000
        self.assertAlmostEqual(cubic_curve.window(0, wmax), 0.7 * wmax)
        self.assertAlmostEqual(cubic_curve.window(cubic_curve.k_seconds(wmax), wmax), wmax)

    def test_slows_down_near_wmax(self):
        wmax, k = 1000, cubic_curve.k_seconds(1000)
        early = cubic_curve.window(1, wmax) - cubic_curve.window(0, wmax)
        late = cubic_curve.window(k, wmax) - cubic_curve.window(k - 1, wmax)
        self.assertGreater(early, 5 * late)

    def test_k_does_not_depend_on_rtt_but_reno_does(self):
        self.assertAlmostEqual(cubic_curve.reno_recovery_seconds(1000, 200),
                               5 * cubic_curve.reno_recovery_seconds(1000, 40))
        self.assertLess(cubic_curve.k_seconds(83333), 45)
        self.assertGreater(cubic_curve.reno_recovery_seconds(83333, 100), 3600)

    def test_bdp_of_the_review_question(self):
        self.assertAlmostEqual(cubic_curve.packets_for(100, 50), 416.67, places=1)


if __name__ == "__main__":
    unittest.main()

Laboratorio

Archivos de partida del laboratorio de la semana.

lab/metrics.pyDescargar lab/metrics.py
"""Laboratorio de la semana 11: las cuentas con las que se leen las mediciones.

Completen cada función y borren los TODO. `make test` prueba este archivo cuando ya no
queda ninguno. Las unidades están en cada docstring: la mayoría de los errores aquí son de
unidades (bits y bytes, ms y s, % y fracción).
"""


def bdp_packets(rate_mbps, rtt_ms, packet_bytes=1500):
    """Producto ancho de banda por retardo, en paquetes de packet_bytes bytes."""
    # TODO
    raise NotImplementedError


def queue_delay_ms(buffer_bytes, rate_mbps):
    """Retardo que agrega un buffer lleno de buffer_bytes bytes a un enlace de rate_mbps."""
    # TODO
    raise NotImplementedError


def mathis_mbps(mss_bytes, rtt_ms, loss_fraction):
    """Throughput de Reno según la fórmula de la pérdida, en Mbit/s.

    loss_fraction es una fracción: 1 % se escribe 0.01.
    """
    # TODO
    raise NotImplementedError


def decrease_ratios(rows):
    """Cuánto reduce la ventana el algoritmo en cada evento de congestión.

    rows es una lista de filas (t, cwnd, ssthresh, rtt), como las del CSV de bench.py, donde
    ssthresh es None mientras no ha habido pérdidas. Cada vez que ssthresh cambia de valor
    hubo un evento: devuelvan ssthresh_nuevo / cwnd_máximo, donde cwnd_máximo es el cwnd más
    alto visto desde el evento anterior (o desde el inicio), contando la fila actual. Al
    pasar un evento, el máximo vuelve a empezar desde el cwnd de esa fila.
    """
    # TODO
    raise NotImplementedError
lab/trace-cubic.csvDescargar lab/trace-cubic.csv
t_s,cwnd_segments,ssthresh_segments,rtt_ms
0.0,20,,40.688
0.11,80,,44.872
0.21,405,,50.263
0.32,483,454,82.205
0.43,402,454,79.456
0.54,317,317,78.772
0.65,318,317,76.971
0.75,321,317,77.24
0.86,324,317,78.299
0.96,328,317,79.318
1.06,331,317,80.347
1.17,334,317,80.638
1.27,337,317,81.202
1.38,236,236,75.468
1.48,238,236,57.273
1.58,241,236,58.278
1.69,244,236,59.084
1.79,247,236,60.405
1.9,250,236,60.083
2.0,252,236,61.209
2.1,254,236,62.114
2.21,257,236,62.485
2.31,259,236,63.099
2.42,261,236,63.698
2.52,263,236,63.97
2.62,265,236,64.573
2.73,267,236,64.967
2.83,268,236,64.999
2.94,270,236,65.496
3.04,271,236,65.952
3.15,273,236,65.865
3.25,275,236,66.895
3.35,276,236,67.207
3.46,277,236,67.281
3.56,278,236,67.918
3.67,279,236,68.473
3.77,280,236,68.488
3.88,281,236,68.236
3.98,282,236,68.461
4.08,282,236,69.33
4.19,283,236,68.79
4.29,284,236,69.182
4.4,284,236,69.517
4.5,285,236,68.964
4.6,285,236,69.266
4.71,285,236,69.223
4.81,286,236,70.394
4.92,286,236,70.237
5.02,286,236,70.06
5.12,287,236,69.751
5.23,287,236,69.944
5.34,287,236,70.688
5.44,287,236,70.34
5.54,287,236,70.456
5.65,287,236,70.256
5.75,287,236,69.995
5.86,287,236,70.076
5.96,287,236,70.025
6.06,287,236,70.068
6.17,287,236,70.256
6.27,287,236,70.049
6.37,287,236,69.76
6.48,287,236,70.216
6.58,287,236,69.995
6.69,287,236,70.328
6.79,287,236,70.206
6.9,287,236,69.867
7.0,287,236,70.059
7.11,287,236,70.317
7.21,287,236,70.098
7.31,287,236,69.89
7.42,287,236,70.128
7.52,287,236,70.209
7.63,287,236,70.126
7.73,287,236,69.838
7.83,288,236,70.598
7.94,288,236,70.425
8.04,288,236,70.355
8.15,289,236,69.679
8.25,290,236,70.524
8.35,291,236,70.49
8.46,291,236,71.603
8.56,292,236,71.144
8.67,293,236,71.278
8.77,294,236,71.452
8.88,294,236,71.842
8.98,295,236,71.717
9.08,296,236,72.417
9.19,297,236,72.726
9.29,297,236,72.005
9.39,298,236,73.131
9.5,299,236,72.951
9.6,300,236,73.196
9.71,301,236,73.386
9.81,302,236,72.83
9.92,304,236,73.962
10.02,305,236,73.925
10.12,307,236,74.26
10.23,308,236,74.893
10.33,310,236,75.483
10.44,312,236,75.264
10.54,314,236,75.918
10.64,316,236,76.468
10.75,318,236,77.109
10.85,320,236,77.892
10.95,323,236,77.926
11.06,326,236,79.267
11.16,328,236,78.81
11.27,331,236,80.347
11.37,334,236,80.545
11.48,337,236,81.234
11.58,236,236,80.665
11.68,239,236,56.928
11.79,243,236,58.272
11.89,248,236,60.133
11.99,252,236,60.874
12.1,257,236,62.032
12.2,261,236,62.716
12.31,265,236,64.003
12.41,269,236,65.181
12.52,273,236,65.943
12.62,276,236,66.652
12.72,280,236,67.589
12.83,283,236,68.219
12.93,286,236,69.044
13.03,289,236,69.437
13.14,292,236,70.942
13.24,296,236,71.019
13.35,299,236,72.298
13.45,301,236,73.271
13.55,304,236,73.769
13.66,306,236,74.164
13.76,308,236,75.198
13.87,310,236,75.304
13.97,312,236,75.611
14.07,314,236,76.366
14.18,316,236,77.39
14.29,318,236,76.91
14.39,320,236,77.238
14.5,321,236,77.954
14.61,323,236,78.678
14.71,324,236,79.192
14.81,326,236,79.019
14.92,327,236,79.657
15.02,328,236,79.206
15.12,330,236,79.478
15.23,331,236,80.252
15.33,331,236,91.427
15.44,231,231,61.613
15.54,234,231,56.773
15.65,237,231,57.223
15.75,240,231,58.333
15.86,243,231,58.931
15.96,245,231,59.383
16.07,248,231,59.555
16.17,250,231,60.54
16.28,252,231,62.314
16.39,254,231,65.061
16.49,256,231,74.176
16.59,180,180,46.536
16.7,182,180,44.241
16.8,185,180,44.884
16.91,188,180,45.135
17.01,190,180,46.132
17.12,192,180,46.667
17.22,194,180,47.044
17.32,197,180,47.791
17.43,198,180,47.863
17.53,200,180,48.789
17.64,202,180,49.249
17.74,203,180,49.29
17.84,205,180,50.044
17.95,206,180,50.204
18.05,207,180,50.181
18.16,209,180,56.62
18.26,210,180,51.583
18.37,211,180,51.238
18.48,211,180,51.39
18.58,213,180,52.076
18.69,213,180,51.871
18.79,214,180,52.368
18.91,215,180,52.115
19.01,215,180,51.711
19.12,216,180,52.927
19.23,216,180,52.578
19.33,217,180,53.266
19.43,217,180,53.172
19.54,218,180,53.1
19.64,218,180,53.202
19.75,218,180,52.945
19.85,219,180,52.916
19.95,219,180,53.368
20.06,219,180,53.665
20.16,219,180,53.536
20.26,219,180,53.059
20.37,219,180,53.173
20.47,220,180,53.916
20.58,221,180,53.516
20.68,222,180,54.225
20.78,223,180,53.944
20.89,224,180,54.799
20.99,225,180,55.116
21.09,226,180,55.234
21.2,227,180,55.061
21.3,228,180,55.913
21.41,229,180,55.979
21.51,230,180,56.275
21.61,231,180,56.428
21.72,232,180,56.874
21.82,233,180,56.896
21.92,234,180,56.742
22.03,235,180,56.988
22.13,236,180,57.382
22.23,237,180,57.966
22.34,238,180,57.843
22.44,239,180,57.735
22.55,240,180,58.218
22.65,241,180,58.278
22.75,242,180,59.276
22.86,243,180,59.102
22.96,244,180,59.097
23.06,245,180,59.174
23.17,246,180,59.673
23.27,246,180,60.042
23.37,247,180,60.406
23.48,249,180,60.033
23.58,249,180,60.876
23.69,250,180,60.705
23.79,251,180,61.312
23.89,252,180,61.153
24.0,253,180,61.365
24.1,254,180,61.901
24.2,255,180,61.942
24.31,256,180,62.02
24.42,257,180,62.161
24.52,258,180,62.59
24.63,258,180,63.605
24.74,259,180,69.803
24.85,260,180,63.953
24.96,261,180,63.622
25.06,262,180,64.02
25.17,263,180,63.2
25.27,264,180,64.356
25.38,265,180,64.154
25.48,265,180,64.382
25.59,266,180,65.313
25.69,267,180,65.367
25.8,268,180,64.895
25.9,269,180,65.416
26.0,270,180,66.032
26.11,271,180,65.774
26.22,272,180,66.471
26.32,273,180,66.461
26.42,275,180,66.562
26.53,278,180,67.289
26.64,281,180,68.223
26.75,285,180,69.104
26.86,289,180,70.307
26.96,293,180,71.007
27.06,298,180,71.766
27.17,302,180,73.27
27.27,306,180,73.648
27.38,311,180,74.921
27.48,316,180,75.621
27.59,320,180,76.94
27.69,326,180,77.726
27.79,331,180,79.511
27.9,336,180,81.189
28.0,280,237,82.577
28.11,239,237,58.396
28.21,244,237,58.97
28.32,249,237,60.339
28.43,253,237,61.284
28.54,258,237,62.563
28.65,262,237,63.834
28.75,266,237,63.781
28.86,270,237,64.899
28.96,274,237,66.209
29.06,277,237,67.305
29.17,281,237,68.368
29.28,285,237,69.206
29.38,288,237,69.56
29.49,291,237,70.275
29.59,294,237,71.106
29.7,297,237,71.842
29.8,300,237,72.499
29.9,303,237,73.047
30.01,305,237,73.492
lab/trace-reno.csvDescargar lab/trace-reno.csv
t_s,cwnd_segments,ssthresh_segments,rtt_ms
0.0,20,,40.662
0.11,78,,45.078
0.21,342,,44.099
0.32,633,358,81.357
0.42,337,358,82.516
0.52,320,179,82.565
0.63,179,179,42.178
0.73,181,179,43.457
0.84,183,179,44.524
0.94,186,179,45.284
1.04,188,179,45.85
1.15,190,179,46.494
1.25,193,179,46.882
1.36,195,179,46.653
1.46,197,179,47.384
1.56,199,179,48.135
1.67,201,179,49.173
1.77,203,179,49.206
1.87,205,179,50.143
1.98,208,179,49.977
2.08,210,179,51.116
2.19,212,179,51.257
2.29,214,179,51.934
2.39,216,179,52.515
2.5,218,179,53.021
2.6,220,179,52.782
2.71,222,179,54.06
2.81,224,179,54.543
2.92,226,179,55.059
3.03,228,179,56.825
3.14,229,179,60.831
3.24,231,179,56.103
3.35,233,179,57.295
3.46,235,179,57.343
3.56,237,179,57.711
3.67,239,179,58.378
3.78,241,179,58.502
3.88,242,179,59.353
3.99,244,179,59.288
4.1,246,179,59.97
4.21,248,179,59.731
4.31,249,179,60.619
4.41,251,179,61.506
4.52,253,179,61.32
4.63,255,179,61.897
4.74,256,179,61.725
4.84,258,179,62.867
4.95,260,179,63.263
5.05,262,179,63.646
5.16,263,179,64.04
5.27,265,179,64.477
5.37,266,179,64.438
5.47,268,179,65.092
5.58,270,179,64.862
5.68,271,179,65.778
5.79,273,179,65.992
5.89,274,179,66.275
5.99,276,179,67.342
6.1,277,179,66.919
6.2,279,179,67.971
6.31,281,179,67.898
6.41,282,179,68.127
6.51,284,179,68.823
6.62,285,179,68.937
6.72,287,179,69.781
6.82,288,179,70.057
6.93,290,179,70.197
7.03,291,179,70.934
7.14,293,179,71.052
7.24,294,179,71.313
7.34,295,179,71.795
7.45,297,179,72.419
7.55,298,179,72.419
7.66,300,179,72.995
7.76,301,179,73.501
7.86,303,179,73.541
7.97,304,179,74.121
8.07,305,179,73.794
8.18,307,179,73.875
8.28,308,179,74.697
8.38,310,179,75.171
8.49,311,179,75.597
8.59,312,179,75.539
8.69,314,179,76.278
8.8,315,179,76.496
8.91,317,179,76.961
9.01,318,179,76.499
9.12,319,179,77.581
9.22,321,179,78.127
9.33,322,179,78.435
9.43,323,179,78.244
9.54,325,179,78.388
9.64,326,179,79.357
9.75,327,179,79.332
9.85,329,179,79.665
9.95,330,179,79.862
10.06,331,179,79.914
10.16,332,179,80.757
10.27,334,179,81.391
10.37,335,179,81.226
10.48,220,168,82.691
10.58,169,168,41.858
10.68,172,168,42.058
10.79,174,168,41.849
10.89,177,168,42.737
11.0,179,168,43.657
11.1,182,168,44.426
11.2,184,168,45.175
11.31,186,168,45.072
11.41,189,168,46.095
11.52,191,168,46.195
11.62,193,168,47.187
11.72,195,168,47.062
11.83,197,168,47.694
11.93,200,168,48.599
12.03,202,168,48.761
12.14,204,168,49.752
12.24,206,168,50.416
12.35,208,168,50.921
12.45,210,168,50.918
12.55,212,168,51.644
12.66,214,168,52.321
12.76,216,168,52.359
12.87,218,168,53.174
12.97,220,168,53.39
13.07,222,168,54.388
13.18,224,168,54.539
13.28,226,168,55.524
13.38,228,168,55.029
13.49,230,168,56.101
13.59,232,168,55.929
13.7,233,168,56.662
13.8,235,168,57.19
13.9,237,168,57.033
14.01,239,168,57.787
14.11,241,168,58.068
14.22,242,168,59.049
14.32,244,168,59.178
14.42,246,168,60.2
14.53,248,168,59.979
14.63,249,168,60.5
14.74,251,168,61.548
14.84,253,168,61.247
14.94,254,168,62.106
15.05,256,168,62.163
15.15,258,168,62.57
15.26,259,168,63.279
15.36,261,168,63.258
15.47,263,168,63.725
15.57,264,168,63.669
15.68,266,168,64.931
15.78,268,168,65.386
15.88,269,168,65.314
15.99,271,168,65.822
16.09,272,168,66.05
16.2,274,168,66.23
16.3,276,168,67.457
16.4,277,168,66.827
16.51,279,168,67.943
16.61,280,168,68.38
16.72,282,168,68.611
16.82,283,168,68.976
16.92,285,168,69.286
17.03,286,168,69.349
17.13,288,168,69.929
17.24,289,168,70.283
17.34,291,168,70.556
17.44,292,168,71.12
17.55,294,168,71.476
17.65,295,168,71.65
17.75,297,168,72.467
17.86,298,168,71.947
17.96,299,168,72.46
18.07,301,168,72.859
18.17,302,168,73.153
18.27,304,168,74.122
18.38,305,168,74.334
18.48,307,168,74.269
18.59,308,168,75.537
18.69,309,168,74.935
18.79,311,168,75.017
18.9,312,168,75.939
19.0,313,168,76.395
19.1,315,168,76.553
19.21,316,168,77.076
19.31,318,168,76.715
19.42,319,168,77.728
19.52,320,168,78.283
19.62,322,168,77.508
19.73,323,168,78.373
19.83,324,168,78.688
19.94,326,168,78.795
20.04,327,168,79.332
20.15,328,168,79.715
20.25,329,168,79.799
20.36,331,168,79.89
20.46,332,168,81.075
20.56,333,168,80.969
20.67,335,168,81.386
20.77,309,168,82.217
20.88,168,168,56.047
20.98,170,168,41.668
21.08,173,168,42.12
21.19,175,168,42.742
21.29,177,168,43.2
21.39,180,168,43.711
21.5,182,168,43.974
21.6,185,168,45.094
21.71,187,168,45.388
21.81,189,168,45.921
21.91,191,168,46.326
22.02,194,168,46.962
22.12,196,168,47.303
22.23,198,168,48.301
22.33,200,168,48.608
22.44,202,168,48.93
22.54,204,168,49.377
22.64,207,168,49.985
22.75,209,168,50.585
22.85,211,168,50.768
22.95,213,168,51.865
23.06,215,168,52.295
23.16,217,168,53.06
23.26,219,168,52.579
23.37,221,168,53.644
23.47,222,168,53.744
23.58,224,168,53.904
23.68,226,168,54.607
23.78,228,168,55.071
23.89,230,168,56.277
23.99,232,168,55.994
24.1,234,168,57.216
24.2,236,168,56.902
24.3,237,168,57.691
24.41,239,168,58.432
24.51,241,168,57.891
24.62,243,168,58.788
24.72,245,168,59.55
24.82,246,168,60.028
24.93,248,168,60.461
25.03,250,168,60.557
25.13,251,168,61.267
25.24,253,168,61.55
25.34,255,168,61.631
25.45,257,168,62.115
25.55,258,168,62.507
25.66,260,168,63.067
25.76,261,168,63.496
25.86,263,168,63.346
25.97,265,168,63.7
26.07,266,168,64.093
26.18,268,168,65.251
26.28,270,168,65.064
26.38,271,168,65.532
26.49,273,168,66.457
26.6,274,168,66.447
26.7,276,168,67.302
26.8,278,168,66.761
26.91,279,168,67.945
27.01,281,168,68.324
27.11,282,168,68.354
27.22,284,168,68.861
27.32,285,168,68.889
27.43,287,168,69.697
27.53,288,168,69.948
27.63,290,168,71.382
27.74,291,168,71.066
27.84,293,168,71.443
27.95,294,168,71.076
28.05,295,168,71.664
28.15,297,168,72.437
28.26,298,168,72.115
28.36,300,168,72.742
28.47,301,168,72.784
28.57,303,168,74.031
28.67,304,168,73.83
28.78,305,168,73.961
28.88,307,168,74.431
28.99,308,168,75.431
29.09,310,168,75.452
29.2,311,168,75.174
29.3,312,168,75.732
29.41,314,168,76.264
29.51,315,168,76.801
29.61,317,168,77.148
29.72,318,168,76.736
29.82,319,168,77.323
29.93,321,168,77.979
30.03,322,168,78.275

Compilación

Cómo se compilan y ejecutan los ejemplos.

MakefileDescargar Makefile
# Semana 11: control de congestión. Se usa dentro del contenedor del curso.
#
#   make              compila tcp_info (estático: los nodos de la topología usan otra libc)
#   make test         compila, levanta la topología, corre las pruebas (test_*.py) y siempre
#                     destruye lo que levantó. Las pruebas de lab/metrics.py se saltan hasta
#                     que lo completen. Toma unos minutos: mide de verdad
#   make up           levanta la topología (dumbbell/dumbbell.clab.yml)
#   make down         destruye la topología y su red de Docker
#   make clean        lo anterior, y borra el binario, los CSV y los gráficos

CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -D_DEFAULT_SOURCE -O2 -g

.PHONY: all test up down clean

all: tcp_info

tcp_info: tcp_info.c
	$(CC) $(CFLAGS) -static -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 dumbbell && containerlab deploy -t dumbbell.clab.yml --reconfigure

down:
	@cd dumbbell && containerlab destroy -t dumbbell.clab.yml --cleanup >/dev/null 2>&1 || true
	@docker network rm redes-w11 >/dev/null 2>&1 || true

clean: down
	rm -f tcp_info *.csv *.png
	rm -rf __pycache__ lab/__pycache__ solution/__pycache__ dumbbell/clab-w11-dumbbell