Semana 9 — Entrega confiable

Objetivos

  • Explicar qué agrega el transporte sobre IP: multiplexación entre procesos.
  • Describir UDP y calcular su checksum.
  • Construir paso a paso un protocolo confiable sobre un canal con errores y pérdidas.
  • Comparar Go-Back-N y Selective Repeat, y calcular su utilización.
  • Conectar la red de toda la clase en el hackathon.

Sesión A — Transporte: multiplexación, UDP y entrega confiable

Lecturas:

  • Kurose y Ross, secs. 3.1 (Introduction and Transport-Layer Services), 3.2 (Multiplexing and Demultiplexing), 3.3 (Connectionless Transport: UDP) y 3.4.1 (Building a Reliable Data Transfer Protocol).
  • CS168, Transport Layer Principles.

Lecturas complementarias:

Temas:

  • IP entrega entre hosts y el transporte entrega entre procesos. Puertos y demultiplexación.
  • Diferencia entre UDP y TCP al demultiplexar: dos campos frente a cuatro.
  • UDP: encabezado de 8 bytes y checksum. Cuándo conviene: DNS, medios en vivo y como base para QUIC.
  • Construir un protocolo paso a paso: canal perfecto, con errores de bits (ACK y NAK, números de secuencia), y con pérdidas (temporizador).
  • Stop-and-wait: utilización en un enlace largo. Ejemplo: 1 Gbps con 30 ms de RTT.
  • Error común: creer que dos generales pueden coordinarse con certeza sobre un canal con pérdidas. Lo que se logra es confiabilidad con un costo, no certeza instantánea.

Sesión B — Ventana deslizante: Go-Back-N y Selective Repeat

Lecturas:

  • Kurose y Ross, secs. 3.4.2 (Pipelined Reliable Data Transfer Protocols), 3.4.3 (Go-Back-N) y 3.4.4 (Selective Repeat).
  • CS168, Transport Layer Principles, solo como marco general: el detalle de las ventanas está en el libro.

Temas:

  • Pipelining: enviar varios paquetes sin esperar. El tamaño de ventana y el producto ancho de banda × retardo.
  • Go-Back-N: ACK acumulativo, un temporizador y retransmitir todo desde el perdido.
  • Selective Repeat: ACK individuales, temporizador por paquete y buffer en el receptor.
  • Comparación con pérdidas altas y bajas. Ejemplos con un diagrama de tiempo.
  • Tamaño de ventana y espacio de números de secuencia: en Selective Repeat, la ventana no puede pasar de la mitad del espacio. Ejemplo del error con cuatro números.

Laboratorio — Hackathon: conectar el Internet de la clase

Cada equipo trae su sistema autónomo de P2. Su configuración se integra en una sola topología, en la computadora del docente.

  1. Cada equipo entrega su carpeta as/ antes de iniciar la sesión.
  2. El docente levanta la topología completa. Las sesiones BGP se levantan solas a partir de as/vecinos.env, y los equipos entran a sus routers a revisar y depurar las que no suban.
  3. Objetivo: que todos los sistemas autónomos se alcancen, con caminos que respeten las políticas. Un tablero muestra quién alcanza a quién.
  4. Diagnosticar lo que falla con show bgp summary, show bgp ipv4 y traceroute.
  5. El docente corta un enlace de tránsito. Cada equipo comprueba si sigue conectado.
  6. Discusión final: qué falló y cuánto de eso era técnico y cuánto era coordinación entre equipos.
  7. Entregar P2 y leer el enunciado de P3.

Entregas

  • P2 entrega.
  • P3 publicado: rimay, entrega confiable (protocolo con ventana deslizante sobre UDP). Entrega en semana 11. Ver projects/README.md.

Preguntas de repaso

  1. Dos clientes hablan con el mismo puerto de un servidor. ¿Cómo separa el sistema operativo esos datos en UDP? ¿Y en TCP?
  2. Calculen la utilización de stop-and-wait con paquetes de 1500 bytes, un enlace de 100 Mbps y 40 ms de RTT. ¿Qué ventana se necesita para llenar el enlace?
  3. ¿Para qué se necesitan números de secuencia si ya existe el temporizador?
  4. En Go-Back-N con ventana 4, se pierde el paquete 2 de la secuencia 0 a 5. Dibujen qué se envía y qué ACK se recibe.
  5. ¿Por qué Selective Repeat falla con 4 números de secuencia y ventana 3? Muestren la ambigüedad.

Código de la semana

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

Ejemplos (21)Pruebas (3)Laboratorio (4)Compilación (1)

Ejemplos

Los programas que usan los apuntes y la clase.

demux_client.cDescargar demux_client.c
// Cliente de prueba: manda un mensaje desde un puerto de origen elegido y espera la respuesta.
//   demux_client <tcp|udp|udp-connect> <ip> <puerto> <puerto_origen> <mensaje> [espera_s]
// Con espera_s, la conexión se queda abierta ese tiempo después de la respuesta.
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <unistd.h>

#define REPLY_WAIT_S 2

int main(int argc, char *argv[]) {
    if (argc != 6 && argc != 7) {
        fprintf(stderr, "uso: %s <tcp|udp|udp-connect> <ip> <puerto> <puerto_origen> <mensaje> [espera_s]\n",
                argv[0]);
        return EXIT_FAILURE;
    }
    int is_tcp = strcmp(argv[1], "tcp") == 0;
    int is_connected = is_tcp || strcmp(argv[1], "udp-connect") == 0;
    if (!is_tcp && !is_connected && strcmp(argv[1], "udp") != 0) {
        fprintf(stderr, "protocolo no válido: %s\n", argv[1]);
        return EXIT_FAILURE;
    }
    int port = atoi(argv[3]);
    int source_port = atoi(argv[4]);
    if (port <= 0 || port > 65535 || source_port <= 0 || source_port > 65535) {
        fprintf(stderr, "puerto fuera de rango\n");
        return EXIT_FAILURE;
    }
    struct sockaddr_in remote;
    memset(&remote, 0, sizeof remote);
    remote.sin_family = AF_INET;
    remote.sin_port = htons((uint16_t)port);
    if (inet_pton(AF_INET, argv[2], &remote.sin_addr) != 1) {
        fprintf(stderr, "dirección no válida: %s\n", argv[2]);
        return EXIT_FAILURE;
    }

    int fd = socket(AF_INET, is_tcp ? SOCK_STREAM : SOCK_DGRAM, 0);
    if (fd == -1) {
        perror("socket");
        return EXIT_FAILURE;
    }
    int yes = 1;
    if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes) == -1) {
        perror("setsockopt");
        return EXIT_FAILURE;
    }
    struct timeval wait = {.tv_sec = REPLY_WAIT_S};
    if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &wait, sizeof wait) == -1) {
        perror("setsockopt");
        return EXIT_FAILURE;
    }
    struct sockaddr_in local;
    memset(&local, 0, sizeof local);
    local.sin_family = AF_INET;
    local.sin_addr.s_addr = htonl(INADDR_ANY);
    local.sin_port = htons((uint16_t)source_port);
    if (bind(fd, (struct sockaddr *)&local, sizeof local) == -1) {
        perror("bind");
        return EXIT_FAILURE;
    }
    if (is_connected && connect(fd, (struct sockaddr *)&remote, sizeof remote) == -1) {
        perror("connect");
        return EXIT_FAILURE;
    }
    size_t length = strlen(argv[5]);
    ssize_t sent = is_connected
        ? send(fd, argv[5], length, 0)
        : sendto(fd, argv[5], length, 0, (struct sockaddr *)&remote, sizeof remote);
    if (sent == -1) {
        perror("send");
        return EXIT_FAILURE;
    }
    char reply[256];
    ssize_t got = recv(fd, reply, sizeof reply - 1, 0);
    if (got == -1) {
        perror("recv");
        return EXIT_FAILURE;
    }
    reply[got] = '\0';
    printf("%s\n", reply);
    if (fflush(stdout) == EOF) {
        perror("fflush");
    }
    if (argc == 7) {
        unsigned rest = (unsigned)atoi(argv[6]);
        while (rest > 0) {
            rest = sleep(rest);
        }
    }
    if (close(fd) == -1) {
        perror("close");
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}
ensayo.pyDescargar ensayo.py
#!/usr/bin/env python3
"""Los pasos del ensayo: cargar sus arreglos, cortar enlaces y volverlos a subir.

    python3 ensayo.py arreglos          carga lab/*.conf en los routers
    python3 ensayo.py corta <a> <b>     baja el enlace entre dos nodos
    python3 ensayo.py sube <a> <b>      lo vuelve a subir
    python3 ensayo.py enlaces           lista los enlaces

Cortar con `ip link set down` hace que los dos extremos se enteren al instante: BGP cierra
la sesión sin esperar a su hold timer. Es lo que se ve cuando se desconecta un cable.
"""
import os
import subprocess
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
ANSWERS = os.path.join(HERE, "solution")
if not os.path.isdir(ANSWERS):
    ANSWERS = os.path.join(HERE, "lab")
PREFIX = "clab-w09-ensayo-"
FIXED_ROUTERS = ["e1", "e2", "e3", "t2"]
LINKS = {
    ("t1", "t2"): ("eth1", "eth1"),
    ("t1", "e1"): ("eth2", "eth1"),
    ("t1", "e2"): ("eth3", "eth1"),
    ("t1", "e3"): ("eth4", "eth1"),
    ("t2", "e2"): ("eth2", "eth2"),
    ("t2", "e3"): ("eth3", "eth2"),
    ("t2", "e4"): ("eth4", "eth1"),
    ("e3", "e4"): ("eth3", "eth2"),
}


def run(*args):
    done = subprocess.run(args, capture_output=True, text=True, timeout=60)
    if done.returncode != 0:
        raise SystemExit(f"falló {' '.join(args)}:\n{done.stdout}{done.stderr}")
    return done.stdout


def set_link(a, b, state):
    if (a, b) in LINKS:
        interface_a, interface_b = LINKS[a, b]
    elif (b, a) in LINKS:
        interface_b, interface_a = LINKS[b, a]
    else:
        raise SystemExit(f"no hay un enlace entre {a} y {b}: python3 ensayo.py enlaces")
    run("docker", "exec", PREFIX + a, "ip", "link", "set", interface_a, state)
    run("docker", "exec", PREFIX + b, "ip", "link", "set", interface_b, state)


def answer_is_ready(router):
    with open(os.path.join(ANSWERS, f"{router}.conf"), encoding="utf-8") as file:
        return "TODO" not in file.read()


def load_fixes():
    for router in FIXED_ROUTERS:
        if not answer_is_ready(router):
            print(f"{router}: lab/{router}.conf todavía tiene su TODO, no lo cargo")
            continue
        path = os.path.join(ANSWERS, f"{router}.conf")
        run("docker", "cp", path, f"{PREFIX}{router}:/tmp/arreglo.conf")
        run("docker", "exec", PREFIX + router, "vtysh", "-f", "/tmp/arreglo.conf")
        # Un filtro de entrada cambiado no se aplica solo a lo que ya se recibió.
        run("docker", "exec", PREFIX + router, "vtysh", "-c", "clear bgp ipv4 unicast * soft in")
        print(f"{router}: arreglo cargado")


def main():
    if len(sys.argv) == 2 and sys.argv[1] == "arreglos":
        load_fixes()
    elif len(sys.argv) == 2 and sys.argv[1] == "enlaces":
        for a, b in LINKS:
            print(f"{a} {b}")
    elif len(sys.argv) == 4 and sys.argv[1] in ("corta", "sube"):
        set_link(sys.argv[2], sys.argv[3], "down" if sys.argv[1] == "corta" else "up")
    else:
        print(__doc__)


if __name__ == "__main__":
    main()
netlab.pyDescargar netlab.py
"""Ayudas de las pruebas de la semana 9: levantar topologías y correr comandos."""
import os
import subprocess
import time
import unittest

HERE = os.path.dirname(os.path.abspath(__file__))
# 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


def run(args, timeout=240, stdin=None):
    return subprocess.run(args, capture_output=True, text=True, timeout=timeout, stdin=stdin)


