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:
- RFC 768, UDP.
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.
- Cada equipo entrega su carpeta
as/antes de iniciar la sesión. - 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. - 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.
- Diagnosticar lo que falla con
show bgp summary,show bgp ipv4ytraceroute. - El docente corta un enlace de tránsito. Cada equipo comprueba si sigue conectado.
- Discusión final: qué falló y cuánto de eso era técnico y cuánto era coordinación entre equipos.
- 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. Verprojects/README.md.
Preguntas de repaso
- Dos clientes hablan con el mismo puerto de un servidor. ¿Cómo separa el sistema operativo esos datos en UDP? ¿Y en TCP?
- 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?
- ¿Para qué se necesitan números de secuencia si ya existe el temporizador?
- 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.
- ¿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.
// 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;
}#!/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()"""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()#!/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()#!/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()#!/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()// 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");
}
}
}
}#!/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()#!/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()// 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);
}
}
}
}#!/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 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}# 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#!/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}%"# 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}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 PROPIASfrr 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 PROPIASfrr 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 10frr 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 PROPIASfrr 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-familyfrr 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/24Pruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
"""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()"""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()"""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.
! 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.! 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.! 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.! 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.
# 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