Semana 3 — Enlace, Ethernet y VLANs
Objetivos
- Explicar lo esencial de la capa física: medios, ancho de banda y modulación.
- Calcular detección de errores con paridad y CRC.
- Comparar protocolos de acceso múltiple.
- Describir un frame de Ethernet y explicar cómo aprende un switch.
- Explicar ARP y configurar VLANs en una red virtualizada.
Sesión A — Capa física en lo esencial · Enlace: framing y detección de errores
Lecturas:
- Kurose y Ross, secs. 1.2.2 (Physical Media), 6.1 (Introduction to the Link Layer) y 6.2 (Error-Detection and -Correction Techniques).
- CS168, Links.
Temas:
- Medios guiados y no guiados: cobre, fibra óptica y radio. Qué limita a cada uno.
- Ancho de banda, modulación y codificación, sin entrar en el detalle de señales.
- Servicios del enlace: framing, acceso al medio, entrega entre vecinos y detección de errores.
- Paridad, checksum y CRC. Ejemplo resuelto: calcular un CRC con un generador pequeño.
- Por qué el enlace usa CRC y el transporte usa un checksum más débil.
Sesión B — Acceso múltiple · Ethernet, ARP, switches y VLANs
Lecturas:
- Kurose y Ross, secs. 6.3 (Multiple Access Links and Protocols) y 6.4 (Switched Local Area Networks).
- CS168, Ethernet, Layer 2 Routing y ARP.
Lecturas complementarias:
- RFC 826, ARP.
Temas:
- Particionar el canal, acceso aleatorio (ALOHA y CSMA/CD) y turnos.
- El frame de Ethernet: direcciones MAC, EtherType y CRC.
- Direcciones MAC frente a direcciones IP: para qué sirve cada una.
- ARP: cómo se encuentra la MAC de un vecino. Por qué se envía la pregunta por broadcast y la respuesta por unicast.
- Switches: aprendizaje, reenvío selectivo y flooding. Por qué no hace falta configurarlos. Hubs como historia.
- Spanning tree: el problema de los ciclos y la tormenta de broadcast.
- VLANs: separar una red física en varias. Puertos access y trunk, y la etiqueta 802.1Q.
- Error común: creer que una VLAN por sí sola da comunicación entre redes. Para pasar de una VLAN a otra hace falta un router.
Laboratorio — Capturar ARP y configurar VLANs
Trabajen en code/week-03/, con las topologías lan (un switch Linux, o bridge, con varios hosts) y campus (dos switches y un router).
- Levantar la topología con
containerlab deployy mirarip neighen un host. Vaciar la tabla, hacer ping a un vecino y capturar el intercambio ARP. - Mirar en el switch, con
bridge fdb show, cómo aprende las direcciones MAC. Predecir qué puertos reciben el primer frame y comprobarlo contcpdumpen otro host. - Activar el filtrado de VLANs en el bridge y poner a los hosts en las VLANs 110 y 120. Comprobar qué pings dejan de funcionar.
- Pasar a la topología de dos switches, con un trunk entre ellos y otro hacia un router, y encaminar entre las VLANs. Encontrar la etiqueta 802.1Q en una captura.
- Entregar P0 y leer el enunciado de P1.
Entregas
- P0 entrega.
- P1 publicado: Tu red (direccionamiento, VLANs, NAT y OSPF de un sistema autónomo). Entrega en semana 6. Ver
projects/README.md.
Preguntas de repaso
- Calculen el CRC de una secuencia corta de bits con el generador 1001. ¿Qué errores detecta siempre un CRC de r bits?
- ¿Por qué CSMA/CD no se usa en una Ethernet conmutada moderna?
- Un host conoce la IP de un destino en otra subred. ¿De quién busca la MAC con ARP? ¿Por qué?
- Un switch recibe un frame hacia una MAC que no tiene en su tabla. ¿Qué hace? ¿Y si la MAC está en el mismo puerto por donde llegó?
- Dos hosts en distintas VLANs están conectados al mismo switch. ¿Qué tiene que pasar para que se comuniquen?
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (14)Pruebas (7)Laboratorio (3)Compilación (1)
Ejemplos
Los programas que usan los apuntes y la clase.
/*
* aloha.c: simula ALOHA ranurado y ALOHA puro, y compara con la fórmula.
*
* aloha [semilla]
*
* G es la carga ofrecida: frames (nuevos o reintentos) que se intenta enviar por cada
* tiempo de frame. S es el throughput: la fracción del tiempo con un frame exitoso.
* Los intentos son independientes (el modelo clásico): no se simula la cola de cada nodo.
*/
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#define NODES 50 /* nodos en el modelo ranurado */
#define SLOTS 400000 /* ranuras simuladas */
#define PURE_FRAMES 400000 /* intentos simulados en ALOHA puro */
/* Generador xorshift propio: el resultado es igual en todas las máquinas. */
static uint64_t rng_state;
static double uniform(void)
{
rng_state ^= rng_state << 13;
rng_state ^= rng_state >> 7;
rng_state ^= rng_state << 17;
return (double)(rng_state >> 11) / 9007199254740992.0; /* 2^53 */
}
/* Una ranura es exitosa si exactamente un nodo decide transmitir. */
static double simulate_slotted(double load)
{
double p = load / NODES;
long successes = 0;
for (long slot = 0; slot < SLOTS; slot++) {
int senders = 0;
for (int node = 0; node < NODES; node++) {
if (uniform() < p) {
senders++;
}
}
if (senders == 1) {
successes++;
}
}
return (double)successes / SLOTS;
}
/*
* Intentos en instantes aleatorios (proceso de Poisson de tasa G). Un frame dura 1 y
* sale bien solo si nadie empezó en el tiempo 1 anterior ni empieza en el tiempo 1
* siguiente: el período vulnerable mide 2.
*/
static double simulate_pure(double load)
{
double gap_before = -log(1.0 - uniform()) / load;
double total_time = 0.0;
long successes = 0;
for (long i = 0; i < PURE_FRAMES; i++) {
double gap_after = -log(1.0 - uniform()) / load;
if (gap_before > 1.0 && gap_after > 1.0) {
successes++;
}
total_time += gap_after;
gap_before = gap_after;
}
return (double)successes / total_time;
}
int main(int argc, char *argv[])
{
static const double loads[] = {0.1, 0.25, 0.5, 0.75, 1.0, 1.5, 2.0, 3.0};
uint64_t seed = 20260303;
if (argc > 2) {
fprintf(stderr, "uso: %s [semilla]\n", argv[0]);
return 2;
}
if (argc == 2) {
char *end;
seed = strtoull(argv[1], &end, 10);
if (*end != '\0' || seed == 0) {
fprintf(stderr, "la semilla debe ser un entero positivo\n");
return 2;
}
}
rng_state = seed;
printf(" G | ranurado sim G*e^-G | puro sim G*e^-2G\n");
for (size_t i = 0; i < sizeof loads / sizeof loads[0]; i++) {
double g = loads[i];
printf("%4.2f | %12.3f %6.3f | %8.3f %7.3f\n", g, simulate_slotted(g), g * exp(-g),
simulate_pure(g), g * exp(-2.0 * g));
}
return 0;
}/*
* arp_probe.c: arma un frame de Ethernet con una consulta ARP, byte por byte, y espera
* la respuesta. Hace a mano lo que el kernel hace antes de enviar el primer paquete a un
* vecino.
*
* arp_probe <interfaz> <ip del vecino>
*
* Necesita permiso para abrir un socket AF_PACKET (root dentro de un nodo).
*/
#define _DEFAULT_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <linux/if_packet.h>
#include <net/ethernet.h>
#include <net/if.h>
#include <netinet/in.h>
#include <poll.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>
#define ETH_HEADER_LEN 14
#define ARP_LEN 28
#define FRAME_LEN (ETH_HEADER_LEN + ARP_LEN)
#define ARP_OP_REQUEST 1
#define ARP_OP_REPLY 2
#define TIMEOUT_MS 1000
/* Escribe un valor de 16 bits en orden de red, sin depender del orden de la máquina. */
static void put_u16(uint8_t *p, uint16_t value)
{
p[0] = (uint8_t)(value >> 8);
p[1] = (uint8_t)(value & 0xff);
}
static uint16_t get_u16(const uint8_t *p)
{
return (uint16_t)((p[0] << 8) | p[1]);
}
static void print_mac(const uint8_t *mac)
{
printf("%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
}
static void print_hex(const uint8_t *data, size_t len)
{
for (size_t i = 0; i < len; i++) {
printf("%02x%c", data[i], (i % 14 == 13 || i + 1 == len) ? '\n' : ' ');
}
}
/* Frame completo: encabezado de Ethernet y mensaje ARP (RFC 826). */
static void build_request(uint8_t *frame, const uint8_t *my_mac, const uint8_t *my_ip,
const uint8_t *target_ip)
{
memset(frame, 0xff, 6); /* destino: broadcast */
memcpy(frame + 6, my_mac, 6); /* origen */
put_u16(frame + 12, ETHERTYPE_ARP); /* EtherType */
uint8_t *arp = frame + ETH_HEADER_LEN;
put_u16(arp + 0, 1); /* tipo de hardware: Ethernet */
put_u16(arp + 2, ETHERTYPE_IP); /* tipo de protocolo: IPv4 */
arp[4] = 6; /* largo de una dirección de hardware */
arp[5] = 4; /* largo de una dirección de protocolo */
put_u16(arp + 6, ARP_OP_REQUEST);
memcpy(arp + 8, my_mac, 6); /* quién pregunta */
memcpy(arp + 14, my_ip, 4);
memset(arp + 18, 0, 6); /* MAC buscada: todavía no se conoce */
memcpy(arp + 24, target_ip, 4);
}
static double elapsed_ms(const struct timespec *start)
{
struct timespec now;
if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) {
perror("clock_gettime");
return -1.0;
}
return (double)(now.tv_sec - start->tv_sec) * 1000.0 +
(double)(now.tv_nsec - start->tv_nsec) / 1e6;
}
/* Espera un ARP Reply del vecino. Devuelve 1 si llegó, 0 si venció el plazo, -1 si hay error. */
static int wait_reply(int fd, const uint8_t *target_ip, const struct timespec *start)
{
uint8_t buf[2048];
struct pollfd pfd = {.fd = fd, .events = POLLIN};
for (;;) {
double spent = elapsed_ms(start);
if (spent < 0.0) {
return -1;
}
if (spent >= TIMEOUT_MS) {
return 0;
}
int ready = poll(&pfd, 1, TIMEOUT_MS - (int)spent);
if (ready < 0) {
if (errno == EINTR) {
continue;
}
perror("poll");
return -1;
}
if (ready == 0) {
return 0;
}
ssize_t n = recv(fd, buf, sizeof buf, 0);
if (n < 0) {
perror("recv");
return -1;
}
if (n < FRAME_LEN) {
continue;
}
const uint8_t *arp = buf + ETH_HEADER_LEN;
if (get_u16(arp + 6) == ARP_OP_REPLY && memcmp(arp + 14, target_ip, 4) == 0) {
printf("respuesta en %.3f ms, la MAC del vecino es ", elapsed_ms(start));
print_mac(arp + 8);
printf("\nframe recibido (%zd bytes):\n", n);
print_hex(buf, (size_t)n);
return 1;
}
}
}
int main(int argc, char *argv[])
{
if (argc != 3) {
fprintf(stderr, "uso: %s <interfaz> <ip del vecino>\n", argv[0]);
return 2;
}
const char *ifname = argv[1];
if (strlen(ifname) >= IFNAMSIZ) {
fprintf(stderr, "nombre de interfaz demasiado largo\n");
return 2;
}
struct in_addr target;
if (inet_pton(AF_INET, argv[2], &target) != 1) {
fprintf(stderr, "dirección IPv4 no válida: %s\n", argv[2]);
return 2;
}
/* SOCK_RAW en AF_PACKET: lo que se envía y se recibe incluye el encabezado de Ethernet. */
int fd = socket(AF_PACKET, SOCK_RAW, htons(ETHERTYPE_ARP));
if (fd < 0) {
perror("socket");
return 1;
}
struct ifreq ifr;
memset(&ifr, 0, sizeof ifr);
strcpy(ifr.ifr_name, ifname);
if (ioctl(fd, SIOCGIFINDEX, &ifr) < 0) {
perror("ioctl(SIOCGIFINDEX)");
close(fd);
return 1;
}
int ifindex = ifr.ifr_ifindex;
if (ioctl(fd, SIOCGIFHWADDR, &ifr) < 0) {
perror("ioctl(SIOCGIFHWADDR)");
close(fd);
return 1;
}
uint8_t my_mac[6];
memcpy(my_mac, ifr.ifr_hwaddr.sa_data, 6);
if (ioctl(fd, SIOCGIFADDR, &ifr) < 0) {
perror("ioctl(SIOCGIFADDR)");
close(fd);
return 1;
}
struct sockaddr_in my_addr;
memcpy(&my_addr, &ifr.ifr_addr, sizeof my_addr);
struct sockaddr_ll where;
memset(&where, 0, sizeof where);
where.sll_family = AF_PACKET;
where.sll_protocol = htons(ETHERTYPE_ARP);
where.sll_ifindex = ifindex;
if (bind(fd, (struct sockaddr *)&where, sizeof where) < 0) {
perror("bind");
close(fd);
return 1;
}
uint8_t frame[FRAME_LEN];
build_request(frame, my_mac, (const uint8_t *)&my_addr.sin_addr,
(const uint8_t *)&target);
printf("frame enviado (%d bytes):\n", FRAME_LEN);
print_hex(frame, FRAME_LEN);
struct timespec start;
if (clock_gettime(CLOCK_MONOTONIC, &start) < 0) {
perror("clock_gettime");
close(fd);
return 1;
}
if (send(fd, frame, FRAME_LEN, 0) < 0) {
perror("send");
close(fd);
return 1;
}
int result = wait_reply(fd, (const uint8_t *)&target, &start);
if (close(fd) < 0) {
perror("close");
return 1;
}
if (result == 0) {
printf("sin respuesta en %d ms\n", TIMEOUT_MS);
return 1;
}
return result == 1 ? 0 : 1;
}#!/usr/bin/env python3
"""Comprueba el paso 4 del laboratorio en la topología campus ya levantada y configurada.
python3 check_lab.py VLANs 110 y 120, trunks y router
python3 check_lab.py --reto h4 en la VLAN 130, sin llevarla a donde no hace falta
"""
import json
import sys
from labutil import Lab
def vlans_on(lab, node, port):
"""Conjunto de VLAN permitidas en un puerto, según bridge -j vlan show."""
data = json.loads(lab.out(node, f"bridge -j vlan show dev {port}"))
return {entry["vlan"] for item in data for entry in item["vlans"]}
def checks_vlans(lab):
yield "sw1 tiene el filtrado de VLANs activo", "vlan_filtering 1" in lab.out(
"sw1", "ip -d link show br0")
yield "sw2 tiene el filtrado de VLANs activo", "vlan_filtering 1" in lab.out(
"sw2", "ip -d link show br0")
yield "el trunk de sw1 lleva 110 y 120, sin la VLAN 1", vlans_on(lab, "sw1", "eth3") == {110, 120}
yield "el trunk de sw2 hacia r1 lleva 110 y 120, sin la VLAN 1", vlans_on(
lab, "sw2", "eth4") == {110, 120}
yield "h1 llega a h3 (misma VLAN, por el trunk)", lab.ping("h1", "10.203.110.3", 2)
yield "h2 llega a h4 (misma VLAN, por el trunk)", lab.ping("h2", "10.203.120.4", 2)
yield "h1 llega a su gateway", lab.ping("h1", "10.203.110.254", 2)
yield "h1 llega a h2 pasando por r1", lab.ping("h1", "10.203.120.2", 2)
trace = lab.out("h1", "traceroute -n -w 1 -q 1 10.203.120.4")
yield "traceroute de h1 a h4 pasa por el gateway", "10.203.110.254" in trace
def checks_reto(lab):
yield "h4 llega a su gateway en la VLAN 130", lab.ping("h4", "10.203.130.254", 2)
yield "h1 llega a h4 pasando por r1", lab.ping("h1", "10.203.130.4", 2)
yield "h2 sigue llegando a h1", lab.ping("h2", "10.203.110.1", 2)
yield "el puerto de h4 es solo de la VLAN 130", vlans_on(lab, "sw2", "eth2") == {130}
yield "el trunk de sw2 hacia r1 lleva la VLAN 130", 130 in vlans_on(lab, "sw2", "eth4")
yield "la VLAN 130 no va hacia sw1, que no la necesita", 130 not in vlans_on(lab, "sw2", "eth3")
def main():
lab = Lab("campus")
checks = checks_reto(lab) if "--reto" in sys.argv else checks_vlans(lab)
failed = 0
for label, passed in checks:
print(f"{'ok ' if passed else 'FALLA'} {label}")
failed += not passed
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main())/*
* crc.c: CRC con un generador pequeño, mostrando la división paso a paso.
*
* crc <datos> <generador> calcula el CRC y la palabra a transmitir
* crc -c <palabra> <generador> revisa una palabra recibida
*
* Los dos argumentos son cadenas de ceros y unos, por ejemplo: crc 1101011 1001
* Es una herramienta para entender el cálculo, bit por bit; una tarjeta de red hace lo
* mismo con un registro de desplazamiento.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_BITS 256
/* Devuelve 1 si la cadena no está vacía y solo tiene ceros y unos. */
static int is_bit_string(const char *s)
{
return s[0] != '\0' && strspn(s, "01") == strlen(s);
}
/*
* División módulo 2 de buf (largo len) entre gen (largo gen_len). Deja el residuo en los
* últimos gen_len - 1 bits de buf. Si trace es distinto de cero, imprime cada resta.
*/
static void mod2_divide(char *buf, size_t len, const char *gen, size_t gen_len, int trace)
{
for (size_t i = 0; i + gen_len <= len; i++) {
if (buf[i] == '0') {
continue;
}
if (trace) {
printf(" %s\n", buf);
printf(" %*s%s (XOR en la posición %zu)\n", (int)i, "", gen, i);
}
for (size_t j = 0; j < gen_len; j++) {
buf[i + j] = (buf[i + j] == gen[j]) ? '0' : '1';
}
}
if (trace) {
printf(" %s\n", buf);
}
}
static void usage(const char *prog)
{
fprintf(stderr, "uso: %s <datos> <generador>\n", prog);
fprintf(stderr, " %s -c <palabra> <generador>\n", prog);
}
int main(int argc, char *argv[])
{
int check = argc == 4 && strcmp(argv[1], "-c") == 0;
if (!check && argc != 3) {
usage(argv[0]);
return 2;
}
const char *input = argv[argc - 2];
const char *gen = argv[argc - 1];
size_t input_len = strlen(input);
size_t gen_len = strlen(gen);
if (!is_bit_string(input) || !is_bit_string(gen)) {
fprintf(stderr, "los argumentos deben ser cadenas de ceros y unos\n");
return 2;
}
/* El generador define r = gen_len - 1 solo si su primer bit es 1. */
if (gen_len < 2 || gen[0] != '1') {
fprintf(stderr, "el generador debe tener al menos 2 bits y empezar con 1\n");
return 2;
}
if (input_len + gen_len > MAX_BITS) {
fprintf(stderr, "máximo %d bits en total\n", MAX_BITS);
return 2;
}
size_t r = gen_len - 1;
char buf[MAX_BITS + 1];
if (check) {
if (input_len < gen_len) {
fprintf(stderr, "la palabra debe ser más larga que el generador\n");
return 2;
}
memcpy(buf, input, input_len + 1);
mod2_divide(buf, input_len, gen, gen_len, 1);
const char *rem = buf + input_len - r;
int ok = strspn(rem, "0") == r;
printf("residuo: %s\n", rem);
printf("%s\n", ok ? "sin errores detectados" : "ERROR detectado");
return ok ? 0 : 1;
}
/* Se agregan r ceros: es multiplicar los datos por 2^r. */
memcpy(buf, input, input_len);
memset(buf + input_len, '0', r);
buf[input_len + r] = '\0';
mod2_divide(buf, input_len + r, gen, gen_len, 1);
const char *rem = buf + input_len;
printf("CRC (r = %zu bits): %s\n", r, rem);
printf("se transmite: %s%s\n", input, rem);
return 0;
}#!/usr/bin/env python3
"""Compara paridad, checksum de Internet y CRC-32 frente a distintos tipos de errores.
Toma frames aleatorios de 64 bytes, los daña de una forma conocida y cuenta cuántos
daños NO detecta cada método. La semilla es fija: el resultado es reproducible.
python3 detect.py [intentos]
"""
import random
import struct
import sys
import zlib
FRAME_BYTES = 64
def parity(data: bytes) -> int:
"""Bit de paridad par de todo el frame."""
return bin(int.from_bytes(data, "big")).count("1") % 2
def internet_checksum(data: bytes) -> int:
"""Suma de palabras de 16 bits con acarreo circular, complementada (RFC 1071)."""
words = struct.unpack(f"!{len(data) // 2}H", data)
total = sum(words)
while total >> 16:
total = (total & 0xFFFF) + (total >> 16)
return ~total & 0xFFFF
METHODS = {"paridad": parity, "checksum": internet_checksum, "crc32": zlib.crc32}
def flip_bits(data: bytes, positions) -> bytes:
out = bytearray(data)
for pos in positions:
out[pos // 8] ^= 0x80 >> (pos % 8)
return bytes(out)
def one_bit(data, rng):
return flip_bits(data, [rng.randrange(len(data) * 8)])
def two_bits(data, rng):
return flip_bits(data, rng.sample(range(len(data) * 8), 2))
def burst(length):
"""Ráfaga: el primer y el último bit cambian; los de en medio, al azar."""
def damage(data, rng):
start = rng.randrange(len(data) * 8 - length)
inner = [start + i for i in range(1, length - 1) if rng.random() < 0.5]
return flip_bits(data, [start, start + length - 1] + inner)
return damage
def swap_words(data, rng):
"""Intercambia dos palabras de 16 bits distintas: el orden cambia, la suma no."""
words = list(struct.unpack(f"!{len(data) // 2}H", data))
while True:
i, j = rng.sample(range(len(words)), 2)
if words[i] != words[j]:
break
words[i], words[j] = words[j], words[i]
return struct.pack(f"!{len(words)}H", *words)
DAMAGES = [
("1 bit", one_bit),
("2 bits", two_bits),
("ráfaga de 16 bits", burst(16)),
("ráfaga de 32 bits", burst(32)),
("ráfaga de 100 bits", burst(100)),
("intercambio de palabras", swap_words),
]
def undetected(damage, trials, rng):
"""Cuenta, por método, los daños que dejan el mismo código de detección."""
misses = dict.fromkeys(METHODS, 0)
for _ in range(trials):
data = bytes(rng.getrandbits(8) for _ in range(FRAME_BYTES))
bad = damage(data, rng)
for name, method in METHODS.items():
if method(data) == method(bad):
misses[name] += 1
return misses
def main():
trials = int(sys.argv[1]) if len(sys.argv) > 1 else 20000
rng = random.Random(20260303)
print(f"{trials} intentos por fila, frames de {FRAME_BYTES} bytes. Daños NO detectados:")
print(f"{'daño':<25}{'paridad':>10}{'checksum':>10}{'crc32':>8}")
for label, damage in DAMAGES:
m = undetected(damage, trials, rng)
print(f"{label:<25}{m['paridad']:>10}{m['checksum']:>10}{m['crc32']:>8}")
if __name__ == "__main__":
main()"""Ayudas para las pruebas y para check_lab.py: levantar topologías y correr comandos."""
import os
import subprocess
import time
HERE = os.path.dirname(os.path.abspath(__file__))
def run(args, check=True, timeout=180, stdin=None):
return subprocess.run(args, check=check, timeout=timeout, text=True, stdin=stdin,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=HERE)
def deploy(topo):
run(["containerlab", "deploy", "-t", f"topo/{topo}.clab.yml", "--reconfigure"], timeout=300)
def destroy(topo):
run(["containerlab", "destroy", "-t", f"topo/{topo}.clab.yml", "--cleanup"], check=False,
timeout=300)
run(["docker", "network", "rm", f"redes-w03-{topo}"], check=False)
class Lab:
"""Una topología ya levantada: lab.sh("h1", "ping ...") corre un comando en un nodo."""
def __init__(self, topo):
self.prefix = f"clab-w03-{topo}-"
def sh(self, node, command, check=False, timeout=60):
return run(["docker", "exec", self.prefix + node, "sh", "-c", command], check=check,
timeout=timeout)
def out(self, node, command):
return self.sh(node, command).stdout
def ping(self, node, target, count=1):
"""True si llega al menos una respuesta."""
return self.sh(node, f"ping -c {count} -W 1 {target}").returncode == 0
def apply_script(self, node, path):
"""Corre un script del laboratorio en el nodo, como se hace a mano con sh < script."""
with open(os.path.join(HERE, path), encoding="utf-8") as script:
done = run(["docker", "exec", "-i", self.prefix + node, "sh", "-e"], stdin=script,
check=False)
if done.returncode != 0:
raise RuntimeError(f"{path} falló en {node}:\n{done.stdout}")
def capture(self, node, iface, action, bpf="", wait=0.5):
"""Captura en node:iface mientras corre action(); devuelve las líneas de tcpdump -e."""
pcap = "/tmp/w03.pcap"
self.sh(node, f"rm -f {pcap}")
run(["docker", "exec", "-d", self.prefix + node, "sh", "-c",
f"tcpdump -U -n -i {iface} -w {pcap} {bpf} 2>/dev/null"])
for _ in range(50):
if self.sh(node, f"test -s {pcap}").returncode == 0:
break
time.sleep(0.1)
time.sleep(wait)
try:
action()
time.sleep(wait)
finally:
self.sh(node, "pkill tcpdump")
time.sleep(0.2)
text = self.out(node, f"tcpdump -e -n -r {pcap} 2>/dev/null")
return [line for line in text.splitlines() if line.strip()]# Laboratorio de la semana 3, paso 4: dos switches unidos por un trunk y un router
# colgado del segundo. Los hosts ya tienen dirección y gateway; las VLANs, los trunks y
# las subinterfaces del router se configuran durante el laboratorio (vlan/).
name: w03-campus
mgmt:
network: redes-w03-campus
ipv4-subnet: 172.90.3.64/26
ipv6-subnet: 3fff:172:90:3:b::/80
topology:
defaults:
kind: linux
image: redes-router
sysctls:
net.ipv6.conf.all.disable_ipv6: 1
net.ipv6.conf.default.disable_ipv6: 1
nodes:
sw1:
exec:
- ip link add br0 type bridge
- sysctl -q -w net.ipv6.conf.br0.disable_ipv6=1
- ip link set eth1 master br0
- ip link set eth2 master br0
- ip link set eth3 master br0
- ip link set br0 up
sw2:
exec:
- ip link add br0 type bridge
- sysctl -q -w net.ipv6.conf.br0.disable_ipv6=1
- ip link set eth1 master br0
- ip link set eth2 master br0
- ip link set eth3 master br0
- ip link set eth4 master br0
- ip link set br0 up
r1:
binds: [r1/frr.conf:/etc/frr/frr.conf:ro]
exec:
- sysctl -q -w net.ipv6.conf.default.disable_ipv6=1
- sysctl -q -w net.ipv6.conf.eth1.disable_ipv6=1
h1:
exec:
- ip addr add 10.203.110.1/24 dev eth1
- ip route replace default via 10.203.110.254
h2:
exec:
- ip addr add 10.203.120.2/24 dev eth1
- ip route replace default via 10.203.120.254
h3:
exec:
- ip addr add 10.203.110.3/24 dev eth1
- ip route replace default via 10.203.110.254
h4:
exec:
- ip addr add 10.203.120.4/24 dev eth1
- ip route replace default via 10.203.120.254
links:
- type: veth
endpoints:
- {node: sw1, interface: eth1}
- {node: h1, interface: eth1, mac: "02:00:00:00:00:01"}
- type: veth
endpoints:
- {node: sw1, interface: eth2}
- {node: h2, interface: eth1, mac: "02:00:00:00:00:02"}
- type: veth
endpoints:
- {node: sw2, interface: eth1}
- {node: h3, interface: eth1, mac: "02:00:00:00:00:03"}
- type: veth
endpoints:
- {node: sw2, interface: eth2}
- {node: h4, interface: eth1, mac: "02:00:00:00:00:04"}
- type: veth
endpoints:
- {node: sw1, interface: eth3, mac: "02:00:00:00:01:03"}
- {node: sw2, interface: eth3, mac: "02:00:00:00:02:03"}
- type: veth
endpoints:
- {node: sw2, interface: eth4, mac: "02:00:00:00:02:04"}
- {node: r1, interface: eth1, mac: "02:00:00:00:00:fe"}# Laboratorio de la semana 3, pasos 1 a 3: un switch (bridge de Linux) y cuatro hosts.
# Las MAC de los hosts son fijas para que la salida coincida con los apuntes. Se
# desactiva IPv6 en la red de datos para que solo se vea el tráfico que provocamos.
name: w03-lan
mgmt:
network: redes-w03-lan
ipv4-subnet: 172.90.3.0/26
ipv6-subnet: 3fff:172:90:3:a::/80
topology:
defaults:
kind: linux
image: redes-router
sysctls:
net.ipv6.conf.all.disable_ipv6: 1
net.ipv6.conf.default.disable_ipv6: 1
binds:
- ../build/arp_probe:/usr/local/bin/arp_probe:ro
nodes:
sw1:
exec:
- ip link add br0 type bridge
- sysctl -q -w net.ipv6.conf.br0.disable_ipv6=1
- ip link set eth1 master br0
- ip link set eth2 master br0
- ip link set eth3 master br0
- ip link set eth4 master br0
- ip link set br0 up
h1:
exec: ["ip addr add 10.203.0.1/24 dev eth1"]
h2:
exec: ["ip addr add 10.203.0.2/24 dev eth1"]
h3:
exec: ["ip addr add 10.203.0.3/24 dev eth1"]
h4:
exec: ["ip addr add 10.203.0.4/24 dev eth1"]
links:
- type: veth
endpoints:
- {node: sw1, interface: eth1}
- {node: h1, interface: eth1, mac: "02:00:00:00:00:01"}
- type: veth
endpoints:
- {node: sw1, interface: eth2}
- {node: h2, interface: eth1, mac: "02:00:00:00:00:02"}
- type: veth
endpoints:
- {node: sw1, interface: eth3}
- {node: h3, interface: eth1, mac: "02:00:00:00:00:03"}
- type: veth
endpoints:
- {node: sw1, interface: eth4}
- {node: h4, interface: eth1, mac: "02:00:00:00:00:04"}# Demostración de la semana 3: tres switches en triángulo. Se levanta con el ciclo
# abierto (eth3 de sw1 apagada) y con STP desactivado. Cada enlace entre switches tiene
# 5 ms de retardo: una tormenta de broadcast sigue siendo una tormenta, pero a un ritmo
# que no se come el procesador de la laptop. No quiten ese retardo.
name: w03-loop
mgmt:
network: redes-w03-loop
ipv4-subnet: 172.90.3.128/26
ipv6-subnet: 3fff:172:90:3:c::/80
topology:
defaults:
kind: linux
image: redes-router
sysctls:
net.ipv6.conf.all.disable_ipv6: 1
net.ipv6.conf.default.disable_ipv6: 1
nodes:
sw1:
exec:
- ip link set eth3 down
- tc qdisc add dev eth2 root netem delay 5ms
- tc qdisc add dev eth3 root netem delay 5ms
- ip link add br0 type bridge priority 4096 forward_delay 200
- sysctl -q -w net.ipv6.conf.br0.disable_ipv6=1
- ip link set eth1 master br0
- ip link set eth2 master br0
- ip link set eth3 master br0
- ip link set br0 up
sw2:
exec:
- tc qdisc add dev eth2 root netem delay 5ms
- tc qdisc add dev eth3 root netem delay 5ms
- ip link add br0 type bridge priority 8192 forward_delay 200
- sysctl -q -w net.ipv6.conf.br0.disable_ipv6=1
- ip link set eth1 master br0
- ip link set eth2 master br0
- ip link set eth3 master br0
- ip link set br0 up
sw3:
exec:
- tc qdisc add dev eth1 root netem delay 5ms
- tc qdisc add dev eth2 root netem delay 5ms
- ip link add br0 type bridge priority 12288 forward_delay 200
- sysctl -q -w net.ipv6.conf.br0.disable_ipv6=1
- ip link set eth1 master br0
- ip link set eth2 master br0
- ip link set br0 up
h1:
exec: ["ip addr add 10.203.0.1/24 dev eth1"]
h2:
exec: ["ip addr add 10.203.0.2/24 dev eth1"]
links:
- endpoints: ["sw1:eth2", "sw2:eth3"]
- endpoints: ["sw2:eth2", "sw3:eth1"]
- endpoints: ["sw3:eth2", "sw1:eth3"]
- type: veth
endpoints:
- {node: sw1, interface: eth1}
- {node: h1, interface: eth1, mac: "02:00:00:00:00:01"}
- type: veth
endpoints:
- {node: sw2, interface: eth1}
- {node: h2, interface: eth1, mac: "02:00:00:00:00:02"}# Paso 4, r1 de campus.clab.yml: una subinterfaz por VLAN sobre el trunk (eth1). Cada
# subinterfaz es el gateway de su subred.
ip link add link eth1 name eth1.110 type vlan id 110
ip link add link eth1 name eth1.120 type vlan id 120
ip addr add 10.203.110.254/24 dev eth1.110
ip addr add 10.203.120.254/24 dev eth1.120
ip link set eth1.110 up
ip link set eth1.120 up# Paso 4, sw1 de campus.clab.yml: h1 (eth1) en la VLAN 110, h2 (eth2) en la 120 y un
# trunk hacia sw2 (eth3) que lleva las dos.
ip link set br0 type bridge vlan_filtering 1
bridge vlan add dev eth1 vid 110 pvid untagged
bridge vlan del dev eth1 vid 1
bridge vlan add dev eth2 vid 120 pvid untagged
bridge vlan del dev eth2 vid 1
bridge vlan add dev eth3 vid 110
bridge vlan add dev eth3 vid 120
bridge vlan del dev eth3 vid 1# Paso 4, sw2 de campus.clab.yml: h3 (eth1) en la VLAN 110, h4 (eth2) en la 120, un
# trunk hacia sw1 (eth3) y otro hacia r1 (eth4).
ip link set br0 type bridge vlan_filtering 1
bridge vlan add dev eth1 vid 110 pvid untagged
bridge vlan del dev eth1 vid 1
bridge vlan add dev eth2 vid 120 pvid untagged
bridge vlan del dev eth2 vid 1
for port in eth3 eth4; do
bridge vlan add dev $port vid 110
bridge vlan add dev $port vid 120
bridge vlan del dev $port vid 1
done# Paso 3: VLANs en el único switch de lan.clab.yml. h1 y h2 van a la VLAN 110; h3 y h4,
# a la 120. Se corre con: docker exec -i clab-w03-lan-sw1 sh < vlan/lan_sw1.sh
ip link set br0 type bridge vlan_filtering 1
for port in eth1 eth2; do
bridge vlan add dev $port vid 110 pvid untagged
bridge vlan del dev $port vid 1
done
for port in eth3 eth4; do
bridge vlan add dev $port vid 120 pvid untagged
bridge vlan del dev $port vid 1
donefrr defaults traditional
hostname r1Pruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
"""Pruebas de aloha.c: la simulación debe coincidir con las fórmulas, con margen."""
import math
import subprocess
import unittest
def table(seed):
out = subprocess.run(["build/aloha", str(seed)], text=True, capture_output=True,
check=True).stdout
rows = {}
for line in out.splitlines()[1:]:
g, slotted, pure = line.split("|")
rows[float(g)] = (float(slotted.split()[0]), float(pure.split()[0]))
return rows
class Aloha(unittest.TestCase):
def setUp(self):
self.rows = table(20260303)
def test_slotted_peak_near_one_over_e_at_load_one(self):
self.assertAlmostEqual(self.rows[1.0][0], 1 / math.e, delta=0.01)
self.assertEqual(max(self.rows, key=lambda g: self.rows[g][0]), 1.0)
def test_pure_peak_near_one_over_2e_at_load_half(self):
self.assertAlmostEqual(self.rows[0.5][1], 1 / (2 * math.e), delta=0.01)
self.assertEqual(max(self.rows, key=lambda g: self.rows[g][1]), 0.5)
def test_more_load_after_the_peak_means_less_throughput(self):
self.assertLess(self.rows[3.0][0], self.rows[1.0][0])
self.assertLess(self.rows[3.0][1], self.rows[0.5][1])
def test_other_seed_gives_similar_result(self):
other = table(7)
self.assertAlmostEqual(other[1.0][0], self.rows[1.0][0], delta=0.01)
if __name__ == "__main__":
unittest.main()"""Paso 4 del laboratorio: dos switches, trunks y un router con subinterfaces."""
import re
import subprocess
import time
import unittest
import labutil
H1, H2, H3 = (f"02:00:00:00:00:0{i}" for i in range(1, 4))
R1 = "02:00:00:00:00:fe"
class BeforeVlans(unittest.TestCase):
"""Sin VLANs, todo es un solo dominio de broadcast. Después, ya no."""
@classmethod
def setUpClass(cls):
cls.addClassCleanup(labutil.destroy, "campus")
labutil.deploy("campus")
cls.lab = labutil.Lab("campus")
time.sleep(6)
def flush(self):
for node in ("h1", "h2", "h3", "h4", "r1"):
self.lab.sh(node, "ip neigh flush all")
for switch in ("sw1", "sw2"):
self.lab.sh(switch, "ip link set br0 type bridge fdb_flush")
def test_1_without_vlans_a_broadcast_reaches_everyone(self):
self.flush()
for host in ("h2", "h4"):
lines = self.lab.capture(host, "eth1",
lambda: self.lab.ping("h1", "10.203.110.3"), "arp")
self.assertTrue(any("Request who-has 10.203.110.3" in l for l in lines), lines)
self.flush()
def test_2_same_port_destination_is_filtered(self):
# h1 y h2 están en sw1. Si sw1 no conoce a h2, hace flooding y el frame llega a sw2. Si
# sw2 ya vio a h2 por ese mismo puerto, descarta el frame; si no, también hace flooding.
lab = self.lab
lab.sh("h2", "ip addr add 10.203.110.2/24 dev eth1", check=True)
try:
self.flush()
lab.ping("h1", "10.203.110.2")
lab.ping("h3", "10.203.110.2")
self.assertRegex(lab.out("sw2", "bridge fdb show br br0"), rf"{H2} dev eth3")
lab.sh("sw1", "ip link set br0 type bridge fdb_flush")
lines = lab.capture("h4", "eth1", lambda: lab.ping("h1", "10.203.110.2"), "icmp")
self.assertEqual(lines, [])
lab.sh("sw1", "ip link set br0 type bridge fdb_flush")
lab.sh("sw2", "ip link set br0 type bridge fdb_flush")
lines = lab.capture("h4", "eth1", lambda: lab.ping("h1", "10.203.110.2"), "icmp")
self.assertEqual(len(lines), 1, lines)
finally:
lab.sh("h2", "ip addr del 10.203.110.2/24 dev eth1")
def test_3_two_subnets_without_router_do_not_talk(self):
self.assertFalse(self.lab.ping("h1", "10.203.120.2"))
def test_4_vlans_and_trunks(self):
lab = self.lab
for node in ("sw1", "sw2", "r1"):
lab.apply_script(node, f"vlan/campus_{node}.sh")
self.flush()
# Un broadcast de la VLAN 110 ya no llega a la 120, ni siquiera por el trunk.
lines = lab.capture("h4", "eth1", lambda: lab.ping("h1", "10.203.110.3"), "arp")
self.assertEqual(lines, [])
# Misma VLAN entre switches: el frame va etiquetado por el trunk.
self.flush()
lines = lab.capture("sw1", "eth3", lambda: lab.ping("h1", "10.203.110.3"), "icmp")
self.assertTrue(lines)
self.assertTrue(all("vlan 110" in l for l in lines), lines)
# En el puerto de acceso el mismo tráfico va sin etiqueta.
lines = lab.capture("h1", "eth1", lambda: lab.ping("h1", "10.203.110.3"), "icmp")
self.assertTrue(lines)
self.assertFalse(any("802.1Q" in l for l in lines), lines)
# Entre VLANs: el paquete pasa dos veces por el trunk, con etiquetas distintas.
self.flush()
lines = lab.capture("sw1", "eth3", lambda: lab.ping("h1", "10.203.120.2"), "vlan and icmp")
request = [l for l in lines if "echo request" in l]
self.assertEqual(len(request), 2, lines)
self.assertRegex(request[0], rf"{H1} > {R1}.*vlan 110")
self.assertRegex(request[1], rf"{R1} > {H2}.*vlan 120")
# El router ve la etiqueta en eth1 y no la ve en la subinterfaz.
lines = lab.capture("r1", "eth1", lambda: lab.ping("h1", "10.203.120.2"), "vlan and icmp")
self.assertTrue(any("vlan 110" in l for l in lines), lines)
lines = lab.capture("r1", "eth1.110", lambda: lab.ping("h1", "10.203.120.2"), "icmp")
self.assertTrue(lines)
self.assertFalse(any("802.1Q" in l for l in lines), lines)
# En otra subred el host busca con ARP la MAC del gateway, no la del destino.
self.assertRegex(lab.out("h1", "ip neigh show 10.203.110.254"), R1)
self.assertEqual(lab.out("h1", "ip neigh show 10.203.120.2").strip(), "")
# Las direcciones IP no cambian por el camino; las MAC y el TTL, sí.
self.flush()
seen_h1 = lab.capture("h1", "eth1", lambda: lab.ping("h1", "10.203.120.2"), "icmp")
self.assertRegex(seen_h1[0], rf"{H1} > {R1}.*10.203.110.1 > 10.203.120.2")
seen_h2 = lab.capture("h2", "eth1", lambda: lab.ping("h1", "10.203.120.2"), "icmp")
self.assertRegex(seen_h2[0], rf"{R1} > {H2}.*10.203.110.1 > 10.203.120.2")
trace = lab.out("h1", "traceroute -n -w 1 -q 1 10.203.120.4")
self.assertRegex(trace, r"1\s+10.203.110.254")
# El mismo MAC puede estar en dos VLANs: la tabla del switch es por (VLAN, MAC).
fdb = lab.out("sw1", "bridge fdb show br br0 | grep -v permanent")
self.assertRegex(fdb, rf"{R1} dev eth3 vlan 110")
self.assertRegex(fdb, rf"{R1} dev eth3 vlan 120")
# El script de comprobación del laboratorio queda en verde.
done = subprocess.run(["python3", "check_lab.py"], text=True, capture_output=True,
check=False, cwd=labutil.HERE)
self.assertEqual(done.returncode, 0, done.stdout)
self.assertNotIn("FALLA", done.stdout)
def test_5_vlan_missing_from_a_trunk_breaks_only_that_vlan(self):
lab = self.lab
lab.sh("sw1", "bridge vlan del dev eth3 vid 120", check=True)
try:
self.assertFalse(lab.ping("h2", "10.203.120.254"))
self.assertTrue(lab.ping("h1", "10.203.110.254"))
done = subprocess.run(["python3", "check_lab.py"], text=True, capture_output=True,
check=False, cwd=labutil.HERE)
self.assertEqual(done.returncode, 1)
self.assertTrue(re.search(r"FALLA\s+el trunk de sw1", done.stdout), done.stdout)
finally:
lab.sh("sw1", "bridge vlan add dev eth3 vid 120", check=True)
def test_6_untagged_frame_on_a_trunk_is_dropped(self):
# El trunk no tiene VLAN por defecto (sin pvid): un frame sin etiqueta no pasa.
lab = self.lab
lab.sh("r1", "ip addr add 10.203.9.1/24 dev eth1", check=True)
try:
lines = lab.capture("sw1", "eth3", lambda: lab.sh("r1", "ping -c 1 -W 1 10.203.9.2"),
"arp")
self.assertEqual(lines, [])
finally:
lab.sh("r1", "ip addr del 10.203.9.1/24 dev eth1")
if __name__ == "__main__":
unittest.main()"""Pruebas de crc.c y de las afirmaciones de los apuntes sobre el CRC."""
import itertools
import subprocess
import unittest
import zlib
CRC = "build/crc"
def run_crc(*args):
return subprocess.run([CRC, *args], text=True, capture_output=True, check=False)
def remainder(word: str, gen: str) -> str:
"""División módulo 2 hecha aparte, en Python, con enteros."""
value, divisor, r = int(word, 2), int(gen, 2), len(gen) - 1
for shift in range(len(word) - len(gen), -1, -1):
if value >> (shift + r) & 1:
value ^= divisor << shift
return format(value, f"0{r}b")
def flip(word: str, error: int) -> str:
return format(int(word, 2) ^ error, f"0{len(word)}b")
class CrcProgram(unittest.TestCase):
def test_worked_example_of_the_notes(self):
out = run_crc("1101011", "1001").stdout
self.assertIn("CRC (r = 3 bits): 111", out)
self.assertIn("se transmite: 1101011111", out)
def test_received_word_without_errors(self):
done = run_crc("-c", "1101011111", "1001")
self.assertEqual(done.returncode, 0)
self.assertIn("residuo: 000", done.stdout)
def test_single_bit_error_is_detected(self):
done = run_crc("-c", "1101011011", "1001")
self.assertEqual(done.returncode, 1)
self.assertIn("ERROR detectado", done.stdout)
def test_error_equal_to_generator_goes_unnoticed(self):
# El error 1001, dos bits a distancia 3, es múltiplo del generador 1001.
bad = flip("1101011111", 0b1001)
self.assertEqual(run_crc("-c", bad, "1001").returncode, 0)
# El ejemplo de los apuntes: el mismo patrón, desplazado tres posiciones.
self.assertEqual(flip("1101011111", 0b1001 << 3), "1100010111")
self.assertEqual(run_crc("-c", "1100010111", "1001").returncode, 0)
def test_matches_independent_division(self):
for bits in range(1, 8):
for data in map("".join, itertools.product("01", repeat=bits)):
for gen in ("1001", "1011", "10011"):
expected = remainder(data + "0" * (len(gen) - 1), gen)
self.assertIn(f"bits): {expected}\n", run_crc(data, gen).stdout)
def test_rejects_bad_arguments(self):
for args in (["10a", "1001"], ["101", "0110"], ["101", "1"], ["101"]):
self.assertEqual(run_crc(*args).returncode, 2, args)
class CrcClaims(unittest.TestCase):
WORD = "1101011111"
def test_every_burst_up_to_r_bits_is_detected(self):
# Una ráfaga de largo b <= r empieza y termina en 1: con r = 3 son 1, 11, 101 y 111.
for pattern in (0b1, 0b11, 0b101, 0b111):
for shift in range(len(self.WORD) - pattern.bit_length() + 1):
bad = flip(self.WORD, pattern << shift)
self.assertNotEqual(remainder(bad, "1001"), "000")
def test_generator_1001_detects_every_odd_error(self):
# 1001 tiene a x + 1 como factor: una cantidad impar de errores nunca da residuo 0.
n = len(self.WORD)
for error in range(1, 2 ** n):
if bin(error).count("1") % 2 == 1:
self.assertNotEqual(remainder(flip(self.WORD, error), "1001"), "000")
def test_fraction_of_undetected_errors_is_one_in_eight(self):
# Con r = 3, de todos los patrones de error no nulos, uno de cada 8 pasa (aprox.).
n = len(self.WORD)
missed = sum(remainder(flip(self.WORD, e), "1001") == "000" for e in range(1, 2 ** n))
self.assertEqual(missed, 2 ** (n - 3) - 1)
def test_ethernet_crc32_check_value(self):
# zlib usa el mismo polinomio y las mismas reflexiones que el FCS de Ethernet.
self.assertEqual(zlib.crc32(b"123456789"), 0xCBF43926)
if __name__ == "__main__":
unittest.main()"""Pruebas de detect.py: lo que dicen los apuntes sobre cada método de detección."""
import random
import unittest
import zlib
import detect
class Methods(unittest.TestCase):
def test_checksum_known_value(self):
# Ejemplo de los apuntes: 0x4500 + 0x0054 = 0x4554; complemento = 0xBAAB.
self.assertEqual(detect.internet_checksum(bytes.fromhex("45000054")), 0xBAAB)
def test_checksum_carry_wraps_around(self):
# 0xFFFF + 0x0001 = 0x10000, que con acarreo circular es 0x0001.
self.assertEqual(detect.internet_checksum(bytes.fromhex("ffff0001")), 0xFFFE)
def test_parity_misses_even_errors(self):
data = bytes(range(64))
self.assertEqual(detect.parity(data), detect.parity(detect.flip_bits(data, [3, 77])))
self.assertNotEqual(detect.parity(data), detect.parity(detect.flip_bits(data, [3])))
class Comparison(unittest.TestCase):
TRIALS = 3000
def misses(self, damage):
return detect.undetected(damage, self.TRIALS, random.Random(7))
def test_one_bit_is_detected_by_everyone(self):
self.assertEqual(self.misses(detect.one_bit),
{"paridad": 0, "checksum": 0, "crc32": 0})
def test_two_bits_escape_parity_always_and_checksum_sometimes(self):
m = self.misses(detect.two_bits)
self.assertEqual(m["paridad"], self.TRIALS)
# Pasa si caen en la misma columna de 16 bits y en sentidos opuestos: cerca de 1/32.
self.assertGreater(m["checksum"], self.TRIALS / 64)
self.assertLess(m["checksum"], self.TRIALS / 16)
self.assertEqual(m["crc32"], 0)
def test_swapped_words_only_caught_by_crc(self):
m = self.misses(detect.swap_words)
self.assertEqual(m["paridad"], self.TRIALS)
self.assertEqual(m["checksum"], self.TRIALS)
self.assertEqual(m["crc32"], 0)
def test_crc32_catches_all_bursts_up_to_32_bits(self):
self.assertEqual(self.misses(detect.burst(32))["crc32"], 0)
self.assertEqual(self.misses(detect.burst(16))["crc32"], 0)
def test_crc32_catches_every_two_bit_error_in_a_short_frame(self):
data = bytes(range(32))
base = zlib.crc32(data)
for i in range(0, len(data) * 8):
for j in range(i + 1, len(data) * 8):
self.assertNotEqual(zlib.crc32(detect.flip_bits(data, [i, j])), base)
if __name__ == "__main__":
unittest.main()"""Pasos 1 a 3 del laboratorio, en la topología de un switch y cuatro hosts."""
import re
import time
import unittest
import labutil
H1, H2, H3, H4 = (f"02:00:00:00:00:0{i}" for i in range(1, 5))
class Lan(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.addClassCleanup(labutil.destroy, "lan")
labutil.deploy("lan")
cls.lab = labutil.Lab("lan")
time.sleep(6) # pasan los mensajes de los primeros segundos
def setUp(self):
for host in ("h1", "h2", "h3", "h4"):
self.lab.sh(host, "ip neigh flush dev eth1")
self.lab.sh("sw1", "ip link set br0 type bridge fdb_flush")
def fdb(self):
text = self.lab.out("sw1", "bridge fdb show br br0 | grep -v permanent")
return {m.group(1): m.group(2) for m in re.finditer(r"(\S+) dev (eth\d)", text)}
def test_1_arp_request_is_broadcast_and_reply_is_unicast(self):
lines = self.lab.capture("h1", "eth1", lambda: self.lab.ping("h1", "10.203.0.2"), "arp")
self.assertRegex(lines[0], rf"{H1} > ff:ff:ff:ff:ff:ff.*Request who-has 10.203.0.2")
self.assertRegex(lines[1], rf"{H2} > {H1}.*Reply 10.203.0.2 is-at {H2}")
self.assertIn(H2, self.lab.out("h1", "ip neigh show dev eth1"))
def test_1_target_learns_the_sender_from_the_request(self):
self.lab.ping("h1", "10.203.0.2")
self.assertIn(H1, self.lab.out("h2", "ip neigh show dev eth1"))
def test_1_other_hosts_get_the_request_but_not_the_reply(self):
lines = self.lab.capture("h3", "eth1", lambda: self.lab.ping("h1", "10.203.0.2"), "arp")
self.assertEqual(len(lines), 1, lines)
self.assertIn("Request", lines[0])
self.assertNotIn(H2, self.lab.out("h3", "ip neigh show dev eth1"))
def test_2_switch_learns_from_source_addresses(self):
self.assertEqual(self.fdb(), {})
self.lab.ping("h1", "10.203.0.2")
self.assertEqual(self.fdb(), {H1: "eth1", H2: "eth2"})
def test_2_unknown_unicast_is_flooded_once(self):
self.lab.ping("h1", "10.203.0.2", 1)
self.lab.sh("sw1", "ip link set br0 type bridge fdb_flush")
# h1 conserva su entrada ARP: envía directo a la MAC de h2, que el switch ya olvidó.
lines = self.lab.capture("h3", "eth1", lambda: self.lab.ping("h1", "10.203.0.2", 3), "icmp")
self.assertEqual(len(lines), 1, lines)
self.assertIn("echo request", lines[0])
def test_2_without_learning_the_bridge_behaves_like_a_hub(self):
self.lab.ping("h1", "10.203.0.2", 1)
self.lab.sh("sw1", "ip link set br0 type bridge ageing_time 0", check=True)
try:
lines = self.lab.capture("h3", "eth1", lambda: self.lab.ping("h1", "10.203.0.2", 3),
"icmp")
finally:
self.lab.sh("sw1", "ip link set br0 type bridge ageing_time 30000", check=True)
self.assertEqual(len(lines), 6, lines)
def test_arp_probe_finds_the_neighbor(self):
done = self.lab.sh("h1", "arp_probe eth1 10.203.0.4")
self.assertEqual(done.returncode, 0, done.stdout)
self.assertIn("la MAC del vecino es " + H4, done.stdout)
self.assertIn("ff ff ff ff ff ff 02 00 00 00 00 01 08 06", done.stdout)
def test_arp_probe_reports_no_answer(self):
done = self.lab.sh("h1", "arp_probe eth1 10.203.0.99")
self.assertEqual(done.returncode, 1)
self.assertIn("sin respuesta", done.stdout)
def test_zz_step_3_vlans_split_the_network(self):
# Se corre al final (orden alfabético): deja el switch con VLANs.
self.lab.apply_script("sw1", "vlan/lan_sw1.sh")
self.assertTrue(self.lab.ping("h1", "10.203.0.2", 2))
self.assertTrue(self.lab.ping("h3", "10.203.0.4", 2))
lines = self.lab.capture("h3", "eth1", lambda: self.lab.ping("h1", "10.203.0.3", 2), "arp")
self.assertEqual(lines, [], "un broadcast de la VLAN 110 no debe llegar a la 120")
self.assertFalse(self.lab.ping("h1", "10.203.0.4", 1))
self.assertRegex(self.lab.out("sw1", "bridge fdb show br br0 | grep -v permanent"),
rf"{H1} dev eth1 vlan 110")
if __name__ == "__main__":
unittest.main()"""Tormenta de broadcast en un ciclo de switches, y spanning tree que la evita."""
import re
import time
import unittest
import labutil
SWITCHES = ("sw1", "sw2", "sw3")
class Loop(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.addClassCleanup(labutil.destroy, "loop")
labutil.deploy("loop")
cls.lab = labutil.Lab("loop")
cls.addClassCleanup(cls.open_loop)
time.sleep(8)
@classmethod
def open_loop(cls):
cls.lab.sh("sw1", "ip link set eth3 down")
def rx(self, host="h2"):
return int(self.lab.out(host, "cat /sys/class/net/eth1/statistics/rx_packets"))
def port_state(self, switch, port):
text = self.lab.out(switch, f"bridge link show dev {port}")
return re.search(r"state (\w+)", text).group(1)
def flush_arp(self):
for host in ("h1", "h2"):
self.lab.sh(host, "ip neigh flush dev eth1")
def test_1_storm_without_stp(self):
lab = self.lab
self.addCleanup(self.open_loop)
self.assertTrue(lab.ping("h1", "10.203.0.2", 2), "con el ciclo abierto funciona")
lab.sh("sw1", "ip link set eth3 up", check=True)
self.flush_arp()
before = self.rx()
lab.ping("h1", "10.203.0.2")
time.sleep(3)
middle = self.rx()
time.sleep(1)
after = self.rx()
self.assertGreater(middle - before, 100, "un solo ARP se multiplica")
self.assertGreater(after - middle, 10, "y sigue circulando sin que nadie envíe nada")
# La tabla queda mal: sw1 cree que h1, conectado a eth1, está por otro puerto.
self.assertRegex(lab.out("sw1", "bridge fdb show br br0 | grep 02:00:00:00:00:01"),
r"dev eth[23]")
self.open_loop()
time.sleep(1)
cut = self.rx()
time.sleep(1.5)
self.assertLess(self.rx() - cut, 10, "al abrir el ciclo, la tormenta se apaga")
def test_2_stp_blocks_one_port_and_recovers_after_a_failure(self):
lab = self.lab
for switch in SWITCHES:
lab.sh(switch, "ip link set br0 type bridge stp_state 1", check=True)
lab.sh("sw1", "ip link set eth3 up", check=True)
self.addCleanup(lab.sh, "sw1", "ip link set eth2 up")
time.sleep(7)
# sw1 tiene la menor prioridad: es la raíz y todos sus puertos reenvían.
self.assertIn("root_port 0", lab.out("sw1", "ip -d link show br0"))
self.assertEqual(self.port_state("sw1", "eth2"), "forwarding")
self.assertEqual(self.port_state("sw1", "eth3"), "forwarding")
# sw3 pierde el enlace con sw2 frente a sw2, que tiene menor prioridad.
self.assertEqual(self.port_state("sw2", "eth2"), "forwarding")
self.assertEqual(self.port_state("sw3", "eth1"), "blocking")
self.flush_arp()
before = self.rx()
self.assertTrue(lab.ping("h1", "10.203.0.2", 2))
time.sleep(2)
self.assertLess(self.rx() - before, 20, "sin tormenta")
# Se corta el enlace sw1-sw2. El puerto bloqueado espera a max_age y luego pasa por
# listening y learning: 20 + 2 + 2 s con estos temporizadores.
lab.sh("sw1", "ip link set eth2 down", check=True)
start = time.monotonic()
self.assertFalse(lab.ping("h1", "10.203.0.2", 1), "h2 queda aislado por un momento")
recovered = None
while time.monotonic() - start < 60:
if lab.ping("h1", "10.203.0.2", 1):
recovered = time.monotonic() - start
break
self.assertIsNotNone(recovered, "STP debe activar el puerto bloqueado")
self.assertGreater(recovered, 10)
self.assertLess(recovered, 45)
self.assertEqual(self.port_state("sw3", "eth1"), "forwarding")
if __name__ == "__main__":
unittest.main()"""Reto del laboratorio. Solo corre con solution/ presente: en la revisión del curso."""
import os
import subprocess
import time
import unittest
import labutil
SOLUTION = os.path.join(labutil.HERE, "solution")
@unittest.skipUnless(os.path.isdir(SOLUTION), "sin carpeta solution/; usen make check-reto")
class Reto(unittest.TestCase):
def test_h4_moves_to_vlan_130(self):
self.addCleanup(labutil.destroy, "campus")
labutil.deploy("campus")
lab = labutil.Lab("campus")
time.sleep(3)
for node in ("sw1", "sw2", "r1"):
lab.apply_script(node, f"vlan/campus_{node}.sh")
for node in ("sw2", "r1", "h4"):
lab.apply_script(node, f"solution/reto_{node}.sh")
done = subprocess.run(["python3", "check_lab.py", "--reto"], text=True,
capture_output=True, check=False, cwd=labutil.HERE)
self.assertEqual(done.returncode, 0, done.stdout)
self.assertNotIn("FALLA", done.stdout)
# Un broadcast de h4 no llega a sw1.
lines = lab.capture("sw1", "eth3", lambda: lab.ping("h4", "10.203.130.99"), "arp")
self.assertEqual(lines, [])
if __name__ == "__main__":
unittest.main()Laboratorio
Archivos de partida del laboratorio de la semana.
# Reto de la semana 3 (h4): escriban aquí los comandos de este nodo.
# Se aplica sobre campus.clab.yml ya configurada con vlan/.# Reto de la semana 3 (r1): escriban aquí los comandos de este nodo.
# Se aplica sobre campus.clab.yml ya configurada con vlan/.# Reto de la semana 3 (sw2): escriban aquí los comandos de este nodo.
# Se aplica sobre campus.clab.yml ya configurada con vlan/.Compilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 3: capa de enlace. CRC y detección de errores, acceso múltiple, ARP, switches,
# spanning tree y VLANs. Se usa dentro del contenedor del curso.
#
# make compila crc, aloha y arp_probe en build/
# make test compila, corre las pruebas (test_*.py) y destruye las topologías
# make lan levanta topo/lan.clab.yml (pasos 1 a 3 del laboratorio)
# make campus levanta topo/campus.clab.yml (paso 4)
# make loop levanta topo/loop.clab.yml (demostración de spanning tree)
# make check comprueba campus ya configurada, con las VLANs del paso 4
# make check-reto comprueba el reto (lab/reto_*.sh ya aplicados sobre campus)
# make destroy destruye las topologías y sus redes de Docker
# make clean lo anterior, y borra lo generado
CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -O2
TOPOS = lan campus loop
# Las pruebas no dejan __pycache__ en la carpeta.
export PYTHONDONTWRITEBYTECODE = 1
BINS = build/crc build/aloha build/arp_probe
.PHONY: all test lan campus loop check check-reto destroy clean
all: $(BINS)
build/crc: crc.c
@mkdir -p build
$(CC) $(CFLAGS) -o $@ $<
build/aloha: aloha.c
@mkdir -p build
$(CC) $(CFLAGS) -o $@ $< -lm
# Enlace estático: el binario corre en los nodos, que no tienen las bibliotecas del
# contenedor del curso. Se compila en la misma arquitectura en la que corren los nodos.
build/arp_probe: arp_probe.c
@mkdir -p build
$(CC) $(CFLAGS) -static -o $@ $<
test: all
@python3 -m unittest discover -p 'test_*.py' -v; rc=$$?; $(MAKE) -s destroy; exit $$rc
lan campus loop: all
containerlab deploy -t topo/$@.clab.yml --reconfigure
check:
python3 check_lab.py
check-reto:
python3 check_lab.py --reto
destroy:
@for t in $(TOPOS); do \
containerlab destroy -t topo/$$t.clab.yml --cleanup >/dev/null 2>&1 || true; \
docker network rm redes-w03-$$t >/dev/null 2>&1 || true; \
done
clean: destroy
rm -rf build __pycache__ topo/clab-*