class Lab:
    """Una topología de Containerlab que se levanta y se destruye desde las pruebas."""

    def __init__(self, folder, name, network):
        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=60):
        """Corre un comando en un nodo y devuelve el resultado completo."""
        return run(["docker", "exec", self.prefix + node, *command], timeout=timeout)

    def out(self, node, *command, timeout=60):
        """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."""
        run(["docker", "exec", "-d", self.prefix + node, "sh", "-c", command])

    def vtysh(self, node, command):
        return self.out(node, "vtysh", "-c", command)


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()
rdt_sim.pyDescargar rdt_sim.py
#!/usr/bin/env python3
"""Construye la entrega confiable paso a paso, con un solo paquete en vuelo.

    python3 rdt_sim.py                     lista los escenarios
    python3 rdt_sim.py <escenario>         corre uno y muestra el diagrama de tiempo
    python3 rdt_sim.py todos               corre todos

Es un simulador: no hay red ni sockets. El canal sigue un guion que dice qué le pasa a cada
transmisión, en orden. Así, un error ocurre siempre en el mismo lugar y se puede razonar.
Cada versión del protocolo agrega un solo mecanismo a la anterior:

    v1    canal perfecto: se envía y ya
    v2    errores de bits: checksum, ACK y NAK
    v2.1  ACK o NAK dañados: un bit de secuencia en los datos
    v2.2  lo mismo sin NAK: el ACK dice qué número confirma
    v3    pérdidas: un temporizador
"""
import heapq
import sys
from dataclasses import dataclass, field

ONE_WAY_MS = 10
TIMEOUT_MS = 30
LATE_EXTRA_MS = 25  # lo que se atrasa un paquete con el resultado "tarde"


@dataclass
class Packet:
    kind: str  # DATA, ACK o NAK
    seq: int | None = None
    payload: str | None = None
    corrupt: bool = False

    def label(self):
        text = self.kind
        if self.seq is not None:
            text += f" {self.seq}"
        if self.payload is not None:
            text += f' "{self.payload}"'
        return text


@dataclass
class Result:
    version: str
    trace: list = field(default_factory=list)
    delivered: list = field(default_factory=list)
    finished: bool = False
    transmissions: int = 0


class Simulation:
    """El emisor y el receptor de una versión, unidos por un canal con guion."""

    def __init__(self, version, messages, script=()):
        self.version = version
        self.messages = list(messages)
        self.script = list(script)
        self.result = Result(version)
        self.events = []
        self.order = 0
        self.now = 0
        self.index = 0  # próximo mensaje por confirmar
        self.send_seq = 0
        self.expected_seq = 0
        self.timer_generation = 0

    @property
    def uses_seq(self):
        return self.version in ("v2.1", "v2.2", "v3")

    # El canal --------------------------------------------------------------------------
    def schedule(self, delay, action, *args):
        heapq.heappush(self.events, (self.now + delay, self.order, action, args))
        self.order += 1

    def log(self, side, text):
        self.result.trace.append((self.now, side, text))

    def transmit(self, side, packet, is_retransmission=False):
        outcome = self.script.pop(0) if self.script else "ok"
        self.result.transmissions += 1
        note = {"ok": "", "corrupt": "  [el canal daña bits]", "lost": "  [el canal lo pierde]",
                "late": "  [llega tarde]"}[outcome]
        again = " (otra vez)" if is_retransmission else ""
        self.log(side, f"envía {packet.label()}{again}{note}")
        if outcome == "lost":
            return
        delay = ONE_WAY_MS + (LATE_EXTRA_MS if outcome == "late" else 0)
        if outcome == "corrupt":
            packet = Packet(packet.kind, packet.seq, packet.payload, corrupt=True)
        handler = self.at_receiver if side == "emisor" else self.at_sender
        self.schedule(delay, handler, packet)

    # El emisor -------------------------------------------------------------------------
    def data_packet(self):
        seq = self.send_seq if self.uses_seq else None
        return Packet("DATA", seq, self.messages[self.index])

    def send_current(self, is_retransmission=False):
        self.transmit("emisor", self.data_packet(), is_retransmission)
        if self.version == "v3":
            self.timer_generation += 1
            self.schedule(TIMEOUT_MS, self.on_timeout, self.timer_generation)

    def advance(self):
        self.timer_generation += 1  # apaga el temporizador
        self.index += 1
        self.send_seq ^= 1
        if self.index < len(self.messages):
            self.send_current()
        else:
            self.result.finished = True
            self.log("emisor", "terminó: todo confirmado")

    def on_timeout(self, generation):
        if generation != self.timer_generation or self.result.finished:
            return
        self.log("emisor", "vence el temporizador")
        self.send_current(is_retransmission=True)

    def at_sender(self, packet):
        if self.result.finished:
            self.log("emisor", f"recibe {packet.label()}: ya terminó, lo ignora")
            return
        if packet.corrupt:
            if self.version == "v3":
                self.log("emisor", "recibe una respuesta dañada: la ignora y espera al temporizador")
            else:
                self.log("emisor", "recibe una respuesta dañada: no sabe qué era, repite")
                self.send_current(is_retransmission=True)
            return
        if packet.kind == "NAK":
            self.log("emisor", f"recibe {packet.label()}: repite")
            self.send_current(is_retransmission=True)
        elif self.version in ("v2.2", "v3") and packet.seq != self.send_seq:
            if self.version == "v3":
                self.log("emisor", f"recibe {packet.label()}: es de un paquete viejo, la ignora")
            else:
                self.log("emisor", f"recibe {packet.label()}: esperaba ACK {self.send_seq}, repite")
                self.send_current(is_retransmission=True)
        else:
            self.log("emisor", f"recibe {packet.label()}")
            self.advance()

    # El receptor -----------------------------------------------------------------------
    def deliver(self, packet):
        text = "?!?" if packet.corrupt else packet.payload
        self.result.delivered.append(text)
        return text

    def reply(self, kind, seq=None):
        self.transmit("receptor", Packet(kind, seq))

    def at_receiver(self, packet):
        version = self.version
        if version == "v1":
            # Sin checksum, el receptor no se entera de que los bits llegaron dañados.
            self.log("receptor", f'recibe y entrega "{self.deliver(packet)}"')
            self.schedule(1, self.next_without_reply)
            return
        if packet.corrupt:
            if version in ("v2", "v2.1"):
                self.log("receptor", "recibe datos dañados")
                self.reply("NAK")
            else:
                self.log("receptor", "recibe datos dañados: confirma el último bueno")
                self.reply("ACK", self.expected_seq ^ 1)
            return
        if version == "v2":
            self.log("receptor", f'recibe y entrega "{self.deliver(packet)}"')
            self.reply("ACK")
            return
        if packet.seq == self.expected_seq:
            self.log("receptor", f'recibe {packet.seq} y entrega "{self.deliver(packet)}"')
            self.expected_seq ^= 1
            self.reply("ACK", None if version == "v2.1" else packet.seq)
        else:
            self.log("receptor", f"recibe {packet.seq}: duplicado, no lo entrega")
            self.reply("ACK", None if version == "v2.1" else packet.seq)

    def next_without_reply(self):
        self.index += 1
        if self.index < len(self.messages):
            self.send_current()
        else:
            self.result.finished = True
            self.log("emisor", "terminó")

    # La simulación ---------------------------------------------------------------------
    def run(self):
        self.send_current()
        while self.events:
            self.now, _, action, args = heapq.heappop(self.events)
            action(*args)
        if not self.result.finished:
            self.log("emisor", "se quedó esperando para siempre: nada en camino, ningún temporizador")
        return self.result


# nombre: (versión, guion del canal, qué muestra)
MESSAGES = ["a", "b", "c"]
SCENARIOS = {
    "v1-perfecto": ("v1", [], "v1 sobre un canal perfecto: funciona"),
    "v1-errores": ("v1", ["ok", "corrupt"], "v1 con un error de bits: entrega basura sin enterarse"),
    "v2-datos": ("v2", ["ok", "ok", "corrupt"], "v2 con un dato dañado: el NAK lo arregla"),
    "v2-ack": ("v2", ["ok", "corrupt"], "v2 con un ACK dañado: el emisor repite y se duplica"),
    "v21-ack": ("v2.1", ["ok", "corrupt"], "v2.1 con un ACK dañado: el bit de secuencia lo arregla"),
    "v22-datos": ("v2.2", ["ok", "ok", "corrupt"], "v2.2 con un dato dañado: un ACK repetido hace de NAK"),
    "v22-perdida": ("v2.2", ["ok", "ok", "lost"], "v2.2 con una pérdida: se queda esperando para siempre"),
    "v3-dato": ("v3", ["ok", "ok", "lost"], "v3 con un dato perdido: el temporizador lo repite"),
    "v3-ack": ("v3", ["ok", "lost"], "v3 con un ACK perdido: repite, y el receptor reconoce el duplicado"),
    "v3-tarde": ("v3", ["ok", "late"], "v3 con un ACK tarde: retransmisión innecesaria, pero correcta"),
    "v3-sin-seq": ("v3-sin-seq", ["ok", "lost"], "temporizador sin números de secuencia: se duplica"),
}


class NoSeqSimulation(Simulation):
    """El temporizador de v3 sin el bit de secuencia, para ver por qué hace falta el bit."""

    @property
    def uses_seq(self):
        return False

    def __init__(self, messages, script=()):
        super().__init__("v3", messages, script)

    def at_receiver(self, packet):
        self.log("receptor", f'recibe y entrega "{self.deliver(packet)}"')
        self.reply("ACK")

    def at_sender(self, packet):
        if self.result.finished:
            self.log("emisor", f"recibe {packet.label()}: ya terminó, lo ignora")
            return
        self.log("emisor", f"recibe {packet.label()}")
        self.advance()


def run_scenario(name):
    version, script, _ = SCENARIOS[name]
    if version == "v3-sin-seq":
        return NoSeqSimulation(MESSAGES, script).run()
    return Simulation(version, MESSAGES, script).run()


def print_result(name):
    result = run_scenario(name)
    print(f"--- {name}: {SCENARIOS[name][2]}")
    for when, side, text in result.trace:
        print(f"{when:5d} ms  {side:<9}{text}")
    verdict = "correcto" if result.delivered == MESSAGES and result.finished else "INCORRECTO"
    print(f"entregado: {' '.join(result.delivered)}   ({verdict}, "
          f"{result.transmissions} transmisiones)")


def main():
    if len(sys.argv) == 2 and sys.argv[1] == "todos":
        for name in SCENARIOS:
            print_result(name)
            print()
    elif len(sys.argv) == 2 and sys.argv[1] in SCENARIOS:
        print_result(sys.argv[1])
    else:
        for name, (_, _, description) in SCENARIOS.items():
            print(f"{name:<13}{description}")


if __name__ == "__main__":
    main()
seq_space.pyDescargar seq_space.py
#!/usr/bin/env python3
"""¿Cuántos números de secuencia hacen falta para una ventana? Lo revisa caso por caso.

    python3 seq_space.py [hasta_N]

El receptor está esperando el paquete r. Sin confirmaciones todavía, el emisor puede estar
retrocedido hasta r - W, de modo que por la red pueden llegar copias de r - W ... r - 1. Hay
ambigüedad si alguna de esas copias viejas lleva el mismo número que un paquete que el
receptor aceptaría como nuevo: solo r en Go-Back-N, y r ... r + W - 1 en Selective Repeat.

Supone que la red no guarda un paquete más viejo que una ventana: es lo que TCP resuelve
limitando el tiempo de vida máximo de un segmento.
"""
import sys


def conflicts(protocol, modulus, window):
    """Los números que se pueden confundir, tomando r = window para no tener negativos."""
    r = window
    old = {index % modulus for index in range(r - window, r)}
    accepted = {r % modulus} if protocol == "gbn" else {(r + i) % modulus for i in range(window)}
    return sorted(old & accepted)


def is_safe(protocol, modulus, window):
    return not conflicts(protocol, modulus, window)


def largest_safe_window(protocol, modulus):
    return max(window for window in range(1, modulus + 1) if is_safe(protocol, modulus, window))


def main():
    limit = int(sys.argv[1]) if len(sys.argv) > 1 else 16
    print(f"{'números':>8}{'bits':>6}{'GBN: W máx':>12}{'SR: W máx':>11}")
    modulus = 2
    while modulus <= limit:
        print(f"{modulus:>8}{modulus.bit_length() - 1:>6}{largest_safe_window('gbn', modulus):>12}"
              f"{largest_safe_window('sr', modulus):>11}")
        modulus *= 2
    print()
    for protocol, modulus, window in [("sr", 4, 2), ("sr", 4, 3), ("gbn", 4, 3), ("gbn", 4, 4)]:
        clash = conflicts(protocol, modulus, window)
        text = f"se confunden {clash}" if clash else "sin ambigüedad"
        print(f"{protocol}, {modulus} números, ventana {window}: {text}")


