Semana 14 — Data centers, SDN, Wi-Fi y 5G
Objetivos
- Explicar por qué los data centers usan topologías Clos y ECMP.
- Describir cómo los overlays virtualizan la red.
- Explicar la separación entre plano de control y plano de datos en SDN.
- Reconocer qué exigen las cargas de IA a la red.
- Explicar qué hace distinta a una red inalámbrica, y describir Wi-Fi y 5G.
Sesión A — Data centers: topologías Clos, ECMP y overlays · SDN
Lecturas:
- Kurose y Ross, secs. 4.4 (Generalized Forwarding and SDN), 5.5 (The SDN Control Plane), 6.5.2 (VXLANs) y 6.6 (Data Center Networking).
- CS168, Topologies, Routing, Virtualization y Software-Defined Networking.
Lecturas complementarias:
Temas:
- Tráfico norte-sur y este-oeste. Por qué un árbol con un núcleo enorme no alcanza.
- Clos y leaf-spine: muchos caminos iguales con switches baratos. Sobresuscripción, con un cálculo.
- ECMP: repartir por flujo, usando un hash de la 5-tupla. Por qué no por paquete.
- Overlays y VXLAN: la red virtual de un cliente sobre la red física. Cómo se relaciona esto con una VPC y con la red de Kubernetes.
- SDN: match-action y controlador. Caso: la red de Google y Orion.
- Error común: creer que SDN significa que no hay routing distribuido. Es una forma de organizar el plano de control, y muchos data centers usan BGP y controladores a la vez.
Sesión B — ✂️ Redes para IA · Wi-Fi y 5G
Lecturas:
- Kurose y Ross, secs. 7.1–7.5 (Wireless and Mobile Networks).
- CS168, Collective Operations, Wireless Links y Cellular.
Lecturas complementarias:
- CS168, Congestion Control y Host Networking.
Temas:
- ✂️ Entrenar modelos: operaciones colectivas como all-reduce, y por qué un flujo lento retrasa a todos.
- Qué cambia sin cables: atenuación, multitrayecto, ruido, terminal oculto y mayor tasa de error.
- Wi-Fi: CSMA/CA, ACK en el enlace y asociación con un access point. Por qué no se usa detección de colisiones.
- 5G: RAN, núcleo y sesión de usuario. Por qué la red se volvió software.
- Movilidad: dentro de Wi-Fi y en 5G.
- Por qué TCP puede confundir pérdida inalámbrica con congestión.
Laboratorio — Una red leaf-spine con balanceo de carga
Trabajen en code/week-14/, con dos spines, tres leaves y hosts.
- Levantar la topología y configurar BGP entre leaves y spines.
- Mirar en un leaf, con
ip route, las rutas con varios next hop. - Correr varios flujos con
iperf3y medir en cada spine cuánto tráfico pasa. ¿Se reparte igual? ¿Por qué? - Cambiar la política de hash del kernel (
fib_multipath_hash_policy) y repetir. - Apagar un spine y medir cuánto tarda en recuperarse el tráfico. Comparar con la semana 5.
- Crear un túnel VXLAN entre dos hosts y encontrar en una captura el frame interno.
- Avanzar con P5.
Preguntas de repaso
- Una red leaf-spine tiene leaves con 48 puertos de 10 Gbps hacia servidores y 6 de 40 Gbps hacia spines. ¿Cuál es la sobresuscripción?
- ¿Por qué ECMP reparte por flujo y no por paquete? ¿Qué problema trae un flujo muy grande?
- ¿Qué agrega un encabezado VXLAN a cada paquete y qué efecto tiene en la MTU?
- ¿Por qué Wi-Fi usa ACK en el enlace si TCP ya los tiene?
- Un trabajo de entrenamiento se comunica en rondas con todas las GPU. ¿Por qué importa más la peor latencia que el promedio?
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (17)Pruebas (2)Laboratorio (2)Compilación (1)
Ejemplos
Los programas que usan los apuntes y la clase.
/* dcf_sim: un modelo muy simplificado de CSMA/CA, ranura por ranura, en tráfico saturado.
*
* dcf_sim shared <estaciones> <ranuras> todos se oyen entre sí
* dcf_sim hidden <ranuras> dos estaciones que no se oyen, y un access point
* dcf_sim hidden_rts <ranuras> las mismas, con RTS/CTS
*
* Una estación espera un backoff al azar y transmite. Sin ACK, duplica la ventana. No es 802.11
* completo: no hay SIFS ni DIFS ni velocidades, y el ACK y el CTS están dentro de los tiempos de
* abajo. Sirve para comparar casos, no para predecir megabits. Unidad de tiempo: la ranura.
*/
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define CW_MIN 16
#define CW_MAX 1024
#define FRAME_SLOTS 150 /* datos más ACK */
#define RTS_SLOTS 4 /* RTS más CTS */
#define MAX_STATIONS 128
static uint64_t rng_state = 0x2545F4914F6CDD1Dull;
static uint32_t next_random(void) {
rng_state ^= rng_state << 13;
rng_state ^= rng_state >> 7;
rng_state ^= rng_state << 17;
return (uint32_t)(rng_state >> 32);
}
static int new_backoff(int window) {
return (int)(next_random() % (uint32_t)window);
}
static int next_window(int window) {
return window * 2 > CW_MAX ? CW_MAX : window * 2;
}
/* Todos se oyen: mientras alguien transmite, los demás congelan su backoff. Solo chocan
* quienes llegan a cero en la misma ranura. */
static int run_shared(int stations, long slots) {
int backoff[MAX_STATIONS], window[MAX_STATIONS];
for (int i = 0; i < stations; i++) {
window[i] = CW_MIN;
backoff[i] = new_backoff(window[i]);
}
long now = 0, delivered = 0, attempts = 0, failures = 0;
while (now < slots) {
int talking = 0;
for (int i = 0; i < stations; i++) {
talking += backoff[i] == 0;
}
if (talking == 0) {
for (int i = 0; i < stations; i++) {
backoff[i]--;
}
now++;
continue;
}
attempts += talking;
if (talking == 1) {
delivered++;
} else {
failures += talking;
}
for (int i = 0; i < stations; i++) {
if (backoff[i] == 0) {
window[i] = talking == 1 ? CW_MIN : next_window(window[i]);
backoff[i] = new_backoff(window[i]);
}
}
now += FRAME_SLOTS;
}
printf("%3d estaciones que se oyen: tiempo útil %5.1f %%; intentos que chocan %5.1f %%\n", stations,
100.0 * (double)delivered * FRAME_SLOTS / (double)now,
attempts ? 100.0 * (double)failures / (double)attempts : 0.0);
return 0;
}
enum state { BACKOFF, HANDSHAKE, DATA, DEFER };
struct station {
enum state state;
int window;
int left;
int damaged;
};
static void start_backoff(struct station *station) {
station->state = BACKOFF;
station->left = new_backoff(station->window);
}
/* Dos estaciones que el access point oye, pero que no se oyen entre ellas: ninguna congela su
* backoff por la otra. Si dos transmisiones se solapan en el access point, las dos se dañan.
* Con RTS/CTS se arriesgan solo unas pocas ranuras, y el CTS hace callar a la otra (DEFER). */
static int run_hidden(int use_rts, long slots) {
struct station stations[2];
for (int i = 0; i < 2; i++) {
stations[i].window = CW_MIN;
stations[i].damaged = 0;
start_backoff(&stations[i]);
}
long delivered = 0, attempts = 0, failures = 0;
for (long now = 0; now < slots; now++) {
for (int i = 0; i < 2; i++) {
struct station *station = &stations[i];
if (station->state == BACKOFF && station->left == 0) {
attempts++;
station->damaged = 0;
station->state = use_rts ? HANDSHAKE : DATA;
station->left = use_rts ? RTS_SLOTS : FRAME_SLOTS;
}
}
int on_air[2];
for (int i = 0; i < 2; i++) {
on_air[i] = stations[i].state == HANDSHAKE || stations[i].state == DATA;
}
if (on_air[0] && on_air[1]) {
stations[0].damaged = stations[1].damaged = 1;
}
for (int i = 0; i < 2; i++) {
struct station *station = &stations[i];
struct station *other = &stations[1 - i];
station->left--;
if (station->state == BACKOFF || station->left > 0) {
if (station->state == BACKOFF && station->left < 0) {
station->left = 0;
}
continue;
}
if (station->state == DEFER) {
start_backoff(station);
} else if (station->damaged) {
failures++;
station->window = next_window(station->window);
start_backoff(station);
} else if (station->state == HANDSHAKE) {
/* CTS: el access point reserva el medio y la otra estación lo oye. */
station->state = DATA;
station->left = FRAME_SLOTS;
other->state = DEFER;
other->left = FRAME_SLOTS;
} else {
delivered++;
station->window = CW_MIN;
start_backoff(station);
}
}
}
printf("2 estaciones ocultas, %s: tiempo útil %5.1f %%; intentos que chocan %5.1f %%\n",
use_rts ? "con RTS/CTS" : "sin RTS/CTS",
100.0 * (double)delivered * FRAME_SLOTS / (double)slots,
attempts ? 100.0 * (double)failures / (double)attempts : 0.0);
return 0;
}
static long parse_long(const char *text, long low, long high) {
char *end;
long value = strtol(text, &end, 10);
if (*text == '\0' || *end != '\0' || value < low || value > high) {
fprintf(stderr, "valor fuera de rango: %s\n", text);
exit(2);
}
return value;
}
int main(int argc, char **argv) {
if (argc == 4 && strcmp(argv[1], "shared") == 0) {
return run_shared((int)parse_long(argv[2], 1, MAX_STATIONS), parse_long(argv[3], 1, 1000000000));
}
if (argc == 3 && strcmp(argv[1], "hidden") == 0) {
return run_hidden(0, parse_long(argv[2], 1, 1000000000));
}
if (argc == 3 && strcmp(argv[1], "hidden_rts") == 0) {
return run_hidden(1, parse_long(argv[2], 1, 1000000000));
}
fprintf(stderr, "uso: %s shared <estaciones> <ranuras>\n %s hidden|hidden_rts <ranuras>\n",
argv[0], argv[0]);
return 2;
}/* df_ping: un ping con el bit DF prendido y un tamaño exacto. El ping de la imagen de los nodos
* (busybox) no tiene esa opción.
*
* df_ping <ip> <bytes_de_datos> [intentos]
*
* El paquete mide bytes_de_datos + 28 (20 de IP y 8 de ICMP). Sale con 0 si llegó alguna
* respuesta, con 1 si no. Necesita un socket crudo, así que corre como root en los nodos.
*/
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <poll.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#define MAX_DATA 9000
#define ICMP_ECHO_REQUEST 8
#define ICMP_ECHO_REPLY 0
#define ICMP_HEADER 8
#define WAIT_MS 800
static uint16_t checksum(const unsigned char *data, size_t size) {
uint32_t sum = 0;
for (size_t i = 0; i + 1 < size; i += 2) {
sum += (uint32_t)(data[i] << 8 | data[i + 1]);
}
if (size % 2) {
sum += (uint32_t)(data[size - 1] << 8);
}
while (sum >> 16) {
sum = (sum & 0xffff) + (sum >> 16);
}
return (uint16_t)~sum;
}
int main(int argc, char **argv) {
if (argc != 3 && argc != 4) {
fprintf(stderr, "uso: %s <ip> <bytes_de_datos> [intentos]\n", argv[0]);
return 2;
}
struct sockaddr_in remote = {.sin_family = AF_INET};
if (inet_pton(AF_INET, argv[1], &remote.sin_addr) != 1) {
fprintf(stderr, "dirección inválida: %s\n", argv[1]);
return 2;
}
long data = strtol(argv[2], NULL, 10);
long tries = argc == 4 ? strtol(argv[3], NULL, 10) : 3;
if (data < 0 || data > MAX_DATA || tries < 1 || tries > 100) {
fprintf(stderr, "valores fuera de rango\n");
return 2;
}
int fd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (fd < 0) {
perror("socket");
return 1;
}
int discover = IP_PMTUDISC_DO;
if (setsockopt(fd, IPPROTO_IP, IP_MTU_DISCOVER, &discover, sizeof discover) < 0) {
perror("setsockopt");
return 1;
}
uint16_t id = (uint16_t)getpid();
static unsigned char packet[ICMP_HEADER + MAX_DATA];
size_t size = ICMP_HEADER + (size_t)data;
long replies = 0;
for (long attempt = 0; attempt < tries && replies == 0; attempt++) {
memset(packet, 0x5a, size);
packet[0] = ICMP_ECHO_REQUEST;
packet[1] = 0;
packet[2] = packet[3] = 0;
packet[4] = (unsigned char)(id >> 8);
packet[5] = (unsigned char)id;
packet[6] = (unsigned char)(attempt >> 8);
packet[7] = (unsigned char)attempt;
uint16_t sum = checksum(packet, size);
packet[2] = (unsigned char)(sum >> 8);
packet[3] = (unsigned char)sum;
if (sendto(fd, packet, size, 0, (struct sockaddr *)&remote, sizeof remote) < 0) {
/* EMSGSIZE: este mismo host ya sabe que el paquete no cabe. */
if (errno == EMSGSIZE) {
printf("%zu bytes: el host no lo envía (EMSGSIZE)\n", size + 20);
close(fd);
return 1;
}
perror("sendto");
close(fd);
return 1;
}
struct pollfd waiting = {.fd = fd, .events = POLLIN};
while (replies == 0) {
int ready = poll(&waiting, 1, WAIT_MS);
if (ready < 0) {
perror("poll");
close(fd);
return 1;
}
if (ready == 0) {
break;
}
unsigned char reply[20 + ICMP_HEADER + MAX_DATA];
ssize_t got = recv(fd, reply, sizeof reply, 0);
if (got < 0) {
if (errno == EMSGSIZE) {
printf("%zu bytes: llegó un aviso de que no cabe (EMSGSIZE)\n", size + 20);
close(fd);
return 1;
}
perror("recv");
close(fd);
return 1;
}
size_t header = got > 0 ? (size_t)(reply[0] & 0x0f) * 4 : 0;
if ((size_t)got >= header + ICMP_HEADER && reply[header] == ICMP_ECHO_REPLY &&
reply[header + 4] == (unsigned char)(id >> 8) && reply[header + 5] == (unsigned char)id) {
replies++;
}
}
}
if (close(fd) < 0) {
perror("close");
}
printf("%zu bytes: %s\n", size + 20, replies ? "respuesta" : "sin respuesta");
return replies ? 0 : 1;
}/* ecmp_sim: cuentas sobre el reparto de flujos con un hash, sin red de por medio.
*
* ecmp_sim balance <caminos> <flujos> <ensayos> qué tan parejo sale el reparto
* ecmp_sim elephant <caminos> <flujos> <ensayos> flujos grandes que chocan en un camino
* ecmp_sim rehash <caminos> <flujos> cuántos flujos cambian si muere un camino
*
* El hash es FNV-1a sobre la 5-tupla. No es el del kernel, pero sirve igual: lo que importa
* es que sea determinista por flujo y que reparta como si fuera al azar.
*/
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_PATHS 64
struct five_tuple {
uint32_t source_ip;
uint32_t dest_ip;
uint16_t source_port;
uint16_t dest_port;
uint8_t protocol;
};
/* Se recorren los campos uno a uno: una struct puede tener relleno sin inicializar. */
static uint32_t fnv_bytes(uint32_t hash, uint64_t value, int count) {
for (int i = 0; i < count; i++) {
hash ^= (uint8_t)(value >> (8 * i));
hash *= 16777619u;
}
return hash;
}
static uint32_t hash_flow(const struct five_tuple *flow) {
uint32_t hash = 2166136261u;
hash = fnv_bytes(hash, flow->source_ip, 4);
hash = fnv_bytes(hash, flow->dest_ip, 4);
hash = fnv_bytes(hash, flow->source_port, 2);
hash = fnv_bytes(hash, flow->dest_port, 2);
hash = fnv_bytes(hash, flow->protocol, 1);
/* FNV mezcla mal los bits altos con entradas cortas: una vuelta más los reparte. */
hash ^= hash >> 15;
hash *= 2246822519u;
hash ^= hash >> 13;
return hash;
}
/* Generador xorshift con semilla fija: los resultados se pueden repetir. */
static uint64_t rng_state = 0x9E3779B97F4A7C15ull;
static uint32_t next_random(void) {
rng_state ^= rng_state << 13;
rng_state ^= rng_state >> 7;
rng_state ^= rng_state << 17;
return (uint32_t)(rng_state >> 32);
}
static struct five_tuple random_flow(void) {
struct five_tuple flow = {.source_ip = next_random(),
.dest_ip = next_random(),
.source_port = (uint16_t)next_random(),
.dest_port = 443,
.protocol = 6};
return flow;
}
/* Módulo: el hash dividido entre la cantidad de caminos. */
static int pick_modulo(uint32_t hash, int paths) {
return (int)(hash % (uint32_t)paths);
}
/* Hash-threshold (RFC 2992): el espacio del hash se parte en regiones iguales. */
static int pick_threshold(uint32_t hash, int paths) {
return (int)(((uint64_t)hash * (uint64_t)paths) >> 32);
}
/* Rendezvous: cada camino saca un puntaje con el hash del flujo y gana el mayor. Si muere un
* camino, solo se mueven los flujos que estaban en él. */
static int pick_rendezvous(uint32_t hash, const int *alive, int paths) {
int best = -1;
uint32_t best_score = 0;
for (int path = 0; path < paths; path++) {
if (!alive[path]) {
continue;
}
uint32_t score = fnv_bytes(hash, (uint64_t)path, 2);
score ^= score >> 15;
score *= 2246822519u;
score ^= score >> 13;
if (best < 0 || score > best_score) {
best = path;
best_score = score;
}
}
return best;
}
static int run_balance(int paths, int flows, int trials) {
double ratio_sum = 0;
double worst_ratio = 0;
for (int trial = 0; trial < trials; trial++) {
int load[MAX_PATHS] = {0};
for (int i = 0; i < flows; i++) {
struct five_tuple flow = random_flow();
load[pick_threshold(hash_flow(&flow), paths)]++;
}
int largest = 0;
for (int path = 0; path < paths; path++) {
if (load[path] > largest) {
largest = load[path];
}
}
double ratio = (double)largest / ((double)flows / paths);
ratio_sum += ratio;
if (ratio > worst_ratio) {
worst_ratio = ratio;
}
}
printf("%d caminos, %d flujos: el camino más cargado lleva %.2f veces lo justo en promedio "
"(peor ensayo: %.2f)\n", paths, flows, ratio_sum / trials, worst_ratio);
return 0;
}
static int run_elephant(int paths, int flows, int trials) {
/* Cada flujo quiere un enlace completo. Los que comparten un camino se lo reparten, así
* que el total es la cantidad de caminos ocupados. */
long clean = 0;
long used_sum = 0;
for (int trial = 0; trial < trials; trial++) {
int load[MAX_PATHS] = {0};
for (int i = 0; i < flows; i++) {
struct five_tuple flow = random_flow();
load[pick_threshold(hash_flow(&flow), paths)]++;
}
int used = 0;
int collision = 0;
for (int path = 0; path < paths; path++) {
if (load[path] > 0) {
used++;
}
if (load[path] > 1) {
collision = 1;
}
}
used_sum += used;
clean += !collision;
}
printf("%d caminos, %d flujos grandes: sin choques en %.1f %% de los ensayos; "
"caminos ocupados en promedio %.2f; throughput total %.0f %% del ideal\n",
paths, flows, 100.0 * (double)clean / trials, (double)used_sum / trials,
100.0 * ((double)used_sum / trials) / (flows < paths ? flows : paths));
return 0;
}
static int run_rehash(int paths, int flows) {
int alive[MAX_PATHS];
for (int path = 0; path < paths; path++) {
alive[path] = 1;
}
/* Muere un camino de en medio: en módulo y en hash-threshold, la tabla
* se vuelve a numerar con un camino menos. */
long moved_modulo = 0, moved_threshold = 0, moved_rendezvous = 0, on_dead = 0;
int dead = paths / 2;
alive[dead] = 0;
int remap[MAX_PATHS];
for (int path = 0, next = 0; path < paths; path++) {
if (path != dead) {
remap[next++] = path;
}
}
int all[MAX_PATHS];
for (int path = 0; path < paths; path++) {
all[path] = 1;
}
for (int i = 0; i < flows; i++) {
struct five_tuple flow = random_flow();
uint32_t hash = hash_flow(&flow);
int before_modulo = pick_modulo(hash, paths);
int before_threshold = pick_threshold(hash, paths);
int before_rendezvous = pick_rendezvous(hash, all, paths);
on_dead += before_threshold == dead;
moved_modulo += remap[pick_modulo(hash, paths - 1)] != before_modulo;
moved_threshold += remap[pick_threshold(hash, paths - 1)] != before_threshold;
moved_rendezvous += pick_rendezvous(hash, alive, paths) != before_rendezvous;
}
printf("%d caminos, muere uno; estaban en él: %.1f %% de los flujos\n", paths,
100.0 * (double)on_dead / flows);
printf(" módulo: se mueven %.1f %%\n", 100.0 * (double)moved_modulo / flows);
printf(" hash-threshold: se mueven %.1f %%\n", 100.0 * (double)moved_threshold / flows);
printf(" rendezvous: se mueven %.1f %%\n", 100.0 * (double)moved_rendezvous / flows);
return 0;
}
static int parse_count(const char *text, int low, int high) {
char *end;
long value = strtol(text, &end, 10);
if (*text == '\0' || *end != '\0' || value < low || value > high) {
fprintf(stderr, "valor fuera de rango: %s\n", text);
exit(2);
}
return (int)value;
}
int main(int argc, char **argv) {
if (argc == 5 && strcmp(argv[1], "balance") == 0) {
return run_balance(parse_count(argv[2], 1, MAX_PATHS), parse_count(argv[3], 1, 10000000),
parse_count(argv[4], 1, 100000));
}
if (argc == 5 && strcmp(argv[1], "elephant") == 0) {
return run_elephant(parse_count(argv[2], 1, MAX_PATHS), parse_count(argv[3], 1, 10000),
parse_count(argv[4], 1, 10000000));
}
if (argc == 4 && strcmp(argv[1], "rehash") == 0) {
return run_rehash(parse_count(argv[2], 2, MAX_PATHS), parse_count(argv[3], 1, 100000000));
}
fprintf(stderr, "uso: %s balance|elephant <caminos> <flujos> <ensayos>\n"
" %s rehash <caminos> <flujos>\n", argv[0], argv[0]);
return 2;
}/* flow_probe: varios flujos UDP lentos, para ver qué le pasa a cada uno cuando falla un camino.
*
* flow_probe recv <puerto> <segundos>
* flow_probe send <ip> <puerto> <flujos> <intervalo_ms> <segundos>
*
* Cada flujo es un socket con su propio puerto de origen, así que ECMP puede mandar a cada
* uno por un camino distinto. Solo se mide un sentido: el receptor cuenta lo que llega y, por
* flujo, el hueco más largo entre dos datagramas. Dirección numérica: los binarios son estáticos.
*
* Formato del datagrama (8 bytes, orden de red): flujo (32 bits), secuencia (32 bits).
*/
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <poll.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>
#define MAX_FLOWS 256
#define WIRE_SIZE 8
struct flow_stats {
int seen;
uint16_t source_port;
uint32_t received;
uint32_t highest_seq;
double last_ms;
double longest_gap_ms;
};
static double now_ms(void) {
struct timespec ts;
if (clock_gettime(CLOCK_MONOTONIC, &ts) < 0) {
perror("clock_gettime");
exit(1);
}
return ts.tv_sec * 1000.0 + ts.tv_nsec / 1e6;
}
static int parse_int(const char *text, int low, int high, const char *name) {
char *end;
long value = strtol(text, &end, 10);
if (*text == '\0' || *end != '\0' || value < low || value > high) {
fprintf(stderr, "%s fuera de rango: %s\n", name, text);
exit(2);
}
return (int)value;
}
static void put_u32(unsigned char *where, uint32_t value) {
uint32_t net = htonl(value);
memcpy(where, &net, sizeof net);
}
static uint32_t get_u32(const unsigned char *where) {
uint32_t net;
memcpy(&net, where, sizeof net);
return ntohl(net);
}
static int run_receiver(int port, int seconds) {
static struct flow_stats flows[MAX_FLOWS];
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0) {
perror("socket");
return 1;
}
struct sockaddr_in local = {.sin_family = AF_INET,
.sin_port = htons((uint16_t)port),
.sin_addr.s_addr = htonl(INADDR_ANY)};
if (bind(fd, (struct sockaddr *)&local, sizeof local) < 0) {
perror("bind");
close(fd);
return 1;
}
double deadline = now_ms() + seconds * 1000.0;
struct pollfd waiting = {.fd = fd, .events = POLLIN};
for (double now = now_ms(); now < deadline; now = now_ms()) {
int ready = poll(&waiting, 1, (int)(deadline - now) + 1);
if (ready < 0) {
if (errno == EINTR) {
continue;
}
perror("poll");
close(fd);
return 1;
}
if (ready == 0) {
break;
}
unsigned char wire[WIRE_SIZE];
struct sockaddr_in peer;
socklen_t peer_len = sizeof peer;
ssize_t got = recvfrom(fd, wire, sizeof wire, 0, (struct sockaddr *)&peer, &peer_len);
if (got < 0) {
perror("recvfrom");
close(fd);
return 1;
}
if (got != WIRE_SIZE) {
continue;
}
uint32_t id = get_u32(wire);
uint32_t seq = get_u32(wire + 4);
if (id >= MAX_FLOWS) {
continue;
}
struct flow_stats *flow = &flows[id];
double arrival = now_ms();
if (flow->seen && arrival - flow->last_ms > flow->longest_gap_ms) {
flow->longest_gap_ms = arrival - flow->last_ms;
}
flow->seen = 1;
flow->source_port = ntohs(peer.sin_port);
flow->received++;
flow->last_ms = arrival;
if (seq > flow->highest_seq) {
flow->highest_seq = seq;
}
}
if (close(fd) < 0) {
perror("close");
}
printf("flujo puerto_origen recibidos perdidos hueco_max_ms\n");
for (int id = 0; id < MAX_FLOWS; id++) {
const struct flow_stats *flow = &flows[id];
if (flow->seen) {
printf("%5d %13u %9u %8u %12.0f\n", id, flow->source_port, flow->received,
flow->highest_seq + 1 - flow->received, flow->longest_gap_ms);
}
}
return 0;
}
static int run_sender(const char *address, int port, int count, int interval_ms, int seconds) {
struct sockaddr_in remote = {.sin_family = AF_INET, .sin_port = htons((uint16_t)port)};
if (inet_pton(AF_INET, address, &remote.sin_addr) != 1) {
fprintf(stderr, "dirección inválida: %s\n", address);
return 2;
}
int sockets[MAX_FLOWS];
for (int id = 0; id < count; id++) {
sockets[id] = socket(AF_INET, SOCK_DGRAM, 0);
if (sockets[id] < 0) {
perror("socket");
return 1;
}
if (connect(sockets[id], (struct sockaddr *)&remote, sizeof remote) < 0) {
perror("connect");
return 1;
}
}
int rounds = seconds * 1000 / interval_ms;
unsigned long refused = 0;
double start = now_ms();
for (int round = 0; round < rounds; round++) {
for (int id = 0; id < count; id++) {
unsigned char wire[WIRE_SIZE];
put_u32(wire, (uint32_t)id);
put_u32(wire + 4, (uint32_t)round);
if (send(sockets[id], wire, sizeof wire, 0) < 0) {
/* Un ICMP de vuelta o una ruta ausente no detienen la medición. */
if (errno == ECONNREFUSED || errno == ENETUNREACH || errno == EHOSTUNREACH) {
refused++;
} else {
perror("send");
return 1;
}
}
}
double next = start + (round + 1) * (double)interval_ms;
double wait = next - now_ms();
if (wait > 0) {
struct timespec pause = {.tv_sec = (time_t)(wait / 1000),
.tv_nsec = (long)((wait - (time_t)(wait / 1000) * 1000.0) * 1e6)};
if (nanosleep(&pause, NULL) < 0 && errno != EINTR) {
perror("nanosleep");
return 1;
}
}
}
for (int id = 0; id < count; id++) {
if (close(sockets[id]) < 0) {
perror("close");
}
}
printf("enviados %d datagramas en %d flujos; %lu envíos con error de red\n", rounds * count, count,
refused);
return 0;
}
int main(int argc, char **argv) {
if (argc == 4 && strcmp(argv[1], "recv") == 0) {
return run_receiver(parse_int(argv[2], 1, 65535, "puerto"),
parse_int(argv[3], 1, 3600, "segundos"));
}
if (argc == 7 && strcmp(argv[1], "send") == 0) {
return run_sender(argv[2], parse_int(argv[3], 1, 65535, "puerto"),
parse_int(argv[4], 1, MAX_FLOWS, "flujos"),
parse_int(argv[5], 1, 10000, "intervalo"),
parse_int(argv[6], 1, 3600, "segundos"));
}
fprintf(stderr, "uso: %s recv <puerto> <segundos>\n"
" %s send <ip> <puerto> <flujos> <intervalo_ms> <segundos>\n",
argv[0], argv[0]);
return 2;
}"""Ayudas de la semana 14: levantar la topología y correr comandos en sus nodos."""
import os
import subprocess
import time
import unittest
HERE = os.path.dirname(os.path.abspath(__file__))
# La solución no se publica. Sin ella, se usa lo que ustedes escriben en lab/.
ANSWERS = os.path.join(HERE, "solution")
if not os.path.isdir(ANSWERS):
ANSWERS = os.path.join(HERE, "lab")
# scenarios.py lo prende para mostrar cada comando que corre.
ECHO = False
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=400)
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=400)
subprocess.run(["docker", "network", "rm", self.network], capture_output=True, text=True)
def sh(self, node, *command, timeout=90):
"""Corre un comando en un nodo y devuelve el resultado completo."""
if ECHO:
print(f" {node}# {' '.join(command)}", flush=True)
return run(["docker", "exec", self.prefix + node, *command], timeout=timeout)
def out(self, node, *command, timeout=90):
"""Corre un comando en un nodo y devuelve stdout y stderr juntos."""
done = self.sh(node, *command, timeout=timeout)
return done.stdout + done.stderr
def daemon(self, node, command):
"""Deja un comando corriendo en segundo plano dentro de un nodo."""
if ECHO:
print(f" {node}# {command} &", flush=True)
run(["docker", "exec", "-d", self.prefix + node, "sh", "-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()# prioridad | match | acción. Gana la prioridad más alta. Lo que no coincide sigue a BGP.
100 | ip saddr 10.214.1.20 tcp dport 5201 | drop
90 | tcp dport 5201 | via s1
80 | tcp dport 5202 | via s2/* ring_reduce: un all-reduce de verdad sobre TCP, en anillo.
*
* ring_reduce <rango> <n> <puerto> <ip_siguiente> <bytes> <rondas>
*
* Cada nodo tiene un vector de enteros de 64 bits y al final de cada ronda todos deben tener la
* suma de todos los vectores. En cada uno de los n-1 pasos, cada nodo le pasa al siguiente lo
* último que recibió del anterior y lo suma a su resultado. Nadie termina una ronda hasta que
* le llegó el aporte de todos. Eso es lo que queremos medir: un enlace lento atrasa al grupo.
*
* Enviar y recibir ocurren a la vez, con poll. Si todos enviaran primero y recibieran después,
* con vectores grandes los buffers se llenarían y el anillo entero se quedaría trabado.
*/
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <poll.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>
#define CONNECT_TRIES 200
static double now_ms(void) {
struct timespec ts;
if (clock_gettime(CLOCK_MONOTONIC, &ts) < 0) {
perror("clock_gettime");
exit(1);
}
return ts.tv_sec * 1000.0 + ts.tv_nsec / 1e6;
}
static long parse_long(const char *text, long low, long high, const char *name) {
char *end;
long value = strtol(text, &end, 10);
if (*text == '\0' || *end != '\0' || value < low || value > high) {
fprintf(stderr, "%s fuera de rango: %s\n", name, text);
exit(2);
}
return value;
}
/* Cada entero viaja en dos palabras de 32 bits en orden de red. */
static void encode(unsigned char *wire, const uint64_t *values, size_t count) {
for (size_t i = 0; i < count; i++) {
uint32_t high = htonl((uint32_t)(values[i] >> 32));
uint32_t low = htonl((uint32_t)values[i]);
memcpy(wire + 8 * i, &high, 4);
memcpy(wire + 8 * i + 4, &low, 4);
}
}
static void decode(uint64_t *values, const unsigned char *wire, size_t count) {
for (size_t i = 0; i < count; i++) {
uint32_t high, low;
memcpy(&high, wire + 8 * i, 4);
memcpy(&low, wire + 8 * i + 4, 4);
values[i] = ((uint64_t)ntohl(high) << 32) | ntohl(low);
}
}
static int set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL);
if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) {
perror("fcntl");
return -1;
}
return 0;
}
/* Abre el puerto propio, se conecta al siguiente y acepta al anterior. */
static int open_ring(int port, const char *next_ip, int *to_next, int *from_prev) {
int listener = socket(AF_INET, SOCK_STREAM, 0);
if (listener < 0) {
perror("socket");
return -1;
}
int yes = 1;
if (setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes) < 0) {
perror("setsockopt");
return -1;
}
struct sockaddr_in local = {.sin_family = AF_INET,
.sin_port = htons((uint16_t)port),
.sin_addr.s_addr = htonl(INADDR_ANY)};
if (bind(listener, (struct sockaddr *)&local, sizeof local) < 0) {
perror("bind");
return -1;
}
if (listen(listener, 1) < 0) {
perror("listen");
return -1;
}
struct sockaddr_in remote = {.sin_family = AF_INET, .sin_port = htons((uint16_t)port)};
if (inet_pton(AF_INET, next_ip, &remote.sin_addr) != 1) {
fprintf(stderr, "dirección inválida: %s\n", next_ip);
return -1;
}
/* Los nodos no arrancan a la vez: se reintenta mientras el siguiente no esté escuchando. */
*to_next = -1;
for (int attempt = 0; attempt < CONNECT_TRIES; attempt++) {
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
perror("socket");
return -1;
}
if (connect(fd, (struct sockaddr *)&remote, sizeof remote) == 0) {
*to_next = fd;
break;
}
if (errno != ECONNREFUSED && errno != ETIMEDOUT && errno != EHOSTUNREACH && errno != ENETUNREACH) {
perror("connect");
close(fd);
return -1;
}
close(fd);
struct timespec pause = {.tv_sec = 0, .tv_nsec = 100000000};
if (nanosleep(&pause, NULL) < 0 && errno != EINTR) {
perror("nanosleep");
return -1;
}
}
if (*to_next < 0) {
fprintf(stderr, "no se pudo conectar con %s\n", next_ip);
return -1;
}
*from_prev = accept(listener, NULL, NULL);
if (*from_prev < 0) {
perror("accept");
return -1;
}
if (close(listener) < 0) {
perror("close");
}
if (setsockopt(*to_next, IPPROTO_TCP, TCP_NODELAY, &yes, sizeof yes) < 0) {
perror("setsockopt");
return -1;
}
if (set_nonblocking(*to_next) < 0 || set_nonblocking(*from_prev) < 0) {
return -1;
}
return 0;
}
/* Envía out[] y recibe in[], los dos de size bytes, sin que uno bloquee al otro. */
static int exchange(int to_next, int from_prev, const unsigned char *out, unsigned char *in, size_t size) {
size_t sent = 0, received = 0;
while (sent < size || received < size) {
struct pollfd fds[2] = {{.fd = sent < size ? to_next : -1, .events = POLLOUT},
{.fd = received < size ? from_prev : -1, .events = POLLIN}};
if (poll(fds, 2, -1) < 0) {
if (errno == EINTR) {
continue;
}
perror("poll");
return -1;
}
if (fds[0].revents & (POLLOUT | POLLERR | POLLHUP)) {
ssize_t n = send(to_next, out + sent, size - sent, MSG_NOSIGNAL);
if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
perror("send");
return -1;
}
if (n > 0) {
sent += (size_t)n;
}
}
if (fds[1].revents & (POLLIN | POLLERR | POLLHUP)) {
ssize_t n = recv(from_prev, in + received, size - received, 0);
if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
perror("recv");
return -1;
}
if (n == 0) {
fprintf(stderr, "el nodo anterior cerró la conexión\n");
return -1;
}
if (n > 0) {
received += (size_t)n;
}
}
}
return 0;
}
int main(int argc, char **argv) {
if (argc != 7) {
fprintf(stderr, "uso: %s <rango> <n> <puerto> <ip_siguiente> <bytes> <rondas>\n", argv[0]);
return 2;
}
int nodes = (int)parse_long(argv[2], 2, 1024, "n");
int rank = (int)parse_long(argv[1], 0, nodes - 1, "rango");
int port = (int)parse_long(argv[3], 1, 65535, "puerto");
size_t count = (size_t)parse_long(argv[5], 8, 1L << 30, "bytes") / 8;
int rounds = (int)parse_long(argv[6], 1, 100000, "rondas");
size_t size = count * 8;
uint64_t *transit = malloc(size), *result = malloc(size);
unsigned char *out = malloc(size), *in = malloc(size);
if (!transit || !result || !out || !in) {
perror("malloc");
return 1;
}
int to_next, from_prev;
if (open_ring(port, argv[4], &to_next, &from_prev) < 0) {
return 1;
}
for (int round = 0; round < rounds; round++) {
double start = now_ms();
for (size_t i = 0; i < count; i++) {
transit[i] = (uint64_t)(rank + 1 + round) + i;
result[i] = transit[i];
}
for (int step = 0; step < nodes - 1; step++) {
encode(out, transit, count);
if (exchange(to_next, from_prev, out, in, size) < 0) {
return 1;
}
decode(transit, in, count);
for (size_t i = 0; i < count; i++) {
result[i] += transit[i];
}
}
/* Se comprueba la suma: si el anillo perdiera o cruzara datos, aquí se notaría. */
for (size_t i = 0; i < count; i++) {
uint64_t expected = (uint64_t)nodes * ((uint64_t)round + i) +
(uint64_t)nodes * ((uint64_t)nodes + 1) / 2;
if (result[i] != expected) {
fprintf(stderr, "suma incorrecta en la ronda %d\n", round);
return 1;
}
}
printf("ronda %d %.1f ms\n", round, now_ms() - start);
if (fflush(stdout) == EOF) {
perror("fflush");
return 1;
}
}
if (close(to_next) < 0 || close(from_prev) < 0) {
perror("close");
}
free(transit);
free(result);
free(out);
free(in);
return 0;
}#!/usr/bin/env python3
"""Lo que probamos en la red leaf-spine, uno por uno.
python3 scenarios.py <escenario> corre un escenario y muestra lo que mide
python3 scenarios.py reset vuelve al estado inicial
python3 scenarios.py lista los escenarios
Cada escenario imprime los comandos que corre en cada nodo. Léanlos: son lo mismo que harían
a mano. Un escenario no deshace el anterior; para eso está reset.
"""
import json
import os
import re
import statistics
import subprocess
import sys
import threading
import time
import netlab
from netlab import ANSWERS, HERE, Lab, wait_until
LAB = Lab("fabric", "w14-fabric", "redes-w14-fabric")
SPINES = ("s1", "s2")
LEAVES = ("l1", "l2", "l3")
HOSTS = ("h1", "h2", "h3", "h4", "h5", "h6")
TENANTS = ("ta1", "tb1", "ta2", "tb2")
H1, H3 = "10.214.1.10", "10.214.2.10"
S1_FROM_L1, S2_FROM_L1 = "10.214.11.0", "10.214.21.0"
# Por qué interfaz sale cada spine hacia cada leaf.
TOWARD = {"l1": "eth1", "l2": "eth2", "l3": "eth3"}
# Un flujo cuyo hueco pasa de esto se cortó: los datagramas van cada 20 ms.
CUT_MS = 150
RING = (("h1", "10.214.2.10"), ("h3", "10.214.1.20"), ("h2", "10.214.2.20"), ("h4", "10.214.1.10"))
# --- Medir --------------------------------------------------------------------------------
def spine_tx(leaf="l2"):
"""Bytes que cada spine lleva enviados hacia un leaf."""
return {spine: int(LAB.out(spine, "cat", f"/sys/class/net/{TOWARD[leaf]}/statistics/tx_bytes"))
for spine in SPINES}
def next_hop(sport, dport=5201, source=H1, dest=H3, protocol="tcp"):
"""El next hop que elige l1 para un paquete de esta 5-tupla que entra por su LAN."""
text = LAB.out("l1", "ip", "route", "get", dest, "from", source, "iif", "br0",
"ipproto", protocol, "sport", str(sport), "dport", str(dport))
found = re.search(r"via (\S+)", text)
return found.group(1) if found else None
def set_policy(value, nodes=LEAVES):
"""0: direcciones IP. 1: 5-tupla o el hash que ya traiga el paquete. 3: los campos que pidamos."""
for leaf in nodes:
if value == 3:
# 0x37: origen, destino, protocolo, puerto de origen y puerto de destino.
LAB.sh(leaf, "sysctl", "-qw", "net.ipv4.fib_multipath_hash_fields=0x37")
LAB.sh(leaf, "sysctl", "-qw", f"net.ipv4.fib_multipath_hash_policy={value}")
def start_iperf_servers(node, *ports):
for port in ports:
LAB.sh(node, "iperf3", "-s", "-D", "-p", str(port))
time.sleep(0.5)
def stop_programs(nodes):
for node in nodes:
LAB.sh(node, "sh", "-c", "pkill iperf3; pkill flow_probe; pkill ring_reduce; pkill tcpdump; true")
def iperf_mbits(node, dest, *options, port=5201, seconds=4):
"""Corre iperf3 y devuelve (Mbit/s en el receptor, retransmisiones del emisor)."""
done = LAB.sh(node, "iperf3", "-c", dest, "-p", str(port), "-t", str(seconds), "-J", *options,
timeout=seconds + 60)
try:
end = json.loads(done.stdout)["end"]
return end["sum_received"]["bits_per_second"] / 1e6, end["sum_sent"].get("retransmits", 0)
except (ValueError, KeyError):
return 0.0, 0
def nstat(node, *names):
"""Contadores del kernel de un nodo, desde que arrancó su espacio de red."""
text = LAB.out(node, "nstat", "-az", *names)
return {found[0]: int(found[1]) for found in re.findall(r"^(\S+)\s+(\d+)", text, re.MULTILINE)}
def flows_on_failure(fail, flows=16, seconds=30, fail_after=4):
"""Corre flow_probe, aplica una falla y devuelve [(flujo, recibidos, perdidos, hueco_ms)]."""
LAB.sh("h3", "sh", "-c", "pkill flow_probe; true")
receiver = subprocess.Popen(
["docker", "exec", LAB.prefix + "h3", "/w14/flow_probe", "recv", "6000", str(seconds + 3)],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
time.sleep(0.5)
sender = subprocess.Popen(
["docker", "exec", LAB.prefix + "h1", "/w14/flow_probe", "send", H3, "6000", str(flows),
"20", str(seconds)],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
time.sleep(fail_after)
fail()
sender.communicate(timeout=seconds + 30)
text, _ = receiver.communicate(timeout=seconds + 30)
rows = re.findall(r"^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s*$", text, re.MULTILINE)
# El receptor solo mide huecos entre dos datagramas. Si un flujo nunca vuelve, lo que falta
# se ve al contar: se esperaba uno cada 20 ms.
expected = seconds * 1000 // 20
return [(int(r[0]), int(r[2]), expected - int(r[2]), max(int(r[4]), (expected - int(r[2])) * 20))
for r in rows]
def summarize_failure(rows):
hit = [row for row in rows if row[3] >= CUT_MS]
print(f" flujos: {len(rows)}; con corte: {len(hit)}; sin corte: {len(rows) - len(hit)}")
if hit:
print(f" hueco en los flujos con corte: {min(r[3] for r in hit)} a "
f"{max(r[3] for r in hit)} ms")
calm = [r[3] for r in rows if r[3] < CUT_MS]
if calm:
print(f" hueco máximo en los flujos sin corte: {max(calm)} ms")
# --- Hash y reparto ------------------------------------------------------------------------
def policy0():
"""Hash solo con las direcciones IP (el valor de fábrica) y 16 flujos de h1 a h3."""
set_policy(0)
measure_flows()
def policy1():
"""Hash con la 5-tupla y los mismos 16 flujos."""
set_policy(1)
measure_flows()
def measure_flows(flows=16, seconds=4):
"""Reparte flujos de h1 a h3 y devuelve qué fracción cruzó cada spine."""
start_iperf_servers("h3", 5201)
before = spine_tx()
rate, _ = iperf_mbits("h1", H3, "-P", str(flows), seconds=seconds)
after = spine_tx()
delta = {spine: after[spine] - before[spine] for spine in SPINES}
total = sum(delta.values()) or 1
for spine in SPINES:
print(f" {spine} hacia l2: {delta[spine] / 1e6:10.1f} MB {100 * delta[spine] / total:5.1f} %")
print(f" total: {rate:.0f} Mbit/s (depende de la computadora)")
return {spine: delta[spine] / total for spine in SPINES}
def routeget():
"""Pregunta al kernel qué next hop usaría para 64 puertos de origen, con cada política."""
results = {}
for policy in (0, 1, 3):
set_policy(policy)
counts = {}
for sport in range(40000, 40064):
hop = next_hop(sport)
counts[hop] = counts.get(hop, 0) + 1
results[policy] = counts
print(f" política {policy}: {counts}")
return results
def predict(flows=12):
"""¿Acierta ip route get con el camino real de un flujo TCP? Con la política 1 y con la 3."""
start_iperf_servers("h3", 5201)
results = {}
# Cada corrida usa puertos nuevos: un puerto recién usado se queda en TIME_WAIT.
first = 20000 + int(time.time()) % 20000
for policy in (1, 3):
set_policy(policy)
hits = 0
for offset in range(flows):
sport = first + offset
hop = next_hop(sport)
before = spine_tx()
iperf_mbits("h1", H3, "--cport", str(sport), seconds=1)
after = spine_tx()
used = S1_FROM_L1 if after["s1"] - before["s1"] > after["s2"] - before["s2"] else S2_FROM_L1
hits += used == hop
first += flows
results[policy] = hits
print(f" política {policy}: ip route get acertó {hits} de {flows} flujos")
return results
def shape(rate="100mbit"):
"""Limita a rate los dos uplinks de l1: así el spine se vuelve el cuello de botella."""
for dev in ("eth1", "eth2"):
LAB.sh("l1", "tc", "qdisc", "replace", "dev", dev, "root", "tbf", "rate", rate,
"burst", "64kb", "latency", "20ms")
def port_on(hop, dport, first):
"""Busca un puerto de origen para que la 5-tupla caiga en ese next hop, según el kernel."""
for sport in range(first, first + 200):
if next_hop(sport, dport) == hop:
return sport
raise RuntimeError(f"ningún puerto de origen cae en {hop}")
def two_elephants(cports):
"""Dos iperf3 a la vez, cada uno con su puerto de origen. Devuelve los Mbit/s de cada uno."""
start_iperf_servers("h3", 5201, 5202)
jobs = [subprocess.Popen(
["docker", "exec", LAB.prefix + "h1", "iperf3", "-c", H3, "-p", str(port), "-t", "6", "-J",
"--cport", str(cport)], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)
for port, cport in zip((5201, 5202), cports)]
rates = []
for job in jobs:
out, _ = job.communicate(timeout=60)
try:
rates.append(json.loads(out)["end"]["sum_received"]["bits_per_second"] / 1e6)
except (ValueError, KeyError):
rates.append(0.0)
return rates
def elephants():
"""Dos flujos grandes con uplinks de 100 Mbit/s: chocando en un spine y separados."""
set_policy(3)
shape()
together = two_elephants((port_on(S1_FROM_L1, 5201, 41000), port_on(S1_FROM_L1, 5202, 41200)))
apart = two_elephants((port_on(S1_FROM_L1, 5201, 42000), port_on(S2_FROM_L1, 5202, 42200)))
print(f" los dos por s1: {together[0]:6.1f} + {together[1]:6.1f} = {sum(together):6.1f} Mbit/s")
print(f" uno por cada spine: {apart[0]:6.1f} + {apart[1]:6.1f} = {sum(apart):6.1f} Mbit/s")
return together, apart
# --- Por qué no por paquete --------------------------------------------------------------
def steer_tables():
"""Tablas 101 y 102 de l1: una sola salida en cada una. Las usan perpacket y sdn_ctl.py."""
LAB.sh("l1", "ip", "route", "replace", "10.214.2.0/24", "via", S1_FROM_L1, "table", "101")
LAB.sh("l1", "ip", "route", "replace", "10.214.2.0/24", "via", S2_FROM_L1, "table", "102")
for mark, table in ((1, 101), (2, 102)):
LAB.sh("l1", "ip", "rule", "add", "fwmark", str(mark), "lookup", str(table))
def unsteer():
LAB.sh("l1", "nft", "delete", "table", "ip", "w14steer")
for mark, table in ((1, 101), (2, 102)):
LAB.sh("l1", "ip", "rule", "del", "fwmark", str(mark), "lookup", str(table))
LAB.sh("l1", "ip", "route", "flush", "table", str(table))
def two_paths(rate="100mbit", delay="2ms"):
"""Dos uplinks iguales para BGP, pero el de s2 con 2 ms más de latencia."""
LAB.sh("l1", "tc", "qdisc", "replace", "dev", "eth1", "root", "netem", "rate", rate, "limit", "2000")
LAB.sh("l1", "tc", "qdisc", "replace", "dev", "eth2", "root", "netem", "rate", rate, "delay", delay,
"limit", "2000")
def nft_install(rules):
done = LAB.sh("l1", "nft", rules)
if done.returncode != 0:
raise RuntimeError(done.stderr)
def perpacket_on():
steer_tables()
rules = ("table ip w14steer { chain pre { type filter hook prerouting priority mangle; "
"ip saddr 10.214.1.10 ip daddr 10.214.2.10 meta mark set numgen inc mod 2 offset 1; }; }")
nft_install(rules)
def reorder_run(seconds=5):
"""Un flujo TCP de h1 a h3. Devuelve Mbit/s, retransmisiones y segmentos fuera de orden."""
names = ("TcpExtTCPOFOQueue",)
before = nstat("h3", *names).get(names[0], 0)
rate, retransmits = iperf_mbits("h1", H3, seconds=seconds)
after = nstat("h3", *names).get(names[0], 0)
print(f" {rate:8.0f} Mbit/s {retransmits:6d} retransmisiones {after - before:8d} fuera de orden en h3")
return rate, retransmits, after - before
def perpacket():
"""Un flujo TCP por dos caminos con 2 ms de diferencia: por flujo y por paquete."""
set_policy(3)
start_iperf_servers("h3", 5201)
two_paths()
print(" por flujo:")
per_flow = reorder_run()
perpacket_on()
print(" por paquete:")
per_packet = reorder_run()
unsteer()
return per_flow, per_packet
# --- Fallas --------------------------------------------------------------------------------
def spine_links(spine):
"""(nodo, interfaz) de los dos extremos de cada enlace de un spine."""
number = spine[1]
ends = [(spine, f"eth{i}") for i in (1, 2, 3)]
ends += [(leaf, f"eth{number}") for leaf in LEAVES]
return ends
def cut_silently(spine="s1"):
for node, dev in spine_links(spine):
LAB.sh(node, "tc", "qdisc", "replace", "dev", dev, "root", "netem", "loss", "100%")
def cut_with_carrier(spine="s1"):
for dev in ("eth1", "eth2", "eth3"):
LAB.sh(spine, "ip", "link", "set", dev, "down")
def set_timers(keepalive, hold):
"""Primero se configura en todos y después se reinicia: el hold se negocia al mínimo de los dos."""
for node in (*SPINES, "l1", "l2"):
LAB.sh(node, "vtysh", "-c", "configure terminal", "-c", f"router bgp {ASN[node]}",
"-c", f"timers bgp {keepalive} {hold}", "-c", "end")
for node in (*SPINES, "l1", "l2"):
LAB.sh(node, "vtysh", "-c", "clear bgp *")
def bfd_config(enable):
"""Prende o apaga BFD en cada sesión entre los spines y l1, l2."""
no = "" if enable else "no "
for spine, leaf, address in NEIGHBORS:
for node, peer in ((spine, address), (leaf, spine_side(address))):
LAB.sh(node, "vtysh", "-c", "configure terminal", "-c", f"router bgp {ASN[node]}",
"-c", f"{no}neighbor {peer} bfd", "-c", "end")
def spine_side(address):
return address[:-1] + "0"
ASN = {"s1": 65140, "s2": 65140, "l1": 65141, "l2": 65142, "l3": 65143}
NEIGHBORS = (("s1", "l1", "10.214.11.1"), ("s1", "l2", "10.214.12.1"),
("s2", "l1", "10.214.21.1"), ("s2", "l2", "10.214.22.1"))
def ecmp_ready():
"""¿l1 tiene dos next hop hacia la LAN de l2 y hay paso de h1 a h3?"""
route = LAB.out("l1", "ip", "route", "show", "10.214.2.0/24")
return route.count("nexthop") == 2 and "linkdown" not in route and ", 0% packet loss" in LAB.out(
"h1", "ping", "-c", "1", "-W", "1", H3)
def carrier():
"""Falla con aviso: se apagan las interfaces de s1. Los flujos que iban por él se cortan."""
set_policy(1)
summarize = flows_on_failure(cut_with_carrier, seconds=12)
summarize_failure(summarize)
return summarize
def silent():
"""Falla silenciosa de s1 con temporizadores 3/9 (keepalive/hold)."""
set_policy(1)
rows = flows_on_failure(cut_silently, seconds=18)
summarize_failure(rows)
return rows
def silent_short():
"""Falla silenciosa de s1 con temporizadores 1/3."""
set_policy(1)
set_timers(1, 3)
wait_until(ecmp_ready, seconds=40)
rows = flows_on_failure(cut_silently, seconds=12)
summarize_failure(rows)
return rows
def watch_l1(seconds, cut_at, log):
"""Anota cuándo cambian, en l1, la entrada ARP de s1 y los next hop hacia l2."""
start = time.monotonic()
last = None
while time.monotonic() - start < seconds:
neighbor = LAB.out("l1", "ip", "neigh", "show", S1_FROM_L1).split()
state = (neighbor[-1] if neighbor else "sin entrada",
LAB.out("l1", "ip", "route", "show", "10.214.2.0/24").count("via"))
if state != last:
log.append((time.monotonic() - start - cut_at, *state))
last = state
time.sleep(0.5)
def silent_default():
"""Falla silenciosa de s1 con los temporizadores de fábrica, 60/180. Tarda unos cuatro minutos."""
set_policy(1)
set_timers(60, 180)
wait_until(ecmp_ready, seconds=40)
log = []
watcher = threading.Thread(target=watch_l1, args=(205, 4, log))
watcher.start()
rows = flows_on_failure(cut_silently, seconds=210)
watcher.join()
summarize_failure(rows)
for moment, neighbor, hops in log:
print(f" a los {moment:6.1f} s del corte: ARP de s1 {neighbor}; next hop hacia l2 en la tabla: {hops}")
return rows
def silent_bfd():
"""Falla silenciosa de s1 con BFD: 100 ms y multiplicador 3."""
set_policy(1)
for spine, leaf, address in NEIGHBORS:
for node, peer in ((spine, address), (leaf, spine_side(address))):
LAB.sh(node, "vtysh", "-c", "configure terminal", "-c", "bfd", "-c", f"peer {peer}",
"-c", "receive-interval 100", "-c", "transmit-interval 100", "-c", "end")
bfd_config(True)
wait_until(lambda: LAB.out("l1", "vtysh", "-c", "show bfd peers brief").count("up") >= 2, seconds=30)
print(LAB.out("l1", "vtysh", "-c", "show bfd peers brief"))
rows = flows_on_failure(cut_silently, seconds=12)
summarize_failure(rows)
return rows
def partial():
"""Se cae solo el enlace s1-l2: l1 deja de usar s1 para l2, y lo sigue usando para lo demás."""
LAB.sh("s1", "ip", "link", "set", "eth2", "down")
time.sleep(2)
print(LAB.out("l1", "ip", "route", "show", "10.214.2.0/24"))
print(LAB.out("s1", "vtysh", "-c", "show bgp ipv4 unicast 10.214.2.0/24"))
# --- VXLAN ---------------------------------------------------------------------------------
VTEP = {"l1": ("10.214.0.1", "10.214.0.2"), "l2": ("10.214.0.2", "10.214.0.1")}
SEGMENTS = ((100, "eth5"), (200, "eth6"))
def overlay(vxlan_mtu=1450):
"""Dos clientes con las mismas direcciones: VNI 100 y VNI 200, con VTEP en l1 y l2."""
for leaf, (local, remote) in VTEP.items():
for vni, port in SEGMENTS:
LAB.sh(leaf, "ip", "link", "add", f"vx{vni}", "type", "vxlan", "id", str(vni),
"local", local, "dstport", "4789", "srcport", "49152", "65535")
LAB.sh(leaf, "ip", "link", "set", f"vx{vni}", "mtu", str(vxlan_mtu))
# Lista de VTEP a los que se copia lo que no se sabe a quién mandar.
LAB.sh(leaf, "bridge", "fdb", "append", "00:00:00:00:00:00", "dev", f"vx{vni}", "dst", remote)
LAB.sh(leaf, "ip", "link", "add", f"br{vni}", "type", "bridge")
LAB.sh(leaf, "ip", "link", "set", f"vx{vni}", "master", f"br{vni}")
LAB.sh(leaf, "ip", "link", "set", port, "master", f"br{vni}")
LAB.sh(leaf, "ip", "link", "set", f"vx{vni}", "up")
LAB.sh(leaf, "ip", "link", "set", f"br{vni}", "up")
if netlab.ECHO:
print(LAB.out("ta1", "ping", "-c", "2", "10.99.0.2"))
print(LAB.out("l1", "bridge", "fdb", "show", "dev", "vx100"))
def overlay_down():
for leaf in VTEP:
for vni, port in SEGMENTS:
LAB.sh(leaf, "ip", "link", "set", port, "nomaster")
LAB.sh(leaf, "ip", "link", "del", f"vx{vni}")
LAB.sh(leaf, "ip", "link", "del", f"br{vni}")
for tenant in TENANTS:
LAB.sh(tenant, "ip", "neigh", "flush", "all")
LAB.sh(tenant, "ip", "link", "set", "eth1", "mtu", "1500")
for node in (*SPINES, "l1", "l2"):
for dev in ("eth1", "eth2"):
LAB.sh(node, "ip", "link", "set", dev, "mtu", "1500")
def host_tunnel_down():
for host in ("h1", "h3"):
LAB.sh(host, "ip", "link", "del", "vx77")
def host_tunnel(srcport=("49152", "65535")):
"""VTEP en los hosts h1 y h3, para que l1 reparta el tráfico ya encapsulado."""
for host, remote, inner in (("h1", H3, "10.77.0.1/24"), ("h3", H1, "10.77.0.2/24")):
LAB.sh(host, "ip", "link", "add", "vx77", "type", "vxlan", "id", "77", "remote", remote,
"dstport", "4789", "dev", "eth1", "srcport", *srcport)
LAB.sh(host, "ip", "addr", "add", inner, "dev", "vx77")
LAB.sh(host, "ip", "link", "set", "vx77", "up")
def tunnel_spread(srcport=("49152", "65535")):
"""16 flujos TCP dentro del túnel de hosts: ¿qué fracción cruza cada spine?"""
set_policy(3)
host_tunnel(srcport)
start_iperf_servers("h3", 5201)
wait_until(lambda: ", 0% packet loss" in LAB.out("h1", "ping", "-c", "1", "-W", "1", "10.77.0.2"), seconds=10)
before = spine_tx()
iperf_mbits("h1", "10.77.0.2", "-P", "16", seconds=3)
after = spine_tx()
host_tunnel_down()
delta = {spine: after[spine] - before[spine] for spine in SPINES}
total = sum(delta.values()) or 1
for spine in SPINES:
print(f" {spine} hacia l2: {delta[spine] / 1e6:9.1f} MB {100 * delta[spine] / total:5.1f} %")
return {spine: delta[spine] / total for spine in SPINES}
def entropy():
"""El túnel entre hosts con todos los puertos de origen externos, y con uno solo."""
print(" puerto de origen externo según el flujo interno (49152 a 65535):")
wide = tunnel_spread()
print(" puerto de origen externo fijo (50000):")
narrow = tunnel_spread(("50000", "50001"))
return wide, narrow
def capture_vxlan(filename, action, wait=1.0):
"""Captura VXLAN en los dos spines mientras corre action. Devuelve las rutas de los pcap."""
os.makedirs(os.path.join(HERE, "captures"), exist_ok=True)
paths = []
for spine in SPINES:
path = f"/captures/{filename}-{spine}.pcap"
paths.append(os.path.join(HERE, "captures", f"{filename}-{spine}.pcap"))
LAB.daemon(spine, f"tcpdump -i eth1 -n -s 200 -U -Z root -w {path} udp port 4789")
time.sleep(1.5)
action()
time.sleep(wait)
for spine in SPINES:
LAB.sh(spine, "pkill", "tcpdump")
time.sleep(0.5)
return paths
def read_capture(paths, *options):
out = ""
for path in paths:
done = subprocess.run(["tshark", "-r", path, *options], capture_output=True, text=True, timeout=60)
out += done.stdout
return out
def tenants():
"""Lo que ve cada cliente, y un ping de ta1 capturado en el underlay."""
print(LAB.out("ta1", "ping", "-c", "1", "10.99.0.2"))
print(LAB.out("tb1", "ping", "-c", "1", "10.99.0.2"))
for node in ("ta1", "tb1", "ta2", "tb2"):
print(f" {node}: {LAB.out(node, 'cat', '/sys/class/net/eth1/address').strip()} "
f"{LAB.out(node, 'ip', 'neigh', 'show', 'dev', 'eth1').strip()}")
paths = capture_vxlan("vxlan", lambda: LAB.sh("ta1", "ping", "-c", "3", "-i", "0.2", "10.99.0.2"))
print(read_capture(paths, "-Y", "icmp", "-T", "fields", "-e", "ip.src", "-e", "ip.dst", "-e",
"udp.srcport", "-e", "udp.dstport", "-e", "vxlan.vni", "-e", "eth.src", "-e",
"eth.dst", "-e", "icmp.type"))
def inner_ports():
"""Ocho flujos TCP dentro de un cliente: ¿cuántos puertos de origen externos se ven?"""
set_policy(1)
LAB.sh("ta2", "iperf3", "-s", "-D", "-p", "5201")
time.sleep(0.5)
before = spine_tx("l2")
paths = capture_vxlan("entropy", lambda: LAB.sh(
"ta1", "iperf3", "-c", "10.99.0.2", "-P", "8", "-t", "2", "-M", "1300", timeout=60))
after = spine_tx("l2")
# El capturador corre en el eth1 de cada spine; lo que importa es qué spine llevó qué.
text = read_capture(paths, "-T", "fields", "-e", "udp.srcport")
ports = {line.strip() for line in text.splitlines() if line.strip()}
print(f" puertos de origen externos distintos: {len(ports)}")
for spine in SPINES:
print(f" {spine} hacia l2: {(after[spine] - before[spine]) / 1e6:.1f} MB")
LAB.sh("ta2", "pkill", "iperf3")
return ports, {spine: after[spine] - before[spine] for spine in SPINES}
def tenant_ping(size):
"""Un ping con DF de ta1 a ta2. El paquete mide size + 28 bytes."""
return LAB.sh("ta1", "/w14/df_ping", "10.99.0.2", str(size)).returncode == 0
def tenant_tcp():
"""Mbit/s de TCP entre ta1 y ta2; 0 si la conexión no logra pasar datos."""
LAB.sh("ta2", "iperf3", "-s", "-D", "-p", "5201")
time.sleep(0.5)
rate, _ = iperf_mbits("ta1", "10.99.0.2", "--connect-timeout", "3000", seconds=3)
LAB.sh("ta2", "pkill", "iperf3")
return rate
def mtu_table():
"""Pings con DF desde un cliente, justo en el borde y en el tamaño completo, más un flujo TCP."""
sizes = {size: tenant_ping(size) for size in (1422, 1423, 1472)}
for size, passed in sizes.items():
print(f" df_ping {size} (paquete de {size + 28} bytes): {'pasa' if passed else 'no pasa'}")
rate = tenant_tcp()
print(f" TCP de ta1 a ta2: {rate:.0f} Mbit/s")
return sizes, rate
def tenant_mtu():
"""Una salida: decirle a cada cliente que su MTU es 1450."""
for tenant in TENANTS:
LAB.sh(tenant, "ip", "link", "set", "eth1", "mtu", "1450")
def jumbo_underlay(mtu=1550):
"""La otra salida, la de los data centers: darle más MTU al underlay."""
for node in (*SPINES, "l1", "l2"):
for dev in ("eth1", "eth2"):
LAB.sh(node, "ip", "link", "set", dev, "mtu", str(mtu))
for leaf in VTEP:
for vni, _ in SEGMENTS:
LAB.sh(leaf, "ip", "link", "set", f"vx{vni}", "mtu", "1500")
def warn():
"""VXLAN con MTU 1500 sobre un underlay de 1500: Linux avisa al cliente con ICMP."""
overlay(vxlan_mtu=1500)
LAB.sh("ta1", "ping", "-c", "1", "10.99.0.2")
first = LAB.out("ta1", "/w14/df_ping", "10.99.0.2", "1472").strip()
print(f" df_ping 1472, primera vez: {first}")
learned = " ".join(LAB.out("ta1", "ip", "route", "get", "10.99.0.2").split())
print(f" ta1 ahora cree: {learned}")
print(f" df_ping 1472, otra vez: {LAB.out('ta1', '/w14/df_ping', '10.99.0.2', '1472').strip()}")
rate = tenant_tcp()
print(f" TCP de ta1 a ta2: {rate:.0f} Mbit/s")
return learned, rate
# --- Trabajo colectivo -----------------------------------------------------------------
def ring_rounds(bytes_count=1000000, rounds=10):
"""Un all-reduce en anillo con h1, h3, h2 y h4. Devuelve los ms de cada ronda en cada nodo."""
jobs = []
for rank, (node, next_ip) in enumerate(RING):
jobs.append(subprocess.Popen(
["docker", "exec", LAB.prefix + node, "/w14/ring_reduce", str(rank), str(len(RING)), "6100",
next_ip, str(bytes_count), str(rounds)],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True))
results = {}
for (node, _), job in zip(RING, jobs):
out, _ = job.communicate(timeout=240)
results[node] = [float(t) for t in re.findall(r"ronda \d+ ([\d.]+) ms", out)]
return results
def collective():
"""Un all-reduce entre cuatro hosts, sin y con un host atrasado."""
start = ring_rounds()
LAB.sh("h4", "tc", "qdisc", "replace", "dev", "eth1", "root", "tbf", "rate", "100mbit",
"burst", "32kb", "latency", "20ms")
slow = ring_rounds()
LAB.sh("h4", "tc", "qdisc", "del", "dev", "eth1", "root")
for node in ("h1", "h3", "h2", "h4"):
print(f" {node}: mediana sin atraso {statistics.median(start[node]):7.1f} ms; "
f"con h4 a 100 Mbit/s {statistics.median(slow[node]):7.1f} ms")
return start, slow
# --- Un enlace con pérdida que no es congestión -------------------------------------------
def lossy_link(*netem):
LAB.sh("l2", "tc", "qdisc", "replace", "dev", "eth3", "root", "netem", "rate", "100mbit",
"delay", "20ms", "limit", "2000", *netem)
def wireless():
"""TCP sobre un último enlace de 100 Mbit/s y 40 ms de RTT, con y sin pérdida."""
start_iperf_servers("h3", 5201)
results = {}
for name, netem in (("sin pérdida", ()), ("0,1 % de pérdida", ("loss", "0.1%")),
("1 % de pérdida", ("loss", "1%")),
("sin pérdida, con 4 ms de jitter", ("delay", "20ms", "4ms"))):
LAB.sh("l2", "tc", "qdisc", "del", "dev", "eth3", "root")
lossy_link(*netem)
rate, retransmits = iperf_mbits("h1", H3, seconds=8)
results[name] = rate
print(f" {name:32} {rate:7.1f} Mbit/s {retransmits:6d} retransmisiones")
LAB.sh("l2", "tc", "qdisc", "del", "dev", "eth3", "root")
return results
# --- Laboratorio --------------------------------------------------------------------------
def load_l3(path=None):
"""Carga en l3 la configuración de BGP de lab/l3-bgp.conf."""
path = path or os.path.join(ANSWERS, "l3-bgp.conf")
with open(path, encoding="utf-8") as file:
lines = [line.strip() for line in file if line.strip() and not line.strip().startswith("!")]
args = ["vtysh", "-c", "configure terminal"]
for line in lines:
args += ["-c", line]
print(LAB.out("l3", *args))
def load_vxlan(path=None):
"""Corre lab/vxlan.sh, que crea un túnel entre h2 y h6."""
path = path or os.path.join(ANSWERS, "vxlan.sh")
with open(path, encoding="utf-8") as file:
script = file.read()
done = subprocess.run(["sh", "-c", script], capture_output=True, text=True, timeout=120,
env={**os.environ, "P": LAB.prefix})
print(done.stdout + done.stderr, end="")
return done
# --- Volver al inicio ---------------------------------------------------------------------
def reset():
"""Vuelve al estado inicial."""
stop_programs((*HOSTS, *SPINES, "l1", "l2", "l3", "ta1", "ta2"))
for node in (*SPINES, *LEAVES):
for dev in ("eth1", "eth2", "eth3"):
LAB.sh(node, "tc", "qdisc", "del", "dev", dev, "root")
if node in SPINES:
LAB.sh(node, "ip", "link", "set", dev, "up")
for host in HOSTS:
LAB.sh(host, "tc", "qdisc", "del", "dev", "eth1", "root")
LAB.sh("l2", "tc", "qdisc", "del", "dev", "eth3", "root")
set_policy(0)
unsteer()
overlay_down()
host_tunnel_down()
bfd_config(False)
for spine, leaf, address in NEIGHBORS:
for node, peer in ((spine, address), (leaf, spine_side(address))):
LAB.sh(node, "vtysh", "-c", "configure terminal", "-c", "bfd", "-c", f"no peer {peer}", "-c", "end")
if "timers bgp 3 9" not in LAB.out("l1", "vtysh", "-c", "show running-config"):
set_timers(3, 9)
wait_until(ecmp_ready, seconds=40)
SCENARIOS = {
"routeget": routeget,
"predict": predict,
"policy0": policy0,
"policy1": policy1,
"elephants": elephants,
"perpacket": perpacket,
"partial": partial,
"carrier": carrier,
"silent": silent,
"silent_short": silent_short,
"silent_default": silent_default,
"silent_bfd": silent_bfd,
"overlay": overlay,
"tenants": tenants,
"inner_ports": inner_ports,
"mtu": mtu_table,
"tenant_mtu": tenant_mtu,
"jumbo": jumbo_underlay,
"warn": warn,
"entropy": entropy,
"collective": collective,
"wireless": wireless,
"l3": load_l3,
"vxlan": load_vxlan,
"reset": reset,
}
def main():
if len(sys.argv) != 2 or sys.argv[1] not in SCENARIOS:
for name, function in SCENARIOS.items():
print(f"{name:14} {(function.__doc__ or 'vuelve al estado inicial').strip().splitlines()[0]}")
return 1
netlab.ECHO = True
SCENARIOS[sys.argv[1]]()
return 0
if __name__ == "__main__":
sys.exit(main())#!/usr/bin/env python3
"""Un controlador de juguete: traduce una política match-action a reglas en el leaf l1.
python3 sdn_ctl.py install <politica> instala la política en l1
python3 sdn_ctl.py show muestra las reglas y sus contadores
python3 sdn_ctl.py remove quita todo: el tráfico vuelve a seguir a BGP
Cada línea de la política dice: prioridad | match | acción. El match es una expresión de nft y
la acción es drop, via s1 o via s2. No es OpenFlow y corre una sola vez; lo que muestra es la
separación: aquí se decide, y el leaf solo aplica lo que le instalan. Con "via", la regla le gana
a BGP: si ese spine muere, el leaf sigue mandando allí hasta que el controlador cambie la regla.
"""
import sys
import netlab
import scenarios
from scenarios import LAB
ACTIONS = {"drop": "drop", "via s1": "meta mark set 1 accept", "via s2": "meta mark set 2 accept"}
def parse_policy(path):
"""Lee la política y devuelve [(prioridad, match, acción)], de mayor a menor prioridad."""
rules = []
with open(path, encoding="utf-8") as file:
for number, line in enumerate(file, 1):
line = line.split("#")[0].strip()
if not line:
continue
parts = [part.strip() for part in line.split("|")]
if len(parts) != 3 or parts[2] not in ACTIONS or not parts[0].isdigit():
raise ValueError(f"{path}:{number}: se esperaba prioridad | match | acción")
rules.append((int(parts[0]), parts[1], parts[2]))
return sorted(rules, key=lambda rule: -rule[0])
def compile_rules(rules):
"""Convierte las reglas en una tabla de nftables, en una sola transacción."""
body = " ".join(f"{match} counter {ACTIONS[action]};" for _, match, action in rules)
return ("table ip w14steer { chain pre { type filter hook prerouting priority mangle; "
f"{body} }}; }}")
def install(path):
rules = parse_policy(path)
scenarios.steer_tables()
scenarios.nft_install(compile_rules(rules))
print(f"instaladas {len(rules)} reglas en l1")
def show():
print(LAB.out("l1", "nft", "list", "table", "ip", "w14steer"))
def main():
netlab.ECHO = True
if len(sys.argv) == 3 and sys.argv[1] == "install":
install(sys.argv[2])
elif len(sys.argv) == 2 and sys.argv[1] == "show":
show()
elif len(sys.argv) == 2 and sys.argv[1] == "remove":
scenarios.unsteer()
else:
print(__doc__)
return 2
return 0
if __name__ == "__main__":
sys.exit(main())#!/usr/bin/env python3
"""Por qué en un trabajo colectivo importa la peor latencia y no el promedio.
python3 tail_rounds.py <trabajadores> <probabilidad_lenta> <ms_normal> <ms_lento> [rondas]
Cada ronda espera al más lento de los trabajadores. Cada uno tarda ms_normal, y con la
probabilidad dada tarda ms_lento. Compara lo simulado con la cuenta exacta.
"""
import random
import sys
def expected_slow_fraction(workers, probability):
"""Probabilidad de que al menos un trabajador sea lento en una ronda."""
return 1 - (1 - probability) ** workers
def simulate(workers, probability, normal, slow, rounds, seed=14):
"""Devuelve (promedio por trabajador, promedio por ronda, fracción de rondas lentas)."""
rng = random.Random(seed)
worker_total = 0.0
round_total = 0.0
slow_rounds = 0
for _ in range(rounds):
slow_workers = sum(rng.random() < probability for _ in range(workers))
worker_total += slow_workers * slow + (workers - slow_workers) * normal
round_total += slow if slow_workers else normal
slow_rounds += bool(slow_workers)
return worker_total / (workers * rounds), round_total / rounds, slow_rounds / rounds
def main():
if len(sys.argv) not in (5, 6):
print(__doc__)
return 2
workers, probability = int(sys.argv[1]), float(sys.argv[2])
normal, slow = float(sys.argv[3]), float(sys.argv[4])
rounds = int(sys.argv[5]) if len(sys.argv) == 6 else 20000
per_worker, per_round, fraction = simulate(workers, probability, normal, slow, rounds)
print(f"{workers} trabajadores, {probability:.2%} lentos: promedio por trabajador "
f"{per_worker:.1f} ms; promedio por ronda {per_round:.1f} ms; rondas lentas "
f"{fraction:.1%} (exacto: {expected_slow_fraction(workers, probability):.1%})")
return 0
if __name__ == "__main__":
sys.exit(main())# Los procesos de FRRouting de esta red. Además de lo de siempre, bfdd.
zebra=yes
staticd=yes
bgpd=yes
bfdd=yes
vtysh_enable=yes
zebra_options="-A 127.0.0.1 -s 90000000"
staticd_options="-A 127.0.0.1"
bgpd_options="-A 127.0.0.1"
bfdd_options="-A 127.0.0.1"# Una red leaf-spine: dos spines, tres leaves, dos hosts por leaf y cuatro nodos de
# dos clientes (tenants) que usaremos con VXLAN.
#
# s1 AS 65140 s2 AS 65140
# / | \ / | \
# / | \________/___|___\____
# / | ___\_____/ | \ \
# l1 AS 65141 l2 AS 65142 l3 AS 65143
# / | | \ / | | \ | \
# h1 h2 ta1 tb1 h3 h4 ta2 tb2 h5 h6
#
# Todo enlace leaf-spine es un /31: spine = .0, leaf = .1.
# 10.214.1j.0/31 s1 - lj 10.214.2j.0/31 s2 - lj
# Cada leaf tiene una LAN 10.214.j.0/24 y un loopback 10.214.0.j/32. Los spines usan
# 10.214.0.101 y 10.214.0.102. l3 arranca sin BGP: lo configuran ustedes (lab/).
name: w14-fabric
mgmt:
network: redes-w14-fabric
ipv4-subnet: 172.90.14.0/26
ipv6-subnet: 3fff:172:90:14::/80
topology:
defaults:
kind: linux
image: redes-router
nodes:
# Se borra la ruta por defecto de gestión (eth0): lo que un nodo no sabe a dónde
# mandar debe fallar, no irse por Docker. IPv6 se apaga para no ensuciar capturas.
s1:
binds: [s1/frr.conf:/etc/frr/frr.conf:ro, daemons:/etc/frr/daemons:ro, ../captures:/captures]
exec: &router-exec
- ip route del default
- sysctl -w net.ipv6.conf.all.disable_ipv6=1
s2:
binds: [s2/frr.conf:/etc/frr/frr.conf:ro, daemons:/etc/frr/daemons:ro, ../captures:/captures]
exec: *router-exec
l1:
binds: [l1/frr.conf:/etc/frr/frr.conf:ro, daemons:/etc/frr/daemons:ro]
exec:
- ip route del default
- sysctl -w net.ipv6.conf.all.disable_ipv6=1
- ip link add br0 type bridge
- ip link set eth3 master br0
- ip link set eth4 master br0
- ip link set br0 up
l2:
binds: [l2/frr.conf:/etc/frr/frr.conf:ro, daemons:/etc/frr/daemons:ro]
exec:
- ip route del default
- sysctl -w net.ipv6.conf.all.disable_ipv6=1
- ip link add br0 type bridge
- ip link set eth3 master br0
- ip link set eth4 master br0
- ip link set br0 up
l3:
binds: [l3/frr.conf:/etc/frr/frr.conf:ro, daemons:/etc/frr/daemons:ro]
exec:
- ip route del default
- sysctl -w net.ipv6.conf.all.disable_ipv6=1
- ip link add br0 type bridge
- ip link set eth3 master br0
- ip link set eth4 master br0
- ip link set br0 up
h1:
binds: [../bin:/w14:ro, ../captures:/captures]
exec:
- ip route del default
- sysctl -w net.ipv6.conf.all.disable_ipv6=1
- ip addr add 10.214.1.10/24 dev eth1
- ip route add default via 10.214.1.1
h2:
binds: [../bin:/w14:ro, ../captures:/captures]
exec:
- ip route del default
- sysctl -w net.ipv6.conf.all.disable_ipv6=1
- ip addr add 10.214.1.20/24 dev eth1
- ip route add default via 10.214.1.1
h3:
binds: [../bin:/w14:ro, ../captures:/captures]
exec:
- ip route del default
- sysctl -w net.ipv6.conf.all.disable_ipv6=1
- ip addr add 10.214.2.10/24 dev eth1
- ip route add default via 10.214.2.1
h4:
binds: [../bin:/w14:ro, ../captures:/captures]
exec:
- ip route del default
- sysctl -w net.ipv6.conf.all.disable_ipv6=1
- ip addr add 10.214.2.20/24 dev eth1
- ip route add default via 10.214.2.1
h5:
binds: [../bin:/w14:ro, ../captures:/captures]
exec:
- ip route del default
- sysctl -w net.ipv6.conf.all.disable_ipv6=1
- ip addr add 10.214.3.10/24 dev eth1
- ip route add default via 10.214.3.1
h6:
binds: [../bin:/w14:ro, ../captures:/captures]
exec:
- ip route del default
- sysctl -w net.ipv6.conf.all.disable_ipv6=1
- ip addr add 10.214.3.20/24 dev eth1
- ip route add default via 10.214.3.1
# Máquinas de los dos clientes. Usan a propósito las mismas direcciones: cada
# cliente elige las suyas sin saber nada del otro.
ta1:
binds: [../bin:/w14:ro, ../captures:/captures]
exec: &tenant-exec-1
- ip route del default
- sysctl -w net.ipv6.conf.all.disable_ipv6=1
- ip addr add 10.99.0.1/24 dev eth1
tb1:
binds: [../bin:/w14:ro]
exec: *tenant-exec-1
ta2:
binds: [../bin:/w14:ro]
exec: &tenant-exec-2
- ip route del default
- sysctl -w net.ipv6.conf.all.disable_ipv6=1
- ip addr add 10.99.0.2/24 dev eth1
tb2:
binds: [../bin:/w14:ro]
exec: *tenant-exec-2
# Containerlab usa MTU 9500 por defecto; aquí queremos el valor de Ethernet.
links:
- {endpoints: ["s1:eth1", "l1:eth1"], mtu: 1500}
- {endpoints: ["s1:eth2", "l2:eth1"], mtu: 1500}
- {endpoints: ["s1:eth3", "l3:eth1"], mtu: 1500}
- {endpoints: ["s2:eth1", "l1:eth2"], mtu: 1500}
- {endpoints: ["s2:eth2", "l2:eth2"], mtu: 1500}
- {endpoints: ["s2:eth3", "l3:eth2"], mtu: 1500}
- {endpoints: ["h1:eth1", "l1:eth3"], mtu: 1500}
- {endpoints: ["h2:eth1", "l1:eth4"], mtu: 1500}
- {endpoints: ["h3:eth1", "l2:eth3"], mtu: 1500}
- {endpoints: ["h4:eth1", "l2:eth4"], mtu: 1500}
- {endpoints: ["h5:eth1", "l3:eth3"], mtu: 1500}
- {endpoints: ["h6:eth1", "l3:eth4"], mtu: 1500}
- {endpoints: ["ta1:eth1", "l1:eth5"], mtu: 1500}
- {endpoints: ["tb1:eth1", "l1:eth6"], mtu: 1500}
- {endpoints: ["ta2:eth1", "l2:eth5"], mtu: 1500}
- {endpoints: ["tb2:eth1", "l2:eth6"], mtu: 1500}frr defaults traditional
hostname l1
interface lo
ip address 10.214.0.1/32
interface eth1
ip address 10.214.11.1/31
interface eth2
ip address 10.214.21.1/31
interface br0
ip address 10.214.1.1/24
router bgp 65141
bgp router-id 10.214.0.1
! Todavía sin políticas: en esta red cada ruta vale.
no bgp ebgp-requires-policy
! Temporizadores cortos: keepalive 3 s, hold 9 s. El valor de fábrica es 60/180.
timers bgp 3 9
neighbor 10.214.11.0 remote-as 65140
neighbor 10.214.11.0 description s1
neighbor 10.214.11.0 advertisement-interval 0
neighbor 10.214.11.0 timers connect 3
neighbor 10.214.21.0 remote-as 65140
neighbor 10.214.21.0 description s2
neighbor 10.214.21.0 advertisement-interval 0
neighbor 10.214.21.0 timers connect 3
address-family ipv4 unicast
network 10.214.1.0/24
network 10.214.0.1/32
! Hasta 8 caminos de igual costo en la forwarding table.
maximum-paths 8
exit-address-familyfrr defaults traditional
hostname l2
interface lo
ip address 10.214.0.2/32
interface eth1
ip address 10.214.12.1/31
interface eth2
ip address 10.214.22.1/31
interface br0
ip address 10.214.2.1/24
router bgp 65142
bgp router-id 10.214.0.2
! Todavía sin políticas: en esta red cada ruta vale.
no bgp ebgp-requires-policy
! Temporizadores cortos: keepalive 3 s, hold 9 s. El valor de fábrica es 60/180.
timers bgp 3 9
neighbor 10.214.12.0 remote-as 65140
neighbor 10.214.12.0 description s1
neighbor 10.214.12.0 advertisement-interval 0
neighbor 10.214.12.0 timers connect 3
neighbor 10.214.22.0 remote-as 65140
neighbor 10.214.22.0 description s2
neighbor 10.214.22.0 advertisement-interval 0
neighbor 10.214.22.0 timers connect 3
address-family ipv4 unicast
network 10.214.2.0/24
network 10.214.0.2/32
! Hasta 8 caminos de igual costo en la forwarding table.
maximum-paths 8
exit-address-familyfrr defaults traditional
hostname l3
interface lo
ip address 10.214.0.3/32
interface eth1
ip address 10.214.13.1/31
interface eth2
ip address 10.214.23.1/31
interface br0
ip address 10.214.3.1/24frr defaults traditional
hostname s1
interface lo
ip address 10.214.0.101/32
interface eth1
ip address 10.214.11.0/31
interface eth2
ip address 10.214.12.0/31
interface eth3
ip address 10.214.13.0/31
router bgp 65140
bgp router-id 10.214.0.101
! Todavía sin políticas: en esta red cada ruta vale.
no bgp ebgp-requires-policy
! Temporizadores cortos: keepalive 3 s, hold 9 s. El valor de fábrica es 60/180.
timers bgp 3 9
neighbor 10.214.11.1 remote-as 65141
neighbor 10.214.11.1 description l1
neighbor 10.214.11.1 advertisement-interval 0
neighbor 10.214.11.1 timers connect 3
neighbor 10.214.12.1 remote-as 65142
neighbor 10.214.12.1 description l2
neighbor 10.214.12.1 advertisement-interval 0
neighbor 10.214.12.1 timers connect 3
neighbor 10.214.13.1 remote-as 65143
neighbor 10.214.13.1 description l3
neighbor 10.214.13.1 advertisement-interval 0
neighbor 10.214.13.1 timers connect 3
address-family ipv4 unicast
network 10.214.0.101/32
exit-address-familyfrr defaults traditional
hostname s2
interface lo
ip address 10.214.0.102/32
interface eth1
ip address 10.214.21.0/31
interface eth2
ip address 10.214.22.0/31
interface eth3
ip address 10.214.23.0/31
router bgp 65140
bgp router-id 10.214.0.102
! Todavía sin políticas: en esta red cada ruta vale.
no bgp ebgp-requires-policy
! Temporizadores cortos: keepalive 3 s, hold 9 s. El valor de fábrica es 60/180.
timers bgp 3 9
neighbor 10.214.21.1 remote-as 65141
neighbor 10.214.21.1 description l1
neighbor 10.214.21.1 advertisement-interval 0
neighbor 10.214.21.1 timers connect 3
neighbor 10.214.22.1 remote-as 65142
neighbor 10.214.22.1 description l2
neighbor 10.214.22.1 advertisement-interval 0
neighbor 10.214.22.1 timers connect 3
neighbor 10.214.23.1 remote-as 65143
neighbor 10.214.23.1 description l3
neighbor 10.214.23.1 advertisement-interval 0
neighbor 10.214.23.1 timers connect 3
address-family ipv4 unicast
network 10.214.0.102/32
exit-address-familyPruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
"""Pruebas de lo que dicen los apuntes sobre la red fabric/fabric.clab.yml."""
import os
import re
import statistics
import subprocess
import time
import unittest
import scenarios
import sdn_ctl
from netlab import HERE, answer_path, wait_until
from scenarios import CUT_MS, H3, LAB, S1_FROM_L1, S2_FROM_L1
ROUTE = "10.214.2.0/24"
def setUpModule():
unittest.addModuleCleanup(LAB.destroy)
LAB.destroy()
LAB.deploy()
if not wait_until(scenarios.ecmp_ready, seconds=90):
raise RuntimeError("la topología no quedó lista")
class Scenario(unittest.TestCase):
def setUp(self):
self.addCleanup(self.restore)
def restore(self):
scenarios.reset()
self.assertTrue(wait_until(scenarios.ecmp_ready, seconds=45), "no se volvió al estado inicial")
class A_Routes(Scenario):
def test_leaf_has_two_next_hops_toward_the_other_leaf(self):
route = LAB.out("l1", "ip", "route", "show", ROUTE)
self.assertIn(f"nexthop via {S1_FROM_L1} dev eth1", route)
self.assertIn(f"nexthop via {S2_FROM_L1} dev eth2", route)
def test_both_paths_have_the_same_length_and_the_shared_as_blocks_valleys(self):
text = LAB.out("l1", "vtysh", "-c", f"show bgp ipv4 unicast {ROUTE}")
self.assertEqual(text.count("65140 65142"), 2, text)
self.assertIn("multipath", text)
def test_without_maximum_paths_there_is_one_next_hop(self):
LAB.sh("l1", "vtysh", "-c", "configure terminal", "-c", "router bgp 65141", "-c",
"address-family ipv4 unicast", "-c", "maximum-paths 1", "-c", "end")
self.addCleanup(LAB.sh, "l1", "vtysh", "-c", "configure terminal", "-c", "router bgp 65141", "-c",
"address-family ipv4 unicast", "-c", "maximum-paths 8", "-c", "end")
self.assertTrue(wait_until(
lambda: "nexthop" not in LAB.out("l1", "ip", "route", "show", ROUTE), seconds=15))
def test_l3_starts_without_bgp(self):
self.assertNotIn("router bgp", LAB.out("l3", "vtysh", "-c", "show running-config"))
def test_a_lost_link_only_moves_traffic_to_that_leaf(self):
LAB.sh("s1", "ip", "link", "set", "eth2", "down")
self.assertTrue(wait_until(
lambda: "nexthop" not in LAB.out("l1", "ip", "route", "show", ROUTE), seconds=15))
self.assertIn(f"via {S2_FROM_L1}", LAB.out("l1", "ip", "route", "show", ROUTE))
# s1 no aprende la LAN de l2 a través de l1: el AS 65140 ya está en el AS_PATH.
self.assertTrue(wait_until(
lambda: "Network not in table" in LAB.out("s1", "vtysh", "-c", f"show bgp ipv4 unicast {ROUTE}"),
seconds=15))
class B_Hash(Scenario):
def distinct_hops(self, policy):
scenarios.set_policy(policy)
return {scenarios.next_hop(sport) for sport in range(40000, 40064)}
def test_policy_0_ignores_ports(self):
self.assertEqual(len(self.distinct_hops(0)), 1)
def test_policy_1_uses_ports(self):
self.assertEqual(self.distinct_hops(1), {S1_FROM_L1, S2_FROM_L1})
def test_policy_3_uses_ports(self):
self.assertEqual(self.distinct_hops(3), {S1_FROM_L1, S2_FROM_L1})
def test_the_sysctl_belongs_to_each_node(self):
scenarios.set_policy(1, nodes=("l1",))
self.assertIn("= 1", LAB.out("l1", "sysctl", "net.ipv4.fib_multipath_hash_policy"))
self.assertIn("= 0", LAB.out("l2", "sysctl", "net.ipv4.fib_multipath_hash_policy"))
def test_policy_0_sends_all_flows_through_one_spine(self):
scenarios.set_policy(0)
shares = scenarios.measure_flows(flows=16, seconds=2)
self.assertGreater(max(shares.values()), 0.95)
def test_policy_1_spreads_many_flows(self):
scenarios.set_policy(1)
shares = scenarios.measure_flows(flows=32, seconds=3)
# Con 32 flujos, que uno se lleve menos del 3 % es casi imposible; parejo no tiene que salir.
self.assertGreater(min(shares.values()), 0.03)
def test_colliding_elephants_get_half_of_what_separated_ones_get(self):
together, apart = scenarios.elephants()
self.assertLess(sum(together), 130)
self.assertGreater(sum(apart), 150)
self.assertGreater(sum(apart), 1.3 * sum(together))
def test_route_get_predicts_the_real_path_with_policy_3(self):
scenarios.set_policy(3)
scenarios.start_iperf_servers("h3", 5201)
for hop, spine, first in ((S1_FROM_L1, "s1", 44000), (S2_FROM_L1, "s2", 45000)):
cport = scenarios.port_on(hop, 5201, first)
before = scenarios.spine_tx()
scenarios.iperf_mbits("h1", H3, "--cport", str(cport), seconds=1)
after = scenarios.spine_tx()
delta = {name: after[name] - before[name] for name in after}
other = "s2" if spine == "s1" else "s1"
self.assertGreater(delta[spine], 20 * max(delta[other], 1000))
class C_PerPacket(Scenario):
def test_splitting_a_flow_reorders_it(self):
per_flow, per_packet = scenarios.perpacket()
# Por paquete hay más segmentos fuera de orden y más retransmisiones, y TCP rinde mucho menos.
self.assertGreater(per_packet[2], per_flow[2])
self.assertGreater(per_packet[1], 100 + 2 * per_flow[1])
self.assertLess(per_packet[0], 0.6 * per_flow[0])
class D_Failure(Scenario):
def check_cut(self, rows, low_ms, high_ms):
self.assertEqual(len(rows), 16)
hit = [row[3] for row in rows if row[3] >= CUT_MS]
calm = [row[3] for row in rows if row[3] < CUT_MS]
# Con 16 flujos, que todos o ninguno pasen por s1 tiene probabilidad 2 × 2^-16.
self.assertTrue(hit, "ningún flujo se cortó")
self.assertTrue(calm, "todos los flujos se cortaron")
for gap in hit:
self.assertGreaterEqual(gap, low_ms)
self.assertLessEqual(gap, high_ms)
return hit
def test_loss_of_carrier_is_almost_instantaneous(self):
rows = scenarios.carrier()
self.assertEqual(len(rows), 16)
self.assertLess(max(row[3] for row in rows), 2000)
def test_silent_failure_with_timers_3_9(self):
self.check_cut(scenarios.silent(), 4000, 14000)
def test_silent_failure_with_timers_1_3(self):
self.check_cut(scenarios.silent_short(), 1000, 6000)
def test_bfd_beats_the_hold_timer(self):
rows = scenarios.silent_bfd()
self.assertEqual(len(rows), 16)
self.assertLess(max(row[3] for row in rows), 2500)
class E_Overlay(Scenario):
def test_tenants_with_the_same_addresses_stay_apart(self):
scenarios.overlay()
for left, right in (("ta1", "ta2"), ("tb1", "tb2")):
self.assertIn(", 0% packet loss", LAB.out(left, "ping", "-c", "2", "-W", "2", "10.99.0.2"))
neighbor = LAB.out(left, "ip", "neigh", "show", "10.99.0.2")
self.assertIn(LAB.out(right, "cat", "/sys/class/net/eth1/address").strip(), neighbor)
def test_capture_shows_vni_and_inner_frame(self):
scenarios.overlay()
LAB.sh("ta1", "ping", "-c", "1", "-W", "2", "10.99.0.2")
paths = scenarios.capture_vxlan("test-vni", lambda: LAB.sh("ta1", "ping", "-c", "3", "-i", "0.2", "10.99.0.2"))
text = scenarios.read_capture(paths, "-Y", "icmp.type == 8", "-T", "fields", "-e", "ip.src", "-e", "ip.dst",
"-e", "udp.dstport", "-e", "vxlan.vni")
self.assertIn("10.214.0.1,10.99.0.1", text)
self.assertIn("10.214.0.2,10.99.0.2", text)
self.assertIn("4789", text)
self.assertIn("100", text)
def test_mtu_black_hole_and_its_two_fixes(self):
scenarios.overlay()
LAB.sh("ta1", "ping", "-c", "1", "-W", "2", "10.99.0.2")
self.assertTrue(scenarios.tenant_ping(1422))
self.assertFalse(scenarios.tenant_ping(1423))
self.assertLess(scenarios.tenant_tcp(), 1.0)
scenarios.tenant_mtu()
self.assertGreater(scenarios.tenant_tcp(), 5.0)
for tenant in scenarios.TENANTS:
LAB.sh(tenant, "ip", "link", "set", "eth1", "mtu", "1500")
scenarios.jumbo_underlay()
self.assertTrue(wait_until(lambda: scenarios.tenant_ping(1472), seconds=10))
self.assertGreater(scenarios.tenant_tcp(), 5.0)
def test_without_lowering_the_mtu_linux_warns_with_icmp(self):
learned, rate = scenarios.warn()
self.assertIn("mtu 1450", learned)
self.assertGreater(rate, 5.0)
def test_entropy_needs_the_outer_source_port(self):
wide, narrow = scenarios.entropy()
self.assertGreater(min(wide.values()), 0.03)
self.assertGreater(max(narrow.values()), 0.95)
def test_each_inner_flow_gets_its_own_outer_source_port(self):
scenarios.overlay()
LAB.sh("ta1", "ping", "-c", "1", "-W", "2", "10.99.0.2")
ports, _ = scenarios.inner_ports()
self.assertGreaterEqual(len(ports), 2)
class F_Sdn(Scenario):
POLICY = os.path.join(HERE, "policy.txt")
def delta_for(self, node, port):
before = scenarios.spine_tx()
rate, _ = scenarios.iperf_mbits(node, H3, port=port, seconds=1)
after = scenarios.spine_tx()
return rate, {name: after[name] - before[name] for name in after}
def test_rules_choose_the_path_by_port_and_can_drop(self):
scenarios.set_policy(1)
scenarios.start_iperf_servers("h3", 5201, 5202)
sdn_ctl.install(self.POLICY)
_, via_s1 = self.delta_for("h1", 5201)
_, via_s2 = self.delta_for("h1", 5202)
self.assertGreater(via_s1["s1"], 20 * max(via_s1["s2"], 1000))
self.assertGreater(via_s2["s2"], 20 * max(via_s2["s1"], 1000))
blocked = LAB.out("h2", "iperf3", "-c", H3, "-p", "5201", "-t", "1", "--connect-timeout", "1500")
self.assertIn("unable to connect", blocked.lower())
self.assertRegex(LAB.out("l1", "nft", "list", "table", "ip", "w14steer"), r"drop|counter packets [1-9]")
def test_a_pinned_flow_stays_broken_after_bgp_converges(self):
scenarios.set_policy(1)
scenarios.start_iperf_servers("h3", 5201, 5202)
sdn_ctl.install(self.POLICY)
scenarios.cut_silently("s1")
self.assertTrue(wait_until(
lambda: "nexthop" not in LAB.out("l1", "ip", "route", "show", ROUTE), seconds=20))
pinned = LAB.out("h1", "iperf3", "-c", H3, "-p", "5201", "-t", "1", "--connect-timeout", "1500")
self.assertIn("unable to connect", pinned.lower())
scenarios.unsteer()
scenarios.iperf_mbits("h1", H3, seconds=1)
self.assertGreater(scenarios.iperf_mbits("h1", H3, seconds=1)[0], 1)
class G_Collective(Scenario):
def test_one_slow_host_delays_everybody(self):
start, slow = scenarios.collective()
for node in ("h1", "h2", "h3", "h4"):
self.assertEqual(len(start[node]), 10)
self.assertEqual(len(slow[node]), 10)
# h3 no toca el enlace lento (h4 hacia h1), y aun así se atrasa con el grupo.
self.assertGreater(statistics.median(slow["h3"]), 3 * statistics.median(start["h3"]))
class H_Lossy(Scenario):
def test_random_loss_hurts_tcp_more_than_it_takes_from_the_link(self):
rates = scenarios.wireless()
self.assertGreater(rates["sin pérdida"], 60)
self.assertLess(rates["1 % de pérdida"], 0.5 * rates["sin pérdida"])
# El enlace que repara sus pérdidas (emulado con jitter) rinde mucho más que el que no.
self.assertGreater(rates["sin pérdida, con 4 ms de jitter"], 2 * rates["1 % de pérdida"])
class I_Lab(Scenario):
def setUp(self):
self.conf = answer_path("l3-bgp.conf")
super().setUp()
scenarios.load_l3(self.conf)
self.addCleanup(LAB.sh, "l3", "vtysh", "-c", "configure terminal", "-c", "no router bgp 65143")
self.assertTrue(wait_until(lambda: LAB.out("l3", "ip", "route", "show", ROUTE).count("nexthop") == 2,
seconds=40), LAB.out("l3", "vtysh", "-c", "show bgp summary"))
def test_l3_joins_the_fabric(self):
text = LAB.out("l1", "ip", "route", "show", "10.214.3.0/24")
self.assertEqual(text.count("nexthop"), 2, text)
self.assertIn(", 0% packet loss", LAB.out("h5", "ping", "-c", "2", "-W", "2", "10.214.1.10"))
self.assertIn(", 0% packet loss", LAB.out("h6", "ping", "-c", "2", "-W", "2", "10.214.2.20"))
self.assertIn("65140 65143", LAB.out("l1", "vtysh", "-c", "show bgp ipv4 unicast 10.214.3.0/24"))
def test_vxlan_between_hosts(self):
answer_path("vxlan.sh")
done = scenarios.load_vxlan()
self.assertEqual(done.returncode, 0, done.stderr)
self.addCleanup(LAB.sh, "h2", "ip", "link", "del", "vx42")
self.addCleanup(LAB.sh, "h6", "ip", "link", "del", "vx42")
self.assertIn("mtu 1450", LAB.out("h2", "ip", "link", "show", "vx42"))
self.assertTrue(wait_until(
lambda: ", 0% packet loss" in LAB.out("h2", "ping", "-c", "1", "-W", "1", "10.42.0.2"), seconds=15))
pcap = os.path.join(HERE, "captures", "test-lab.pcap")
LAB.daemon("h6", "tcpdump -i eth1 -n -s 200 -U -Z root -w /captures/test-lab.pcap udp port 4789")
time.sleep(1.5)
LAB.sh("h2", "ping", "-c", "3", "-i", "0.2", "10.42.0.2")
time.sleep(1)
LAB.sh("h6", "pkill", "tcpdump")
time.sleep(0.5)
text = subprocess.run(["tshark", "-r", pcap, "-Y", "icmp", "-T", "fields", "-e", "ip.src", "-e", "vxlan.vni"],
capture_output=True, text=True, timeout=60).stdout
self.assertIn("10.214.1.20,10.42.0.1", text)
self.assertIn("42", text)
class J_Student_View(unittest.TestCase):
def test_lab_files_are_templates(self):
# Solo para quien publica: ustedes les borran el TODO al completarlos.
if not os.path.isdir(os.path.join(HERE, "solution")):
self.skipTest("sin solution/, lab/ es de ustedes")
for name in ("l3-bgp.conf", "vxlan.sh"):
with open(os.path.join(HERE, "lab", name), encoding="utf-8") as file:
self.assertIn("TODO", file.read())
def test_policy_parser_orders_and_rejects(self):
rules = sdn_ctl.parse_policy(os.path.join(HERE, "policy.txt"))
self.assertEqual([rule[0] for rule in rules], [100, 90, 80])
self.assertEqual(rules[0][2], "drop")
table = sdn_ctl.compile_rules(rules)
self.assertIn("tcp dport 5201 counter meta mark set 1 accept", table)"""Pruebas de los simuladores: no necesitan red."""
import os
import re
import subprocess
import unittest
import tail_rounds
from netlab import HERE
def tool(*args):
done = subprocess.run([os.path.join(HERE, "bin", args[0]), *args[1:]], capture_output=True,
text=True, timeout=120)
return done.returncode, done.stdout + done.stderr
class EcmpSim(unittest.TestCase):
def elephant(self, paths, flows):
_, out = tool("ecmp_sim", "elephant", str(paths), str(flows), "200000")
found = re.search(r"sin choques en ([\d.]+) %.*promedio ([\d.]+); throughput total (\d+) %", out)
return float(found.group(1)), float(found.group(2)), int(found.group(3))
def test_two_elephants_on_two_paths_match_the_closed_form(self):
# P(sin choque) = 1/2; caminos ocupados en promedio = 1,5; o sea 75 % del ideal.
clean, used, share = self.elephant(2, 2)
self.assertAlmostEqual(clean, 50.0, delta=1.0)
self.assertAlmostEqual(used, 1.5, delta=0.02)
self.assertEqual(share, 75)
def test_four_elephants_on_four_paths_match_the_closed_form(self):
# P(sin choque) = 4!/4^4 = 9,375 %; ocupados = 4 × (1 - (3/4)^4) ≈ 2,73.
clean, used, _ = self.elephant(4, 4)
self.assertAlmostEqual(clean, 9.375, delta=0.5)
self.assertAlmostEqual(used, 2.734, delta=0.03)
def test_more_flows_balance_better(self):
ratio = {}
for flows in (8, 64, 6400):
_, out = tool("ecmp_sim", "balance", "2", str(flows), "5000")
ratio[flows] = float(re.search(r"lleva ([\d.]+) veces", out).group(1))
self.assertGreater(ratio[8], ratio[64])
self.assertGreater(ratio[64], ratio[6400])
self.assertLess(ratio[6400], 1.05)
def test_rehash_moves_less_with_better_schemes(self):
_, out = tool("ecmp_sim", "rehash", "8", "1000000")
dead = float(re.search(r"en él: ([\d.]+) %", out).group(1))
modulo = float(re.search(r"módulo: +se mueven ([\d.]+)", out).group(1))
threshold = float(re.search(r"hash-threshold: +se mueven ([\d.]+)", out).group(1))
rendezvous = float(re.search(r"rendezvous: +se mueven ([\d.]+)", out).group(1))
self.assertAlmostEqual(dead, 12.5, delta=0.5)
self.assertGreater(modulo, threshold)
self.assertGreater(threshold, rendezvous)
self.assertAlmostEqual(rendezvous, dead, delta=0.5)
class DcfSim(unittest.TestCase):
def useful(self, *args):
_, out = tool("dcf_sim", *args)
found = re.search(r"tiempo útil\s+([\d.]+) %; intentos que chocan\s+([\d.]+) %", out)
return float(found.group(1)), float(found.group(2))
def test_lone_station_never_collides(self):
useful, collisions = self.useful("shared", "1", "1000000")
self.assertEqual(collisions, 0.0)
# Un backoff promedio de 7,5 ranuras por cada trama de 150: 150 / 157,5.
self.assertAlmostEqual(useful, 95.2, delta=0.5)
def test_more_stations_collide_more(self):
few = self.useful("shared", "2", "5000000")
many = self.useful("shared", "50", "5000000")
self.assertGreater(many[1], few[1])
self.assertLess(many[0], few[0])
def test_rts_rescues_hidden_stations(self):
without = self.useful("hidden", "5000000")
with_rts = self.useful("hidden_rts", "5000000")
self.assertGreater(with_rts[0], without[0] + 20)
self.assertLess(with_rts[1], without[1])
class TailRounds(unittest.TestCase):
def test_simulation_matches_the_exact_probability(self):
_, _, fraction = tail_rounds.simulate(10, 0.01, 1, 100, 50000)
self.assertAlmostEqual(fraction, tail_rounds.expected_slow_fraction(10, 0.01), delta=0.01)
def test_the_round_is_worse_than_the_average_worker(self):
per_worker, per_round, _ = tail_rounds.simulate(1000, 0.01, 1, 100, 2000)
self.assertLess(per_worker, 3)
self.assertGreater(per_round, 95)Laboratorio
Archivos de partida del laboratorio de la semana.
! Laboratorio de la semana 14, paso 1: BGP en el leaf l3.
!
! l3 ya tiene direcciones en sus interfaces, pero no habla BGP. Mientras no lo configuren, h5 y h6
! no llegan a nadie. Se carga con: python3 scenarios.py l3
! Cada línea es un comando de vtysh, ya dentro de "configure terminal". Las que empiezan con ! se
! ignoran. Si se equivocan, corran "show running-config" en vtysh para ver qué quedó.
!
! Lo que deben lograr:
! - l3 es el AS 65143, con router-id 10.214.0.3.
! - Tiene una sesión con cada spine (mismo AS 65140). Las direcciones de los spines están en
! fabric/fabric.clab.yml; l3 usa eth1 hacia s1 y eth2 hacia s2.
! - Temporizadores 3/9, como el resto de la red, y advertisement-interval 0 en cada sesión.
! - Anuncia su LAN (10.214.3.0/24) y su loopback (10.214.0.3/32).
! - Instala hasta 8 caminos de igual costo.
! - Como en toda esta red, todavía sin políticas: ebgp-requires-policy se apaga.
!
! TODO: escriban aquí la configuración de BGP de l3 y borren esta línea.#!/bin/sh
# Laboratorio de la semana 14, paso 6: un túnel VXLAN entre h2 y h6. Se corre en el contenedor del
# curso, y requiere que l3 ya hable BGP. Lo carga: python3 scenarios.py vxlan
#
# Lo que deben lograr:
# - En h2 y en h6, una interfaz vx42 de tipo vxlan con VNI 42, puerto 4789 y como otro extremo la
# dirección real del otro host. Usen "dev eth1", para que el kernel calcule la MTU de vx42.
# - h2 tiene 10.42.0.1/24 en vx42, y h6 tiene 10.42.0.2/24.
#
# Ejemplo de cómo correr un comando en h2: docker exec "${P}h2" ip link show eth1
# P lo pone scenarios.py; si lo corren a mano, usen P=clab-w14-fabric- sh lab/vxlan.sh
P="${P:-clab-w14-fabric-}"
# TODO: creen las dos interfaces, denles sus direcciones, súbanlas y borren esta línea.Compilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 14: data centers y redes modernas. Se usa dentro del contenedor del curso.
#
# make compila los programas en bin/ (estáticos, para correr en los nodos)
# make test compila, levanta la topología, corre las pruebas (test_*.py) y siempre
# la destruye. Las pruebas del laboratorio se saltan hasta que completen
# lab/l3-bgp.conf y lab/vxlan.sh
# make up levanta la red leaf-spine (fabric/fabric.clab.yml)
# make down destruye la topología y su red de Docker
# make clean lo anterior, y borra los binarios y las capturas
CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -D_DEFAULT_SOURCE -O2 -g -static
PROGRAMS = bin/flow_probe bin/ring_reduce bin/ecmp_sim bin/dcf_sim bin/df_ping
.PHONY: all test up down clean
all: $(PROGRAMS) | captures
captures:
mkdir -p captures
bin:
mkdir -p bin
bin/%: %.c | bin
$(CC) $(CFLAGS) -o $@ $<
test: all
@status=0; PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -p 'test_*.py' -v || status=$$?; \
$(MAKE) --no-print-directory down; exit $$status
up: all
cd fabric && containerlab deploy -t fabric.clab.yml --reconfigure
down:
@cd fabric && containerlab destroy -t fabric.clab.yml --cleanup >/dev/null 2>&1 || true
@docker network rm redes-w14-fabric >/dev/null 2>&1 || true
clean: down
rm -rf bin captures __pycache__ fabric/clab-w14-fabric fabric/.fabric.clab.yml.bak