if __name__ == "__main__":
    main()
tablero.pyDescargar tablero.py
#!/usr/bin/env python3
"""El tablero del hackathon: quién alcanza a quién, y qué sesiones BGP no están arriba.

    python3 tablero.py [archivo_de_equipos]        por omisión, ensayo/equipos.txt

El archivo tiene una línea por equipo: nombre, contenedor y dirección. Cada celda es un
ping desde la dirección de la fila hacia la de la columna: que dé "ok" exige que el camino
exista en los dos sentidos.
"""
import json
import os
import subprocess
import sys
from concurrent.futures import ThreadPoolExecutor

HERE = os.path.dirname(os.path.abspath(__file__))
DEFAULT_TEAMS = os.path.join(HERE, "ensayo", "equipos.txt")


def read_teams(path=DEFAULT_TEAMS):
    teams = []
    with open(path, encoding="utf-8") as file:
        for line in file:
            line = line.split("#")[0].strip()
            if line:
                name, container, address = line.split()
                teams.append((name, container, address))
    return teams


def can_reach(source, target):
    command = ["docker", "exec", source[1], "ping", "-c", "2", "-W", "1", "-I", source[2], target[2]]
    return subprocess.run(command, capture_output=True, text=True, timeout=30).returncode == 0


def reachability(teams):
    """Devuelve {(origen, destino): True o False} para cada par de equipos distintos."""
    pairs = [(a, b) for a in teams for b in teams if a is not b]
    with ThreadPoolExecutor(max_workers=32) as pool:
        results = list(pool.map(lambda pair: can_reach(*pair), pairs))
    return {(a[0], b[0]): good for (a, b), good in zip(pairs, results)}


def sessions_down(teams):
    """Devuelve las sesiones BGP que no están en Established: (equipo, vecino, estado)."""
    down = []
    for name, container, _ in teams:
        done = subprocess.run(["docker", "exec", container, "vtysh", "-c", "show bgp summary json"],
                              capture_output=True, text=True, timeout=30)
        try:
            summary = json.loads(done.stdout)
        except json.JSONDecodeError:
            down.append((name, "?", "sin respuesta de vtysh"))
            continue
        peers = summary.get("ipv4Unicast", {}).get("peers", {})
        for address, peer in peers.items():
            if peer.get("state") != "Established":
                down.append((name, address, peer.get("state", "?")))
    return down


def render(teams, matrix):
    names = [team[0] for team in teams]
    width = max(len(name) for name in names) + 2
    lines = [" " * width + "".join(f"{name:>{width}}" for name in names)]
    for row in names:
        cells = ["-" if row == column else ("ok" if matrix[row, column] else "X")
                 for column in names]
        lines.append(f"{row:<{width}}" + "".join(f"{cell:>{width}}" for cell in cells))
    good = sum(matrix.values())
    lines.append(f"{good} de {len(matrix)} pares se alcanzan")
    return "\n".join(lines)


def main():
    path = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_TEAMS
    teams = read_teams(path)
    print(render(teams, reachability(teams)))
    down = sessions_down(teams)
    print("sesiones BGP que no están arriba:" if down else "todas las sesiones BGP están arriba")
    for name, address, state in down:
        print(f"  {name}: vecino {address}, {state}")


if __name__ == "__main__":
    main()
tcp_demux.cDescargar tcp_demux.c
// Servidor TCP que muestra qué socket atiende cada conexión.
//   tcp_demux <puerto>
// El socket que escucha nunca recibe datos: cada conexión aceptada tiene el suyo.
#include <arpa/inet.h>
#include <netinet/in.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

#define MAX_CONNECTIONS 16
#define MAX_MESSAGE 1500

static void describe_peer(int fd, char *text, size_t size) {
    struct sockaddr_in peer;
    socklen_t len = sizeof peer;
    char ip[INET_ADDRSTRLEN] = "?";
    if (getpeername(fd, (struct sockaddr *)&peer, &len) == -1) {
        perror("getpeername");
        snprintf(text, size, "?");
        return;
    }
    if (inet_ntop(AF_INET, &peer.sin_addr, ip, sizeof ip) == NULL) {
        perror("inet_ntop");
    }
    snprintf(text, size, "%s:%u", ip, ntohs(peer.sin_port));
}

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "uso: %s <puerto>\n", argv[0]);
        return EXIT_FAILURE;
    }
    int port = atoi(argv[1]);
    if (port <= 0 || port > 65535) {
        fprintf(stderr, "puerto no válido: %s\n", argv[1]);
        return EXIT_FAILURE;
    }
    if (setvbuf(stdout, NULL, _IOLBF, 0) != 0) {
        perror("setvbuf");
    }
    int listener = socket(AF_INET, SOCK_STREAM, 0);
    if (listener == -1) {
        perror("socket");
        return EXIT_FAILURE;
    }
    int yes = 1;
    if (setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes) == -1) {
        perror("setsockopt");
        return EXIT_FAILURE;
    }
    struct sockaddr_in local;
    memset(&local, 0, sizeof local);
    local.sin_family = AF_INET;
    local.sin_addr.s_addr = htonl(INADDR_ANY);
    local.sin_port = htons((uint16_t)port);
    if (bind(listener, (struct sockaddr *)&local, sizeof local) == -1) {
        perror("bind");
        return EXIT_FAILURE;
    }
    if (listen(listener, 8) == -1) {
        perror("listen");
        return EXIT_FAILURE;
    }
    printf("socket %d  escuchando en el puerto %d\n", listener, port);

    struct pollfd watched[MAX_CONNECTIONS + 1];
    watched[0] = (struct pollfd){.fd = listener, .events = POLLIN};
    nfds_t count = 1;

    for (;;) {
        if (poll(watched, count, -1) == -1) {
            perror("poll");
            return EXIT_FAILURE;
        }
        char peer[64];
        if ((watched[0].revents & POLLIN) && count <= MAX_CONNECTIONS) {
            int fd = accept(listener, NULL, NULL);
            if (fd == -1) {
                perror("accept");
            } else {
                describe_peer(fd, peer, sizeof peer);
                printf("socket %d  conexión nueva de %s\n", fd, peer);
                watched[count++] = (struct pollfd){.fd = fd, .events = POLLIN};
            }
        }
        for (nfds_t i = 1; i < count; i++) {
            if (!(watched[i].revents & (POLLIN | POLLHUP))) {
                continue;
            }
            char data[MAX_MESSAGE];
            ssize_t got = recv(watched[i].fd, data, sizeof data - 1, 0);
            if (got == -1) {
                perror("recv");
            }
            if (got <= 0) {
                if (close(watched[i].fd) == -1) {
                    perror("close");
                }
                watched[i] = watched[--count];
                i--;
                continue;
            }
            data[got] = '\0';
            describe_peer(watched[i].fd, peer, sizeof peer);
            printf("socket %d  de %s  \"%s\"\n", watched[i].fd, peer, data);
            char reply[64];
            int len = snprintf(reply, sizeof reply, "recibido en el socket %d", watched[i].fd);
            if (send(watched[i].fd, reply, (size_t)len, 0) == -1) {
                perror("send");
            }
        }
    }
}
tftp_mini.pyDescargar tftp_mini.py
#!/usr/bin/env python3
"""Un TFTP mínimo (RFC 1350), solo lectura: un protocolo real de stop-and-wait sobre UDP.

    python3 tftp_mini.py serve <carpeta> [--timeout-ms 200] [--port 69]
    python3 tftp_mini.py get <ip> <archivo> <destino> [--port 69] [--ignore-block-number]

Por cada bloque de 512 bytes el servidor espera su ACK. Si el temporizador vence, repite el
bloque. Un bloque de menos de 512 bytes indica el final. --ignore-block-number hace que el
cliente escriba todo lo que llega, sin mirar el número de bloque: sirve para ver qué pasa
sin números de secuencia.
"""
import argparse
import hashlib
import os
import socket
import struct
import threading
import time

OP_RRQ, OP_DATA, OP_ACK, OP_ERROR = 1, 3, 4, 5
BLOCK_SIZE = 512
MAX_TRIES = 40
GET_IDLE_S = 10


def send_file(path, client, timeout_s):
    """Atiende una lectura, desde un socket y un puerto nuevos, como pide el RFC 1350."""
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.connect(client)
    block_number = 0
    retransmissions = 0
    try:
        with open(path, "rb") as file:
            while True:
                data = file.read(BLOCK_SIZE)
                block_number += 1
                packet = struct.pack("!HH", OP_DATA, block_number & 0xffff) + data
                retransmissions += stop_and_wait(sock, packet, block_number & 0xffff, timeout_s)
                if len(data) < BLOCK_SIZE:
                    break
        print(f"enviado {os.path.basename(path)}: {block_number} bloques, "
              f"{retransmissions} retransmisiones", flush=True)
    except FileNotFoundError:
        sock.send(struct.pack("!HH", OP_ERROR, 1) + b"no existe\0")
    except (TimeoutError, ConnectionError) as error:
        print(f"transferencia abandonada: {error}", flush=True)
    finally:
        sock.close()


def stop_and_wait(sock, packet, block_number, timeout_s):
    """Manda un bloque y espera su ACK. Devuelve cuántas veces tuvo que repetirlo."""
    for tries in range(MAX_TRIES):
        sock.send(packet)
        deadline = time.monotonic() + timeout_s
        while (left := deadline - time.monotonic()) > 0:
            sock.settimeout(left)
            try:
                reply = sock.recv(64)
            except TimeoutError:
                break
            # Un ACK viejo se ignora: repetir por eso multiplicaría los bloques en la red.
            if len(reply) == 4 and struct.unpack("!HH", reply) == (OP_ACK, block_number):
                return tries
    raise TimeoutError(f"sin ACK del bloque {block_number}")


def serve(folder, port, timeout_ms):
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind(("0.0.0.0", port))
    print(f"TFTP en el puerto {port}, carpeta {folder}", flush=True)
    while True:
        request, client = sock.recvfrom(1024)
        if len(request) < 4 or struct.unpack("!H", request[:2])[0] != OP_RRQ:
            continue
        name = request[2:].split(b"\0")[0].decode(errors="replace")
        path = os.path.join(folder, os.path.basename(name))
        threading.Thread(target=send_file, args=(path, client, timeout_ms / 1000),
                         daemon=True).start()


def get(server_ip, name, target, port, ignore_block_number):
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.settimeout(1.0)
    request = struct.pack("!H", OP_RRQ) + name.encode() + b"\0octet\0"
    digest = hashlib.sha256()
    expected = 1
    duplicates = 0
    total = 0
    started = time.monotonic()
    last_heard = started
    sock.sendto(request, (server_ip, port))
    with open(target, "wb") as file:
        while True:
            try:
                packet, sender = sock.recvfrom(BLOCK_SIZE + 4)
            except TimeoutError:
                if time.monotonic() - last_heard > GET_IDLE_S:
                    raise SystemExit("el servidor dejó de responder") from None
                if expected == 1:
                    sock.sendto(request, (server_ip, port))
                continue
            last_heard = time.monotonic()
            opcode, block = struct.unpack("!HH", packet[:4])
            if opcode == OP_ERROR:
                raise SystemExit(f"error del servidor: {packet[4:-1].decode(errors='replace')}")
            if opcode != OP_DATA:
                continue
            is_new = block == (expected & 0xffff)
            sock.sendto(struct.pack("!HH", OP_ACK, block), sender)
            if not is_new:
                duplicates += 1
            if is_new or ignore_block_number:
                data = packet[4:]
                file.write(data)
                digest.update(data)
                total += len(data)
            if is_new:
                expected += 1
                if len(packet) - 4 < BLOCK_SIZE:
                    break
    elapsed = time.monotonic() - started
    rate = total * 8 / elapsed / 1000
    print(f"bytes={total} tiempo={elapsed:.2f}s tasa={rate:.1f} kbit/s "
          f"duplicados={duplicates} sha256={digest.hexdigest()[:16]}")


def main():
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    commands = parser.add_subparsers(dest="command", required=True)
    server = commands.add_parser("serve")
    server.add_argument("folder")
    server.add_argument("--port", type=int, default=69)
    server.add_argument("--timeout-ms", type=int, default=200)
    client = commands.add_parser("get")
    client.add_argument("server_ip")
    client.add_argument("name")
    client.add_argument("target")
    client.add_argument("--port", type=int, default=69)
    client.add_argument("--ignore-block-number", action="store_true")
    args = parser.parse_args()
    if args.command == "serve":
        serve(args.folder, args.port, args.timeout_ms)
    else:
        get(args.server_ip, args.name, args.target, args.port, args.ignore_block_number)


if __name__ == "__main__":
    main()
udp_checksum.pyDescargar udp_checksum.py
#!/usr/bin/env python3
"""Calcula a mano el checksum de un datagrama UDP sobre IPv4, palabra por palabra.

    python3 udp_checksum.py <ip_origen> <ip_destino> <puerto_origen> <puerto_destino> <datos>

Sigue el RFC 768: suma de complemento a uno del pseudo-encabezado, el encabezado UDP
(con el checksum en cero) y los datos, y al final el complemento. Si el resultado da
cero, se escribe 0xffff, porque en UDP sobre IPv4 un cero quiere decir "sin checksum".
"""
import argparse
import ipaddress
import struct

PROTO_UDP = 17


def pseudo_header(source, destination, udp_length):
    return (ipaddress.IPv4Address(source).packed + ipaddress.IPv4Address(destination).packed
            + struct.pack("!BBH", 0, PROTO_UDP, udp_length))


def words(data):
    """Parte en palabras de 16 bits. Si el largo es impar, completa con un byte en cero."""
    if len(data) % 2:
        data += b"\0"
    return [int.from_bytes(data[i:i + 2], "big") for i in range(0, len(data), 2)]


def ones_complement_sum(values):
    total = sum(values)
    while total > 0xffff:
        total = (total & 0xffff) + (total >> 16)
    return total


def udp_checksum(source, destination, source_port, destination_port, payload):
    length = 8 + len(payload)
    data = (pseudo_header(source, destination, length)
            + struct.pack("!HHHH", source_port, destination_port, length, 0) + payload)
    result = ~ones_complement_sum(words(data)) & 0xffff
    return result if result != 0 else 0xffff


def explain(source, destination, source_port, destination_port, payload):
    """Devuelve la cuenta como texto, en líneas."""
    length = 8 + len(payload)
    parts = [("pseudo-encabezado", pseudo_header(source, destination, length)),
             ("encabezado UDP", struct.pack("!HHHH", source_port, destination_port, length, 0)),
             ("datos", payload)]
    lines = []
    all_words = []
    for name, data in parts:
        block = words(data)
        all_words += block
        lines.append(f"{name:<18} " + " ".join(f"{word:04x}" for word in block))
    total = sum(all_words)
    lines.append(f"{'suma':<18} {total:x}")
    folded = ones_complement_sum(all_words)
    if total != folded:
        lines.append(f"{'con acarreos':<18} {folded:04x}")
    lines.append(f"{'complemento':<18} {~folded & 0xffff:04x}")
    lines.append(f"{'checksum':<18} "
                 f"{udp_checksum(source, destination, source_port, destination_port, payload):04x}")
    return lines


def main():
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    parser.add_argument("source")
    parser.add_argument("destination")
    parser.add_argument("source_port", type=int)
    parser.add_argument("destination_port", type=int)
    parser.add_argument("payload")
    args = parser.parse_args()
    print("\n".join(explain(args.source, args.destination, args.source_port,
                            args.destination_port, args.payload.encode())))


if __name__ == "__main__":
    main()
udp_demux.cDescargar udp_demux.c
// Servidor UDP que muestra qué socket recibe cada datagrama.
//   udp_demux <puerto>                       un solo socket, sin connect
//   udp_demux <puerto> <ip> <puerto_remoto>  además, un socket conectado a ese cliente
// Contesta a cada datagrama con el número de socket que lo recibió.
#include <arpa/inet.h>
#include <netinet/in.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

#define MAX_DATAGRAM 1500

static int parse_port(const char *text) {
    int port = atoi(text);
    if (port <= 0 || port > 65535) {
        fprintf(stderr, "puerto no válido: %s\n", text);
        exit(EXIT_FAILURE);
    }
    return port;
}

// Crea un socket UDP ligado al puerto. SO_REUSEADDR permite dos sockets en el mismo puerto.
static int bound_socket(int port) {
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (fd == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }
    int yes = 1;
    if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes) == -1) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }
    struct sockaddr_in local;
    memset(&local, 0, sizeof local);
    local.sin_family = AF_INET;
    local.sin_addr.s_addr = htonl(INADDR_ANY);
    local.sin_port = htons((uint16_t)port);
    if (bind(fd, (struct sockaddr *)&local, sizeof local) == -1) {
        perror("bind");
        exit(EXIT_FAILURE);
    }
    return fd;
}

// Recibe un datagrama de fd, informa quién lo mandó y contesta.
static void serve_one(int fd) {
    char data[MAX_DATAGRAM];
    struct sockaddr_in peer;
    socklen_t peer_len = sizeof peer;
    ssize_t got = recvfrom(fd, data, sizeof data - 1, 0, (struct sockaddr *)&peer, &peer_len);
    if (got == -1) {
        perror("recvfrom");
        return;
    }
    data[got] = '\0';
    char ip[INET_ADDRSTRLEN];
    if (inet_ntop(AF_INET, &peer.sin_addr, ip, sizeof ip) == NULL) {
        perror("inet_ntop");
        return;
    }
    printf("socket %d  de %s:%u  \"%s\"\n", fd, ip, ntohs(peer.sin_port), data);

    char reply[64];
    int len = snprintf(reply, sizeof reply, "recibido en el socket %d", fd);
    if (sendto(fd, reply, (size_t)len, 0, (struct sockaddr *)&peer, peer_len) == -1) {
        perror("sendto");
    }
}

int main(int argc, char *argv[]) {
    if (argc != 2 && argc != 4) {
        fprintf(stderr, "uso: %s <puerto> [<ip> <puerto_remoto>]\n", argv[0]);
        return EXIT_FAILURE;
    }
    if (setvbuf(stdout, NULL, _IOLBF, 0) != 0) {
        perror("setvbuf");
    }
    int port = parse_port(argv[1]);
    struct pollfd watched[2] = {{.fd = bound_socket(port), .events = POLLIN}};
    nfds_t count = 1;

    if (argc == 4) {
        // connect en UDP no manda nada: solo le dice al kernel de quién es este socket.
        struct sockaddr_in remote;
        memset(&remote, 0, sizeof remote);
        remote.sin_family = AF_INET;
        remote.sin_port = htons((uint16_t)parse_port(argv[3]));
        if (inet_pton(AF_INET, argv[2], &remote.sin_addr) != 1) {
            fprintf(stderr, "dirección no válida: %s\n", argv[2]);
            return EXIT_FAILURE;
        }
        int fd = bound_socket(port);
        if (connect(fd, (struct sockaddr *)&remote, sizeof remote) == -1) {
            perror("connect");
            return EXIT_FAILURE;
        }
        watched[1] = (struct pollfd){.fd = fd, .events = POLLIN};
        count = 2;
    }

    for (;;) {
        if (poll(watched, count, -1) == -1) {
            perror("poll");
            return EXIT_FAILURE;
        }
        for (nfds_t i = 0; i < count; i++) {
            if (watched[i].revents & POLLIN) {
                serve_one(watched[i].fd);
            }
        }
    }
}
window_sim.pyDescargar window_sim.py
#!/usr/bin/env python3
"""Simulador de ventana deslizante: Go-Back-N y Selective Repeat, de libro.

    python3 window_sim.py traza gbn --window 4 --packets 6 --drop-data 2
    python3 window_sim.py traza sr --window 4 --packets 6 --drop-data 2
    python3 window_sim.py util --mbps 1000 --rtt 30 --bytes 1500
    python3 window_sim.py barrido
    python3 window_sim.py ambiguedad

Trabaja con paquetes numerados, sin bytes, sin conexión y con un temporizador fijo. Los
ACK son de libro: "ACK n" confirma el paquete n (en Go-Back-N, hasta el n inclusive). TCP
usa otra convención: su ACK dice qué byte espera a continuación.

Modelo: el emisor transmite un paquete a la vez, a la velocidad del enlace (d milisegundos
por paquete), y cada sentido demora rtt/2. Los ACK no ocupan tiempo de transmisión. El
enlace no tiene cola ni otros flujos: aquí no hay congestión.
"""
import argparse
import heapq
import random
from dataclasses import dataclass, field


@dataclass
class Config:
    protocol: str  # "gbn" o "sr"
    window: int
    packets: int
    d: float = 1.0  # tiempo de transmisión de un paquete, en ms
    rtt: float = 10.0
    timeout: float | None = None  # si no se da: el doble de lo que tarda una ventana en volver
    modulus: int | None = None  # espacio de números de secuencia; None es sin límite
    drop_data: list = field(default_factory=list)  # (paquete, copia), con copia desde 1
    drop_ack: list = field(default_factory=list)  # posición entre los ACK enviados, desde 1
    loss_data: float = 0.0
    loss_ack: float = 0.0
    seed: int = 1
    max_time: float = 10_000_000.0

    def timer(self):
        return self.timeout if self.timeout else 2 * (self.rtt + self.window * self.d)


@dataclass
class Stats:
    delivered: list = field(default_factory=list)
    transmissions: int = 0
    retransmissions: int = 0
    finish_time: float = 0.0
    finished: bool = False
    acks_sent: int = 0

    def utilization(self, d):
        return len(self.delivered) * d / self.finish_time if self.finish_time else 0.0

    def per_packet(self):
        return self.transmissions / len(self.delivered) if self.delivered else float("inf")


class WindowSim:
    """El emisor y el receptor de Go-Back-N o de Selective Repeat."""

    def __init__(self, config, trace=None):
        self.c = config
        self.trace = trace
        self.random = random.Random(config.seed)
        self.stats = Stats()
        self.events = []
        self.order = 0
        self.now = 0.0
        self.tx_free = 0.0
        self.copies = {}
        self.base = 0  # índice del paquete más antiguo sin confirmar
        self.next = 0
        self.acked = set()
        self.timers = {}  # generación del temporizador: uno por paquete en SR, uno solo en GBN
        self.expected = 0  # receptor, como número de secuencia
        self.rcv_buffer = {}  # receptor de SR: número de secuencia -> contenido

    # Ayudas ----------------------------------------------------------------------------
    def wire(self, index):
        return index % self.c.modulus if self.c.modulus else index

    def space(self, value):
        return value % self.c.modulus if self.c.modulus else value

    def log(self, side, text):
        if self.trace is not None:
            self.trace.append((self.now, side, text))

    def schedule(self, when, action, *args):
        heapq.heappush(self.events, (when, self.order, action, args))
        self.order += 1

    # Emisor ----------------------------------------------------------------------------
    def send(self, index, again=False):
        copy = self.copies.get(index, 0) + 1
        self.copies[index] = copy
        self.stats.transmissions += 1
        if again:
            self.stats.retransmissions += 1
        self.tx_free = max(self.tx_free, self.now) + self.c.d
        lost = (index, copy) in self.c.drop_data or self.random.random() < self.c.loss_data
        note = "  [se pierde]" if lost else ""
        word = "repite" if again else "envía"
        self.log("emisor", f"{word} {self.wire(index)}{note}")
        if not lost:
            self.schedule(self.tx_free + self.c.rtt / 2, self.at_receiver, self.wire(index), index)
        if self.c.protocol == "sr":
            self.arm_timer(index)

    def fill_window(self):
        while self.next < self.c.packets and self.next < self.base + self.c.window:
            first_in_flight = self.next == self.base
            self.send(self.next)
            if self.c.protocol == "gbn" and first_in_flight:
                self.arm_timer("gbn")
            self.next += 1

    def arm_timer(self, key):
        generation = self.timers.get(key, 0) + 1
        self.timers[key] = generation
        self.schedule(self.tx_free + self.c.timer(), self.on_timeout, key, generation)

    def stop_timer(self, key):
        self.timers[key] = self.timers.get(key, 0) + 1

    def on_timeout(self, key, generation):
        if self.timers.get(key) != generation or self.stats.finished:
            return
        if self.c.protocol == "gbn":
            self.log("emisor", f"vence el temporizador: repite desde {self.wire(self.base)}")
            for index in range(self.base, self.next):
                self.send(index, again=True)
            self.arm_timer("gbn")
        else:
            self.log("emisor", f"vence el temporizador de {self.wire(key)}")
            self.send(key, again=True)

    def at_sender(self, ack):
        for index in range(self.base, self.next):
            if self.wire(index) == ack:
                break
        else:
            self.log("emisor", f"recibe ACK {ack}: ya confirmado, lo ignora")
            return
        if self.c.protocol == "gbn":
            self.log("emisor", f"recibe ACK {ack}: confirma hasta {ack}")
            self.base = index + 1
            if self.base == self.next:
                self.stop_timer("gbn")
            else:
                self.arm_timer("gbn")
        else:
            self.log("emisor", f"recibe ACK {ack}")
            self.acked.add(index)
            self.stop_timer(index)
            while self.base in self.acked:
                self.base += 1
        if self.base >= self.c.packets:
            self.stats.finished = True
            self.stats.finish_time = self.now
            return
        self.fill_window()

    # Receptor --------------------------------------------------------------------------
    def send_ack(self, ack):
        self.stats.acks_sent += 1
        lost = self.stats.acks_sent in self.c.drop_ack or self.random.random() < self.c.loss_ack
        self.log("receptor", f"  ACK {ack}" + ("  [se pierde]" if lost else ""))
        if not lost:
            self.schedule(self.now + self.c.rtt / 2, self.at_sender, ack)

    def at_receiver(self, seq, content):
        if self.c.protocol == "gbn":
            self.gbn_receive(seq, content)
        else:
            self.sr_receive(seq, content)

    def gbn_receive(self, seq, content):
        if seq == self.expected:
            self.stats.delivered.append(content)
            self.log("receptor", f"recibe {seq}, entrega el contenido {content}")
            self.expected = self.space(self.expected + 1)
            self.send_ack(seq)
            return
        last_good = self.space(self.expected - 1)
        if self.stats.delivered:
            self.log("receptor", f"recibe {seq}: esperaba {self.expected}, lo descarta")
            self.send_ack(last_good)
        else:
            self.log("receptor", f"recibe {seq}: esperaba {self.expected}, lo descarta (nada que confirmar)")

    def sr_receive(self, seq, content):
        window, modulus = self.c.window, self.c.modulus
        offset = (seq - self.expected) % modulus if modulus else seq - self.expected
        if 0 <= offset < window:
            self.rcv_buffer[seq] = content
            released = []
            next_expected = self.expected
            while next_expected in self.rcv_buffer:
                released.append(self.rcv_buffer.pop(next_expected))
                next_expected = self.space(next_expected + 1)
            self.expected = next_expected
            self.stats.delivered += released
            what = f"entrega el contenido {released}" if released else "lo guarda"
            self.log("receptor", f"recibe {seq}, {what}")
            self.send_ack(seq)
            return
        behind = (self.expected - seq) % modulus if modulus else self.expected - seq
        if 0 < behind <= window:
            self.log("receptor", f"recibe {seq}: ya entregado, solo confirma")
            self.send_ack(seq)
        else:
            self.log("receptor", f"recibe {seq}: fuera de ventana, lo ignora")

    # Simulación ------------------------------------------------------------------------
    def run(self):
        self.fill_window()
        while self.events and not self.stats.finished:
            self.now, _, action, args = heapq.heappop(self.events)
            if self.now > self.c.max_time:
                break
            action(*args)
        if not self.stats.finished:
            self.stats.finish_time = self.now
        return self.stats


def simulate(**kwargs):
    return WindowSim(Config(**kwargs)).run()


def stop_and_wait_utilization(d, rtt):
    """La fórmula: un paquete cada d + rtt."""
    return d / (rtt + d)


def utilization(window, d, rtt):
    """Con ventana W y sin pérdidas: W paquetes cada d + rtt, sin pasar de 1."""
    return min(1.0, window * d / (rtt + d))


def window_to_fill(d, rtt):
    """La ventana mínima para llenar el enlace: (d + rtt) / d, redondeando hacia arriba."""
    needed = (rtt + d) / d
    whole = int(needed)
    return whole if abs(needed - whole) < 1e-9 else whole + 1


def sweep(window=50, packets=2000, d=1.0, rtt=40.0, seeds=5, losses=(0, 0.001, 0.01, 0.05, 0.1, 0.2),
          ack_only=False, timeout=None):
    """GBN contra SR, promediando varias semillas. Devuelve una lista de filas."""
    rows = []
    for loss in losses:
        row = {"loss": loss}
        for protocol in ("gbn", "sr"):
            per_packet = util = 0.0
            for seed in range(1, seeds + 1):
                kwargs = {"loss_ack": loss} if ack_only else {"loss_data": loss, "loss_ack": loss}
                stats = simulate(protocol=protocol, window=window, packets=packets, d=d, rtt=rtt,
                                 seed=seed, timeout=timeout, **kwargs)
                per_packet += stats.per_packet()
                util += stats.utilization(d)
            row[protocol] = (per_packet / seeds, util / seeds)
        rows.append(row)
    return rows


# Escenarios de ambigüedad: la primera ronda llega bien y se pierden todos sus ACK. El emisor
# repite el paquete 0, y el receptor ya está esperando números que dan la vuelta.
def ambiguity_run(protocol, modulus, window, packets=8):
    trace = []
    acks_lost = list(range(1, window + 1))
    stats = WindowSim(Config(protocol=protocol, window=window, packets=packets, d=1.0, rtt=10.0,
                             modulus=modulus, drop_ack=acks_lost, max_time=5000.0), trace).run()
    return stats, trace


def print_trace(trace):
    for when, side, text in trace:
        print(f"{when:7.1f}  {side:<9}{text}")


def command_trace(args):
    trace = []
    drop_data = [(index, 1) for index in args.drop_data]
    stats = WindowSim(Config(protocol=args.protocol, window=args.window, packets=args.packets,
                             d=args.d, rtt=args.rtt, timeout=args.timeout, drop_data=drop_data,
                             drop_ack=args.drop_ack), trace).run()
    print_trace(trace)
    print(f"entregado: {stats.delivered}   transmisiones: {stats.transmissions} "
          f"(repetidas: {stats.retransmissions})   tiempo: {stats.finish_time:.1f} ms")


def command_utilization(args):
    d = args.bytes * 8 / (args.mbps * 1000)
    print(f"d = {args.bytes} bytes a {args.mbps:g} Mbps = {d:.4f} ms    RTT = {args.rtt:g} ms")
    print(f"fórmula, stop-and-wait: U = d / (RTT + d) = "
          f"{stop_and_wait_utilization(d, args.rtt):.5f}")
    packets = args.packets
    stats = simulate(protocol="gbn", window=1, packets=packets, d=d, rtt=args.rtt)
    print(f"simulador, ventana 1:   U = {stats.utilization(d):.5f}")
    needed = window_to_fill(d, args.rtt)
    print(f"ventana para llenar el enlace: (RTT + d) / d = {needed} paquetes")
    print(f"throughput de stop-and-wait: {args.bytes * 8 / ((args.rtt + d) * 1000):.3f} Mbps")
    print()
    print(f"{'ventana':>8}{'fórmula':>10}{'simulador':>11}")
    for window in args.windows or [1, 10, 100, needed // 2, needed, needed * 2]:
        # Muchas ventanas: la fórmula es de régimen, y el comienzo y el final no aportan.
        count = max(packets, window * 40)
        measured = simulate(protocol="gbn", window=window, packets=count, d=d, rtt=args.rtt)
        print(f"{window:>8}{utilization(window, d, args.rtt):>10.4f}"
              f"{measured.utilization(d):>11.4f}")


def print_sweep(rows):
    print(f"{'pérdida':>8} | {'GBN envíos/paq':>14}{'GBN U':>8} | {'SR envíos/paq':>14}{'SR U':>8}")
    for row in rows:
        gbn, sr = row["gbn"], row["sr"]
        print(f"{row['loss'] * 100:7.1f}% | {gbn[0]:>14.2f}{gbn[1]:>8.2f} | {sr[0]:>14.2f}{sr[1]:>8.2f}")


def command_sweep(args):
    timer = args.timeout if args.timeout else 2 * (args.rtt + args.window * args.d)
    print(f"ventana {args.window}, {args.packets} paquetes, d = {args.d:g} ms, RTT = {args.rtt:g} ms, "
          f"temporizador = {timer:g} ms, promedio de {args.seeds} semillas")
    print("pérdida en los dos sentidos:")
    print_sweep(sweep(args.window, args.packets, args.d, args.rtt, args.seeds, timeout=args.timeout))
    print("pérdida solo en los ACK:")
    print_sweep(sweep(args.window, args.packets, args.d, args.rtt, args.seeds, ack_only=True,
                      timeout=args.timeout))


def command_ambiguity(args):
    for protocol, modulus, window in args.cases or [("sr", 4, 3), ("sr", 4, 2)]:
        stats, trace = ambiguity_run(protocol, modulus, window)
        print(f"--- {protocol}: {modulus} números, ventana {window}; "
              "se pierden los ACK de la primera ronda")
        print_trace(trace[:args.lines])
        if len(trace) > args.lines:
            print(f"        ... ({len(trace) - args.lines} líneas más)")
        expected = list(range(len(stats.delivered)))
        verdict = "correcto" if stats.delivered == expected and stats.finished else "INCORRECTO"
        print(f"contenidos entregados: {stats.delivered}   ({verdict})")
        print()


def main():
    parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    commands = parser.add_subparsers(dest="command", required=True)

    trace = commands.add_parser("traza")
    trace.add_argument("protocol", choices=["gbn", "sr"])
    trace.add_argument("--window", type=int, default=4)
    trace.add_argument("--packets", type=int, default=6)
    trace.add_argument("--d", type=float, default=1.0)
    trace.add_argument("--rtt", type=float, default=10.0)
    trace.add_argument("--timeout", type=float, default=None)
    trace.add_argument("--drop-data", type=int, nargs="*", default=[])
    trace.add_argument("--drop-ack", type=int, nargs="*", default=[])
    trace.set_defaults(run=command_trace)

    util = commands.add_parser("util")
    util.add_argument("--mbps", type=float, default=1000)
    util.add_argument("--rtt", type=float, default=30)
    util.add_argument("--bytes", type=int, default=1500)
    util.add_argument("--packets", type=int, default=200)
    util.add_argument("--windows", type=int, nargs="*", default=None)
    util.set_defaults(run=command_utilization)

    sweep_cmd = commands.add_parser("barrido")
    sweep_cmd.add_argument("--window", type=int, default=50)
    sweep_cmd.add_argument("--packets", type=int, default=2000)
    sweep_cmd.add_argument("--d", type=float, default=1.0)
    sweep_cmd.add_argument("--rtt", type=float, default=40.0)
    sweep_cmd.add_argument("--seeds", type=int, default=5)
    sweep_cmd.add_argument("--timeout", type=float, default=None)
    sweep_cmd.set_defaults(run=command_sweep)

    ambiguity = commands.add_parser("ambiguedad")
    ambiguity.add_argument("--lines", type=int, default=30)
    ambiguity.set_defaults(run=command_ambiguity, cases=None)

    args = parser.parse_args()
    args.run(args)


if __name__ == "__main__":
    main()
ensayo/ensayo.clab.ymlDescargar ensayo/ensayo.clab.yml
# Ensayo del hackathon: cuatro equipos de un solo router y dos proveedores de tránsito.
# La red trae cuatro fallas a propósito, de las que se ven en un hackathon de verdad. Diagnostíquenlas
# con tablero.py y vtysh; sus arreglos van en lab/.
#
#            t1 ------- peer ------- t2         AS 64901 y 64902
#          /  |  \                 /  |  \
#         /   |   \_____ e2 ______/   |   \
#        /    |                       |    \
#      e1     \________ e3 ___________/     e4
#                       \_______ peer ______/
#
# AS 64911 a 64914, prefijos 100.64.11.0/24 a 100.64.14.0/24. Las direcciones no chocan con las de
# ningún equipo del curso, que usan 100.(64 + X) con X desde 1.
name: w09-ensayo
mgmt:
  network: redes-w09-ensayo
  ipv4-subnet: 172.90.9.64/26
  ipv6-subnet: 3fff:172:90:9:1::/80
topology:
  defaults:
    kind: linux
    image: redes-router
  nodes:
    t1:
      binds: [t1/frr.conf:/etc/frr/frr.conf:ro]
      exec:
        - ip route del default
        - sysctl -w net.ipv6.conf.all.disable_ipv6=1
    t2:
      binds: [t2/frr.conf:/etc/frr/frr.conf:ro]
      exec:
        - ip route del default
        - sysctl -w net.ipv6.conf.all.disable_ipv6=1
    e1:
      binds: [e1/frr.conf:/etc/frr/frr.conf:ro]
      exec:
        - ip route del default
        - sysctl -w net.ipv6.conf.all.disable_ipv6=1
        - ip link add dum0 type dummy
        - ip link set dum0 up
    e2:
      binds: [e2/frr.conf:/etc/frr/frr.conf:ro]
      exec:
        - ip route del default
        - sysctl -w net.ipv6.conf.all.disable_ipv6=1
        - ip link add dum0 type dummy
        - ip link set dum0 up
    e3:
      binds: [e3/frr.conf:/etc/frr/frr.conf:ro]
      exec:
        - ip route del default
        - sysctl -w net.ipv6.conf.all.disable_ipv6=1
        - ip link add dum0 type dummy
        - ip link set dum0 up
    e4:
      binds: [e4/frr.conf:/etc/frr/frr.conf:ro]
      exec:
        - ip route del default
        - sysctl -w net.ipv6.conf.all.disable_ipv6=1
        - ip link add dum0 type dummy
        - ip link set dum0 up
  # Containerlab usa MTU 9500 por defecto; aquí queremos el valor de Ethernet.
  links:
    - {endpoints: ["t1:eth1", "t2:eth1"], mtu: 1500}
    - {endpoints: ["t1:eth2", "e1:eth1"], mtu: 1500}
    - {endpoints: ["t1:eth3", "e2:eth1"], mtu: 1500}
    - {endpoints: ["t1:eth4", "e3:eth1"], mtu: 1500}
    - {endpoints: ["t2:eth2", "e2:eth2"], mtu: 1500}
    - {endpoints: ["t2:eth3", "e3:eth2"], mtu: 1500}
    - {endpoints: ["t2:eth4", "e4:eth1"], mtu: 1500}
    - {endpoints: ["e3:eth3", "e4:eth2"], mtu: 1500}
ensayo/equipos.txtDescargar ensayo/equipos.txt
# Lo que lee tablero.py: nombre, contenedor del router, dirección desde la que se prueba
# y dirección a la que los demás deben llegar. Se puede tener un archivo así por cada red.
e1 clab-w09-ensayo-e1 100.64.11.1
e2 clab-w09-ensayo-e2 100.64.12.1
e3 clab-w09-ensayo-e3 100.64.13.1
e4 clab-w09-ensayo-e4 100.64.14.1
par/enlace.shDescargar par/enlace.sh
#!/bin/sh
# Configura el enlace entre r y srv: velocidad, retardo y fallas en cada sentido.
#   sh par/enlace.sh <velocidad> <rtt_ms> [perdida_datos_%] [perdida_ack_%] [corrupcion_datos_%]
#   sh par/enlace.sh limpio
# Los "datos" van de srv hacia r. Los ACK van de r hacia srv.
set -eu
srv=clab-w09-par-srv
r=clab-w09-par-r
docker exec "$srv" tc qdisc del dev eth1 root 2>/dev/null || true
docker exec "$r" tc qdisc del dev eth3 root 2>/dev/null || true
[ "$1" = limpio ] && exit 0

rate=$1
one_way=$(($2 / 2))
loss_data=${3:-0}
loss_ack=${4:-0}
corrupt=${5:-0}
# El límite de la cola alcanza para el producto velocidad por retardo de estos ejemplos.
docker exec "$srv" tc qdisc add dev eth1 root netem limit 20000 \
  delay "${one_way}ms" rate "$rate" loss "${loss_data}%" corrupt "${corrupt}%"
docker exec "$r" tc qdisc add dev eth3 root netem limit 20000 \
  delay "${one_way}ms" rate "$rate" loss "${loss_ack}%"
par/par.clab.ymlDescargar par/par.clab.yml
# Dos clientes y un servidor, separados por un router. Es la red de la Sesión A: sirve para
# ver la demultiplexación y para medir un protocolo real de stop-and-wait.
#
#    h1 ---\
#           r --- srv        los datos de una transferencia van de srv hacia h1
#    h2 ---/
name: w09-par
mgmt:
  network: redes-w09-par
  ipv4-subnet: 172.90.9.0/26
  ipv6-subnet: 3fff:172:90:9::/80
topology:
  defaults:
    kind: linux
    image: redes-router
    binds: [..:/w09]
  nodes:
    r:
      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.209.21.1/24 dev eth1
        - ip addr add 10.209.22.1/24 dev eth2
        - ip addr add 10.209.23.1/24 dev eth3
    h1:
      exec:
        - ip route del default
        - sysctl -w net.ipv6.conf.all.disable_ipv6=1
        - ip addr add 10.209.21.10/24 dev eth1
        - ip route add default via 10.209.21.1
    h2:
      exec:
        - ip route del default
        - sysctl -w net.ipv6.conf.all.disable_ipv6=1
        - ip addr add 10.209.22.10/24 dev eth1
        - ip route add default via 10.209.22.1
    srv:
      exec:
        - ip route del default
        - sysctl -w net.ipv6.conf.all.disable_ipv6=1
        - ip addr add 10.209.23.10/24 dev eth1
        - ip route add default via 10.209.23.1
  # Containerlab usa MTU 9500 por defecto; aquí queremos el valor de Ethernet.
  links:
    - {endpoints: ["h1:eth1", "r:eth1"], mtu: 1500}
    - {endpoints: ["h2:eth1", "r:eth2"], mtu: 1500}
    - {endpoints: ["srv:eth1", "r:eth3"], mtu: 1500}
ensayo/e1/frr.confDescargar ensayo/e1/frr.conf
frr defaults traditional
hostname e1
interface eth1
 ip address 10.209.2.2/30
interface dum0
 ip address 100.64.11.1/24
router bgp 64911
 bgp router-id 10.209.0.11
 no bgp ebgp-requires-policy
 timers bgp 3 9
 neighbor 10.209.2.1 remote-as 64091
 neighbor 10.209.2.1 description t1 (proveedor)
 neighbor 10.209.2.1 advertisement-interval 0
 address-family ipv4 unicast
  network 100.64.11.0/24
  neighbor 10.209.2.1 route-map SOLO-PROPIAS out
 exit-address-family
bgp as-path access-list PROPIAS seq 5 permit ^$
route-map SOLO-PROPIAS permit 10
 match as-path PROPIAS
ensayo/e2/frr.confDescargar ensayo/e2/frr.conf
frr defaults traditional
hostname e2
interface eth1
 ip address 10.209.3.2/30
interface eth2
 ip address 10.209.5.2/30
interface dum0
 ip address 100.64.12.1/24
router bgp 64912
 bgp router-id 10.209.0.12
 no bgp ebgp-requires-policy
 timers bgp 3 9
 neighbor 10.209.3.1 remote-as 64901
 neighbor 10.209.3.1 description t1 (proveedor)
 neighbor 10.209.3.1 advertisement-interval 0
 neighbor 10.209.5.1 remote-as 64902
 neighbor 10.209.5.1 description t2 (proveedor)
 neighbor 10.209.5.1 advertisement-interval 0
 address-family ipv4 unicast
  network 100.64.21.0/24
  neighbor 10.209.3.1 route-map SOLO-PROPIAS out
  neighbor 10.209.5.1 route-map SOLO-PROPIAS out
 exit-address-family
bgp as-path access-list PROPIAS seq 5 permit ^$
route-map SOLO-PROPIAS permit 10
 match as-path PROPIAS
ensayo/e3/frr.confDescargar ensayo/e3/frr.conf
frr defaults traditional
hostname e3
interface eth1
 ip address 10.209.4.2/30
interface eth2
 ip address 10.209.6.2/30
interface eth3
 ip address 10.209.8.1/30
interface dum0
 ip address 100.64.13.1/24
router bgp 64913
 bgp router-id 10.209.0.13
 no bgp ebgp-requires-policy
 timers bgp 3 9
 neighbor 10.209.4.1 remote-as 64901
 neighbor 10.209.4.1 description t1 (proveedor)
 neighbor 10.209.4.1 advertisement-interval 0
 neighbor 10.209.6.1 remote-as 64902
 neighbor 10.209.6.1 description t2 (proveedor)
 neighbor 10.209.6.1 advertisement-interval 0
 neighbor 10.209.8.2 remote-as 64914
 neighbor 10.209.8.2 description e4 (peer)
 neighbor 10.209.8.2 advertisement-interval 0
 address-family ipv4 unicast
  network 100.64.13.0/24
  neighbor 10.209.4.1 route-map SOLO-PROPIAS out
  neighbor 10.209.6.1 route-map NADA out
  neighbor 10.209.8.2 route-map SOLO-PROPIAS out
 exit-address-family
bgp as-path access-list PROPIAS seq 5 permit ^$
route-map SOLO-PROPIAS permit 10
 match as-path PROPIAS
route-map NADA deny 10
ensayo/e4/frr.confDescargar ensayo/e4/frr.conf
frr defaults traditional
hostname e4
interface eth1
 ip address 10.209.7.2/30
interface eth2
 ip address 10.209.8.2/30
interface dum0
 ip address 100.64.14.1/24
router bgp 64914
 bgp router-id 10.209.0.14
 no bgp ebgp-requires-policy
 timers bgp 3 9
 neighbor 10.209.7.1 remote-as 64902
 neighbor 10.209.7.1 description t2 (proveedor)
 neighbor 10.209.7.1 advertisement-interval 0
 neighbor 10.209.8.1 remote-as 64913
 neighbor 10.209.8.1 description e3 (peer)
 neighbor 10.209.8.1 advertisement-interval 0
 address-family ipv4 unicast
  network 100.64.14.0/24
  neighbor 10.209.7.1 route-map SOLO-PROPIAS out
  neighbor 10.209.8.1 route-map SOLO-PROPIAS out
 exit-address-family
bgp as-path access-list PROPIAS seq 5 permit ^$
route-map SOLO-PROPIAS permit 10
 match as-path PROPIAS
ensayo/t1/frr.confDescargar ensayo/t1/frr.conf
frr defaults traditional
hostname t1
interface eth1
 ip address 10.209.1.1/30
interface eth2
 ip address 10.209.2.1/30
interface eth3
 ip address 10.209.3.1/30
interface eth4
 ip address 10.209.4.1/30
router bgp 64901
 bgp router-id 10.209.0.1
 no bgp ebgp-requires-policy
 timers bgp 3 9
 neighbor 10.209.1.2 remote-as 64902
 neighbor 10.209.1.2 description t2 (peer)
 neighbor 10.209.1.2 advertisement-interval 0
 neighbor 10.209.2.2 remote-as 64911
 neighbor 10.209.2.2 description e1 (cliente)
 neighbor 10.209.2.2 advertisement-interval 0
 neighbor 10.209.3.2 remote-as 64912
 neighbor 10.209.3.2 description e2 (cliente)
 neighbor 10.209.3.2 advertisement-interval 0
 neighbor 10.209.4.2 remote-as 64913
 neighbor 10.209.4.2 description e3 (cliente)
 neighbor 10.209.4.2 advertisement-interval 0
 address-family ipv4 unicast
 exit-address-family
ensayo/t2/frr.confDescargar ensayo/t2/frr.conf
frr defaults traditional
hostname t2
interface eth1
 ip address 10.209.1.2/30
interface eth2
 ip address 10.209.5.1/30
interface eth3
 ip address 10.209.6.1/30
interface eth4
 ip address 10.209.7.1/30
router bgp 64902
 bgp router-id 10.209.0.2
 no bgp ebgp-requires-policy
 timers bgp 3 9
 neighbor 10.209.1.1 remote-as 64901
 neighbor 10.209.1.1 description t1 (peer)
 neighbor 10.209.1.1 advertisement-interval 0
 neighbor 10.209.5.2 remote-as 64912
 neighbor 10.209.5.2 description e2 (cliente)
 neighbor 10.209.5.2 advertisement-interval 0
 neighbor 10.209.6.2 remote-as 64913
 neighbor 10.209.6.2 description e3 (cliente)
 neighbor 10.209.6.2 advertisement-interval 0
 neighbor 10.209.7.2 remote-as 64914
 neighbor 10.209.7.2 description e4 (cliente)
 neighbor 10.209.7.2 advertisement-interval 0
 address-family ipv4 unicast
  neighbor 10.209.7.2 prefix-list DE-E4 in
 exit-address-family
ip prefix-list DE-E4 seq 5 permit 100.64.41.0/24

Pruebas

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

test_ensayo.pyDescargar test_ensayo.py
"""Pruebas del ensayo del hackathon: las fallas se ven como dicen los apuntes y, con los
arreglos de lab/, el tablero se llena. Las de los arreglos se saltan si hay TODO."""
import re
import unittest

import ensayo
import tablero
from netlab import Lab, answer_path, wait_until

LAB = Lab("ensayo", "w09-ensayo", "redes-w09-ensayo")
TEAMS = tablero.read_teams()
NAMES = [team[0] for team in TEAMS]


def setUpModule():
    unittest.addModuleCleanup(LAB.destroy)
    LAB.deploy()
    # Esperamos a que estén arriba las sesiones que sí pueden subir.
    if not wait_until(sessions_settled, seconds=90, step=2):
        raise RuntimeError("las sesiones no subieron como se esperaba:\n" + LAB.vtysh("t1", "show bgp summary"))


def sessions_settled():
    down = tablero.sessions_down(TEAMS)
    return [item[:2] for item in down] == [("e1", "10.209.2.1")]


def pairs_that_fail():
    matrix = tablero.reachability(TEAMS)
    return {pair for pair, good in matrix.items() if not good}


def everything_except(*good):
    pairs = {(a, b) for a in NAMES for b in NAMES if a != b}
    return pairs - set(good) - {(b, a) for a, b in good}


class A_TheFaults(unittest.TestCase):
    """Corre antes de cargar los arreglos: el orden alfabético de las clases importa."""

    def test_board_shows_only_the_peering_alive(self):
        expected = everything_except(("e3", "e4"))
        self.assertTrue(wait_until(lambda: pairs_that_fail() == expected, seconds=40, step=3),
                        pairs_that_fail())

    def test_e1_has_the_wrong_as_for_its_provider(self):
        self.assertIn("64091", LAB.vtysh("e1", "show bgp summary"))
        state = LAB.vtysh("e1", "show bgp neighbors 10.209.2.1")
        self.assertNotIn("BGP state = Established", state)

    def test_e2_network_statement_matches_nothing(self):
        route = LAB.vtysh("e2", "show bgp ipv4 unicast 100.64.21.0/24")
        self.assertIn("import-check", route)
        self.assertIn("Not advertised", route)
        self.assertIn("Network not in table", LAB.vtysh("t1", "show bgp ipv4 unicast 100.64.12.0/24"))

    def test_t2_filter_rejects_the_announcement_of_e4(self):
        self.assertIn("Network not in table", LAB.vtysh("t2", "show bgp ipv4 unicast 100.64.14.0/24"))
        self.assertIn("100.64.14.0/24",
                      LAB.vtysh("e4", "show bgp ipv4 unicast neighbors 10.209.7.1 advertised-routes"))

    def test_e3_says_nothing_to_t2(self):
        table = LAB.vtysh("e3", "show bgp ipv4 unicast neighbors 10.209.6.1 advertised-routes")
        self.assertNotIn("100.64.13.0/24", table)
        self.assertIn("100.64.13.0/24",
                      LAB.vtysh("e3", "show bgp ipv4 unicast neighbors 10.209.4.1 advertised-routes"))


class B_TheFixes(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        for router in ensayo.FIXED_ROUTERS:
            answer_path(f"{router}.conf")
        ensayo.load_fixes()

    def setUp(self):
        self.assertTrue(wait_until(lambda: not pairs_that_fail(), seconds=90, step=3),
                        f"siguen sin alcanzarse: {sorted(pairs_that_fail())}")

    def cut_and_expect(self, a, b, expected):
        ensayo.set_link(a, b, "down")
        self.addCleanup(ensayo.set_link, a, b, "up")
        self.assertTrue(wait_until(lambda: pairs_that_fail() == expected, seconds=60, step=3),
                        f"sin {a}-{b}, fallan {sorted(pairs_that_fail())}")

    def test_01_every_team_reaches_every_team(self):
        self.assertEqual(tablero.sessions_down(TEAMS), [])

    def test_02_two_providers_survive_losing_one(self):
        self.cut_and_expect("t1", "e3", set())

    def test_03_a_peer_does_not_replace_a_provider(self):
        self.cut_and_expect("t2", "e4", {("e1", "e4"), ("e4", "e1"), ("e2", "e4"), ("e4", "e2")})

    def test_04_cutting_the_peering_of_the_providers_splits_the_single_homed(self):
        self.cut_and_expect("t1", "t2", {("e1", "e4"), ("e4", "e1")})

    def test_05_one_provider_means_isolated(self):
        expected = {(a, b) for a in NAMES for b in NAMES if a != b and "e1" in (a, b)}
        self.cut_and_expect("t1", "e1", expected)

    def test_06_render_says_how_many(self):
        text = tablero.render(TEAMS, tablero.reachability(TEAMS))
        self.assertTrue(re.search(r"12 de 12 pares", text), text)


if __name__ == "__main__":
    unittest.main()
test_par.pyDescargar test_par.py
"""Pruebas sobre la red de la Sesión A: demultiplexación, UDP y un stop-and-wait real."""
import re
import time
import unittest

import udp_checksum
from netlab import HERE, Lab, run

LAB = Lab("par", "w09-par", "redes-w09-par")
H1, H2, SRV = "10.209.21.10", "10.209.22.10", "10.209.23.10"
PORT = "9009"
FOLDER = "/tmp/tftp"


def setUpModule():
    unittest.addModuleCleanup(LAB.destroy)
    LAB.deploy()
    # Un archivo chico y otro más grande, aleatorios, para la transferencia.
    LAB.out("srv", "sh", "-c", f"mkdir -p {FOLDER} && head -c 25600 /dev/urandom > {FOLDER}/25k")


def link(*args):
    done = run(["sh", f"{HERE}/par/enlace.sh", *args], timeout=30)
    if done.returncode != 0:
        raise RuntimeError(done.stdout + done.stderr)


def client(node, protocol, source_port, message, target_port=PORT, *rest):
    return LAB.out(node, "/w09/bin/demux_client", protocol, SRV, target_port, str(source_port),
                   message, *rest)


class A_Demultiplexing(unittest.TestCase):
    def tearDown(self):
        for program in ("udp_demux", "tcp_demux"):
            LAB.sh("srv", "pkill", program)
        time.sleep(0.2)

    def start(self, command):
        LAB.daemon("srv", f"/w09/bin/{command} >/tmp/demux.log 2>&1")
        time.sleep(0.5)

    def log(self):
        return LAB.out("srv", "cat", "/tmp/demux.log")

    def test_udp_one_socket_for_every_client(self):
        self.start(f"udp_demux {PORT}")
        replies = [client("h1", "udp", 5000, "uno"), client("h2", "udp", 5000, "dos"),
                   client("h1", "udp", 5001, "tres")]
        self.assertEqual(len(set(replies)), 1, replies)
        for peer in (f"{H1}:5000", f"{H2}:5000", f"{H1}:5001"):
            self.assertIn(peer, self.log())
        sockets = [line for line in LAB.out("srv", "ss", "-uan").splitlines() if f":{PORT}" in line]
        self.assertEqual(len(sockets), 1, sockets)

    def test_connected_udp_socket_matches_the_source_too(self):
        self.start(f"udp_demux {PORT} {H1} 5000")
        exact = client("h1", "udp", 5000, "uno")
        other_host = client("h2", "udp", 5000, "dos")
        other_port = client("h1", "udp", 5001, "tres")
        self.assertNotEqual(exact, other_host)
        self.assertEqual(other_host, other_port)

    def test_tcp_one_socket_per_connection(self):
        self.start(f"tcp_demux {PORT}")
        for node, source_port in (("h1", 5000), ("h2", 5000), ("h1", 5001)):
            LAB.daemon(node, f"/w09/bin/demux_client tcp {SRV} {PORT} {source_port} hola 3")
        time.sleep(1.0)
        sockets = LAB.out("srv", "ss", "-tan")
        established = [line for line in sockets.splitlines()
                       if line.startswith("ESTAB") and f":{PORT}" in line]
        self.assertEqual(len(established), 3, sockets)
        for peer in (f"{H1}:5000", f"{H2}:5000", f"{H1}:5001"):
            self.assertTrue(any(peer in line for line in established), peer)
        self.assertEqual(len(set(re.findall(r"socket (\d+)  de", self.log()))), 3, self.log())
        time.sleep(2.5)

    def test_closed_ports(self):
        self.assertIn("Connection refused", client("h1", "tcp", 5002, "x"))
        self.assertIn("Connection refused", client("h1", "udp-connect", 5003, "x"))
        # Sin connect, el aviso de ICMP no le llega a la aplicación: solo vence su espera.
        self.assertIn("temporarily unavailable", client("h1", "udp", 5004, "x"))


class B_Checksum(unittest.TestCase):
    def test_my_calculation_matches_a_real_datagram(self):
        LAB.daemon("srv", "tcpdump -i eth1 -n -U -w /tmp/c.pcap udp port 9009 >/dev/null 2>&1")
        time.sleep(1.0)
        client("h1", "udp", 5000, "hola")
        time.sleep(1.0)
        LAB.sh("srv", "pkill", "tcpdump")
        time.sleep(0.5)
        dump = LAB.out("srv", "tcpdump", "-r", "/tmp/c.pcap", "-n", "-x", "-c", "1")
        hexa = "".join(re.findall(r"0x[0-9a-f]+:\s+((?:[0-9a-f]{4} ?)+)", dump)).replace(" ", "")
        self.assertEqual(hexa[:2], "45", dump)
        udp = bytes.fromhex(hexa[40:])
        self.assertEqual(udp[8:], b"hola")
        captured = int.from_bytes(udp[6:8], "big")
        self.assertEqual(captured, udp_checksum.udp_checksum(H1, SRV, 5000, 9009, b"hola"))


class C_RealStopAndWait(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        LAB.daemon("srv", f"python3 /w09/tftp_mini.py serve {FOLDER} --timeout-ms 200 >/tmp/tftp.log 2>&1")
        time.sleep(1.0)

    @classmethod
    def tearDownClass(cls):
        LAB.sh("srv", "pkill", "-f", "tftp_mini")
        link("limpio")

    def get(self, *flags):
        output = LAB.out("h1", "python3", "/w09/tftp_mini.py", "get", SRV, "25k", "/tmp/out", *flags,
                         timeout=120)
        match = re.search(r"tasa=([\d.]+) kbit/s duplicados=(\d+) sha256=(\w+)", output)
        self.assertIsNotNone(match, output)
        return float(match[1]), int(match[2]), match[3]

    def original_hash(self):
        return LAB.out("srv", "sha256sum", f"{FOLDER}/25k")[:16]

    def test_rate_is_set_by_the_rtt(self):
        link("100mbit", "30")
        at_30 = self.get()[0]
        link("100mbit", "10")
        at_10 = self.get()[0]
        expected_30 = 512 * 8 / 30
        self.assertLess(at_30, expected_30 * 1.1)
        self.assertGreater(at_30, expected_30 * 0.6)
        self.assertGreater(at_10, at_30 * 2)

    def test_a_faster_link_does_not_help(self):
        link("100mbit", "30")
        slow = self.get()[0]
        link("1gbit", "30")
        fast = self.get()[0]
        self.assertLess(fast, slow * 1.3)

    def test_survives_loss_with_the_right_hash(self):
        link("100mbit", "10", "10", "10")
        self.assertEqual(self.get()[2], self.original_hash())

    def test_without_block_numbers_a_lost_ack_duplicates_data(self):
        link("100mbit", "10", "0", "15")
        for _ in range(3):
            _, duplicates, digest = self.get("--ignore-block-number")
            if duplicates:
                self.assertNotEqual(digest, self.original_hash())
                return
        self.fail("no se perdió ningún ACK en tres transferencias")

    def test_udp_checksum_turns_damage_into_loss(self):
        link("100mbit", "10", "0", "0", "10")
        before = self.csum_errors()
        self.assertEqual(self.get()[2], self.original_hash())
        self.assertGreater(self.csum_errors(), before)

    def csum_errors(self):
        lines = [line for line in LAB.out("h1", "cat", "/proc/net/snmp").splitlines()
                 if line.startswith("Udp:")]
        return int(lines[1].split()[lines[0].split().index("InCsumErrors")])


class D_WindowWithTcp(unittest.TestCase):
    """TCP como protocolo real de ventana: más ventana, más throughput, hasta llenar el enlace."""

    @classmethod
    def setUpClass(cls):
        link("100mbit", "30")
        LAB.daemon("h1", "iperf3 -s -p 5209 >/dev/null 2>&1")
        time.sleep(1.0)

    @classmethod
    def tearDownClass(cls):
        LAB.sh("h1", "pkill", "iperf3")
        link("limpio")

    def throughput(self, *options):
        output = LAB.out("srv", "iperf3", "-c", H1, "-p", "5209", "-t", "3", "-f", "m", *options,
                         timeout=60)
        match = re.search(r"([\d.]+) Mbits/sec\s+receiver", output)
        self.assertIsNotNone(match, output)
        return float(match[1])

    def test_doubling_the_window_helps_and_auto_tuning_beats_stop_and_wait(self):
        small = self.throughput("-w", "16K")
        large = self.throughput("-w", "64K")
        auto = self.throughput()
        self.assertGreater(large, small * 1.8)
        self.assertGreater(auto, large)
        # stop-and-wait con paquetes de 1500 bytes y 30 ms: 0,4 Mbps.
        self.assertGreater(small, 0.4 * 5)
        self.assertGreater(auto, 0.4 * 50)


if __name__ == "__main__":
    unittest.main()
test_sim.pyDescargar test_sim.py
"""Pruebas de los simuladores y del checksum: no necesitan ninguna topología."""
import unittest

import rdt_sim
import seq_space
import udp_checksum
import window_sim


class A_BuildingUp(unittest.TestCase):
    MESSAGES = rdt_sim.MESSAGES

    def run_it(self, name):
        return rdt_sim.run_scenario(name)

    def test_v1_works_on_a_perfect_channel(self):
        self.assertEqual(self.run_it("v1-perfecto").delivered, self.MESSAGES)

    def test_v1_delivers_garbage_without_noticing(self):
        result = self.run_it("v1-errores")
        self.assertIn("?!?", result.delivered)
        self.assertTrue(result.finished)

    def test_v2_recovers_from_damaged_data(self):
        self.assertEqual(self.run_it("v2-datos").delivered, self.MESSAGES)

    def test_v2_duplicates_when_the_ack_is_damaged(self):
        self.assertEqual(self.run_it("v2-ack").delivered, ["a", "a", "b", "c"])

    def test_the_sequence_bit_fixes_it(self):
        self.assertEqual(self.run_it("v21-ack").delivered, self.MESSAGES)

    def test_no_nak_is_needed(self):
        result = self.run_it("v22-datos")
        self.assertEqual(result.delivered, self.MESSAGES)
        self.assertFalse(any("NAK" in text for _, _, text in result.trace))

    def test_without_a_timer_a_loss_hangs_forever(self):
        result = self.run_it("v22-perdida")
        self.assertFalse(result.finished)
        self.assertEqual(result.delivered, ["a"])

    def test_v3_survives_loss_in_both_directions_and_a_late_ack(self):
        for name in ("v3-dato", "v3-ack", "v3-tarde"):
            with self.subTest(name):
                result = self.run_it(name)
                self.assertTrue(result.finished)
                self.assertEqual(result.delivered, self.MESSAGES)

    def test_a_timer_without_sequence_numbers_duplicates(self):
        self.assertEqual(self.run_it("v3-sin-seq").delivered, ["a", "a", "b", "c"])


class B_Traces(unittest.TestCase):
    """La pregunta de repaso: ventana 4, se pierde el 2 de los paquetes 0 a 5."""

    def trace(self, protocol):
        trace = []
        config = window_sim.Config(protocol=protocol, window=4, packets=6,
                                   drop_data=[(2, 1)])
        return window_sim.WindowSim(config, trace).run(), trace

    def test_gbn_resends_everything_from_the_lost_packet(self):
        stats, trace = self.trace("gbn")
        self.assertEqual(stats.delivered, list(range(6)))
        self.assertEqual(stats.transmissions, 10)
        repeated = [text for _, _, text in trace if text.startswith("repite")]
        self.assertEqual(repeated, ["repite 2", "repite 3", "repite 4", "repite 5"])

    def test_gbn_receiver_discards_and_repeats_the_last_ack(self):
        _, trace = self.trace("gbn")
        discarded = [text for _, _, text in trace if "lo descarta" in text]
        self.assertEqual(len(discarded), 3)

    def test_sr_resends_only_the_lost_packet(self):
        stats, trace = self.trace("sr")
        self.assertEqual(stats.delivered, list(range(6)))
        self.assertEqual(stats.transmissions, 7)
        repeated = [text for _, _, text in trace if text.startswith("repite")]
        self.assertEqual(repeated, ["repite 2"])

    def test_sr_delivers_in_order_even_when_it_receives_out_of_order(self):
        _, trace = self.trace("sr")
        self.assertTrue(any("entrega el contenido [2, 3, 4, 5]" in text for _, _, text in trace))


class C_Utilization(unittest.TestCase):
    def test_the_week_example(self):
        d = 1500 * 8 / 1e6  # ms a 1 Gbps
        expected = window_sim.stop_and_wait_utilization(d, 30)
        self.assertAlmostEqual(expected, 0.0004, places=5)
        stats = window_sim.simulate(protocol="gbn", window=1, packets=100, d=d, rtt=30)
        self.assertAlmostEqual(stats.utilization(d), expected, places=6)
        self.assertEqual(window_sim.window_to_fill(d, 30), 2501)

    def test_the_review_question(self):
        d = 1500 * 8 / 1e5  # ms a 100 Mbps
        self.assertAlmostEqual(window_sim.stop_and_wait_utilization(d, 40), 0.00299, places=5)
        self.assertEqual(window_sim.window_to_fill(d, 40), 335)

    def test_utilization_grows_with_the_window_until_it_fills_the_link(self):
        d, rtt = 1.0, 40.0
        for window, packets in [(1, 200), (10, 400), (20, 800), (41, 1600), (80, 3200)]:
            stats = window_sim.simulate(protocol="sr", window=window, packets=packets, d=d, rtt=rtt)
            self.assertAlmostEqual(stats.utilization(d), window_sim.utilization(window, d, rtt),
                                   delta=0.05, msg=f"ventana {window}")


class D_Comparison(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.both = window_sim.sweep(window=30, packets=1000, seeds=3, losses=(0, 0.01, 0.1))
        cls.acks = window_sim.sweep(window=30, packets=1000, seeds=3, losses=(0.05,), ack_only=True)

    def test_without_loss_they_are_the_same(self):
        row = self.both[0]
        self.assertEqual(row["gbn"], row["sr"])
        self.assertEqual(row["gbn"][0], 1.0)

    def test_with_loss_gbn_sends_more_and_the_gap_widens(self):
        low, high = self.both[1], self.both[2]
        self.assertGreater(low["gbn"][0], low["sr"][0])
        self.assertGreater(high["gbn"][0] / high["sr"][0], low["gbn"][0] / low["sr"][0])

    def test_loss_of_acks_alone_favors_the_cumulative_ack(self):
        row = self.acks[0]
        self.assertLess(row["gbn"][0], row["sr"][0])
        self.assertGreater(row["gbn"][1], row["sr"][1])


class E_SequenceSpace(unittest.TestCase):
    def test_sr_with_four_numbers_and_window_three_delivers_old_data_as_new(self):
        stats, _ = window_sim.ambiguity_run("sr", 4, 3)
        self.assertNotEqual(stats.delivered, list(range(len(stats.delivered))))

    def test_sr_with_four_numbers_and_window_two_is_correct(self):
        stats, _ = window_sim.ambiguity_run("sr", 4, 2)
        self.assertTrue(stats.finished)
        self.assertEqual(stats.delivered, list(range(8)))

    def test_gbn_limits_by_simulation(self):
        good, _ = window_sim.ambiguity_run("gbn", 4, 3)
        self.assertEqual(good.delivered, list(range(8)))
        bad, _ = window_sim.ambiguity_run("gbn", 4, 4)
        self.assertNotEqual(bad.delivered, list(range(len(bad.delivered))))

    def test_the_limits_for_every_size(self):
        for bits in range(1, 8):
            modulus = 2 ** bits
            self.assertEqual(seq_space.largest_safe_window("gbn", modulus), modulus - 1)
            self.assertEqual(seq_space.largest_safe_window("sr", modulus), modulus // 2)


class F_Checksum(unittest.TestCase):
    def test_the_datagram_of_the_notes(self):
        value = udp_checksum.udp_checksum("10.209.21.10", "10.209.23.10", 5000, 9009, b"hola")
        self.assertEqual(value, 0xb296)

    def test_odd_length_is_padded(self):
        even = udp_checksum.udp_checksum("10.0.0.1", "10.0.0.2", 1, 2, b"ab\0")
        odd = udp_checksum.udp_checksum("10.0.0.1", "10.0.0.2", 1, 2, b"ab")
        # El relleno no cuenta en el largo: los dos datos solo difieren en esa palabra de largo.
        self.assertEqual(sum(udp_checksum.words(b"ab\0")), sum(udp_checksum.words(b"ab")))
        self.assertNotEqual(even, odd)

    def test_a_computed_zero_is_sent_as_all_ones(self):
        base = udp_checksum.ones_complement_sum(udp_checksum.words(
            udp_checksum.pseudo_header("10.0.0.1", "10.0.0.2", 10)
            + bytes([0, 1, 0, 2, 0, 10, 0, 0])))
        filler = (0xffff - base) & 0xffff
        value = udp_checksum.udp_checksum("10.0.0.1", "10.0.0.2", 1, 2, filler.to_bytes(2, "big"))
        self.assertEqual(value, 0xffff)

    def test_swapping_two_words_goes_undetected(self):
        first = udp_checksum.udp_checksum("10.0.0.1", "10.0.0.2", 1, 2, b"abcd")
        second = udp_checksum.udp_checksum("10.0.0.1", "10.0.0.2", 1, 2, b"cdab")
        self.assertEqual(first, second)


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

Laboratorio

Archivos de partida del laboratorio de la semana.

lab/e1.confDescargar lab/e1.conf
! Arreglo de e1 (AS 64911), en sintaxis de vtysh: se carga con python3 ensayo.py arreglos.
! Paso 2 del ensayo. Diagnostiquen primero con tablero.py y con show bgp summary.
! TODO: escriban aquí los comandos que hacen falta y borren esta línea.
lab/e2.confDescargar lab/e2.conf
! Arreglo de e2 (AS 64912), en sintaxis de vtysh: se carga con python3 ensayo.py arreglos.
! Paso 2 del ensayo. Comparen lo que e2 tiene en sus interfaces con lo que anuncia.
! TODO: escriban aquí los comandos que hacen falta y borren esta línea.
lab/e3.confDescargar lab/e3.conf
! Arreglo de e3 (AS 64913), en sintaxis de vtysh: se carga con python3 ensayo.py arreglos.
! Paso 3 del ensayo. El tablero no delata esta falla: busquen con qué vecinos habla e3 y
! qué le anuncia a cada uno.
! TODO: escriban aquí los comandos que hacen falta y borren esta línea.
lab/t2.confDescargar lab/t2.conf
! Arreglo de t2 (AS 64902), en sintaxis de vtysh: se carga con python3 ensayo.py arreglos.
! Paso 2 del ensayo. Comparen lo que un cliente de t2 anuncia con lo que t2 deja entrar.
! TODO: escriban aquí los comandos que hacen falta y borren esta línea.

Compilación

Cómo se compilan y ejecutan los ejemplos.

MakefileDescargar Makefile
# Semana 9: transporte, UDP y entrega confiable. Se usa dentro del contenedor del curso.
#
#   make              compila los programas de demultiplexación (estáticos, para correr
#                     dentro de los nodos)
#   make test         compila, corre los simuladores, levanta las dos topologías una tras
#                     otra y corre las pruebas (test_*.py); siempre destruye lo que levantó.
#                     Las pruebas de los arreglos usan lab/ y se saltan hasta que los completen
#   make up           levanta la red de cuatro nodos (par/par.clab.yml)
#   make up-ensayo    levanta el ensayo del hackathon, con sus fallas (ensayo/ensayo.clab.yml)
#   make down         destruye las dos topologías y sus redes de Docker
#   make clean        lo anterior, y borra los binarios

CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -D_DEFAULT_SOURCE -O2 -g
LDFLAGS = -static
PROGRAMS = bin/udp_demux bin/tcp_demux bin/demux_client

.PHONY: all test up up-ensayo down clean

all: $(PROGRAMS)

bin/%: %.c
	@mkdir -p bin
	$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)

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 par && containerlab deploy -t par.clab.yml --reconfigure

up-ensayo:
	cd ensayo && containerlab deploy -t ensayo.clab.yml --reconfigure

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

clean: down
	rm -rf bin __pycache__ par/clab-w09-par ensayo/clab-w09-ensayo