Semana 5 — Routing dentro de una red
Objetivos
- Plantear el routing como un problema sobre grafos.
- Ejecutar a mano los algoritmos de estado de enlace y de vector de distancias.
- Explicar problemas de convergencia, como el conteo al infinito.
- Configurar OSPF con FRRouting y observar cómo reacciona ante una falla.
Sesión A — Routing: estado de enlace y vector de distancias
Lecturas:
- Kurose y Ross, secs. 5.1 (Introduction) y 5.2 (Routing Algorithms).
- CS168, Model for Intra-Domain Routing, Distance Vector y Link State.
Temas:
- El grafo, los costos y qué significa una ruta válida: sin ciclos ni callejones sin salida.
- Plano de control centralizado o distribuido.
- Estado de enlace: inundar la topología y correr Dijkstra. Ejemplo resuelto en un grafo de seis nodos.
- Vector de distancias: Bellman-Ford distribuido. Ejemplo resuelto iteración por iteración.
- Las buenas noticias viajan rápido y las malas, lento: conteo al infinito. Split horizon y poison reverse, y sus límites.
- Comparación: mensajes, tiempo de convergencia y qué pasa si un router se equivoca.
- Error común: confundir la tabla de routing con la de forwarding. FRRouting calcula la primera e instala lo elegido en la segunda, la del kernel.
Sesión B — OSPF y routing jerárquico
Lecturas:
- Kurose y Ross, sec. 5.3 (Intra-AS Routing in the Internet: OSPF).
- CS168, Model for Inter-Domain Routing.
- FRRouting, OSPFv2.
Lecturas complementarias:
- RFC 2328, OSPF versión 2.
Temas:
- Por qué un solo algoritmo no alcanza: escala y autonomía administrativa. Sistemas autónomos.
- OSPF: Hello, adyacencias, LSAs y base de datos de estado de enlace. Costos configurables.
- Áreas y backbone: cómo reducen la inundación.
- Configurar FRRouting:
frr.conf,vtysh,show ip ospf neighboryshow ip route. - Tiempos de convergencia: temporizadores Hello y Dead, y por qué detectar la falla suele ser lo más lento.
- Error común: dos routers que no forman adyacencia. Causas: área distinta, temporizadores distintos, máscara diferente (solo en redes broadcast; en enlaces punto a punto no se compara) o MTU distinta, que deja a los vecinos en
ExStart.
Laboratorio — OSPF con FRRouting: una falla de enlace en vivo
Trabajen en code/week-05/, con una topología de cinco routers que tiene caminos alternativos.
- Levantar la topología y configurar OSPF en dos routers. Seguir con
vtyshlos estados de la adyacencia hasta llegar aFull. - Terminar la configuración. Calcular a mano con Dijkstra la tabla de un router y compararla con
show ip route ospf. - Cambiar un costo para mover el tráfico por otro camino, y confirmarlo con
traceroute. - Dejar corriendo un ping y apagar un enlace con
ip link set eth1 down. Medir cuántos paquetes se pierden: OSPF se entera al instante porque la interfaz cae. Luego repetir con un corte silencioso, contc qdisc add dev ethN root netem loss 100%en los dos extremos, y comparar: ahora solo los temporizadores delatan la falla. - Repetir con temporizadores más cortos y comparar. ¿Qué se gana y qué se arriesga?
- Provocar una adyacencia que no sube (por ejemplo, con áreas distintas) y diagnosticarla con
show ip ospf interface. - Avanzar con P1.
Preguntas de repaso
- Corran Dijkstra desde un nodo en un grafo dado y escriban su tabla de forwarding.
- Muestren con tres nodos en línea cómo aparece el conteo al infinito. ¿Por qué poison reverse no resuelve todos los casos?
- Comparen estado de enlace y vector de distancias en información que guarda cada router y en lo que pasa si uno anuncia un costo falso.
- Un enlace falla, y tres routers ya actualizaron su tabla y dos todavía no. ¿Qué pueden sufrir los paquetes en ese momento?
- ¿Por qué OSPF usa áreas? ¿Qué información cruza de un área a otra?
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (28)Pruebas (4)Laboratorio (4)Compilación (1)
Ejemplos
Los programas que usan los apuntes y la clase.
/* dijkstra.c: el algoritmo de estado de enlace, paso a paso.
*
* Lee un grafo (líneas "nodo nodo costo") y corre Dijkstra desde un nodo. Imprime
* la tabla de cada iteración y la forwarding table que resulta. Cuando dos caminos
* empatan guarda los dos: un destino puede tener varios next hop (ECMP).
*
* ./dijkstra graphs/six.txt a
*/
#include <inttypes.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_NODES 32
#define NAME_SIZE 8
#define INFINITE_COST INT32_MAX
static char names[MAX_NODES][NAME_SIZE];
static int node_count;
static int32_t cost[MAX_NODES][MAX_NODES]; /* 0 = sin enlace */
static int find_or_add(const char *name) {
for (int i = 0; i < node_count; i++) {
if (strcmp(names[i], name) == 0) {
return i;
}
}
if (node_count == MAX_NODES) {
fprintf(stderr, "demasiados nodos (máximo %d)\n", MAX_NODES);
exit(EXIT_FAILURE);
}
snprintf(names[node_count], NAME_SIZE, "%s", name);
return node_count++;
}
static int compare_names(const void *left, const void *right) {
return strcmp(left, right);
}
/* Primera pasada: nombres, en orden alfabético. Segunda pasada: costos. */
static int load_graph(const char *path) {
int links = 0;
for (int pass = 0; pass < 2; pass++) {
FILE *file = fopen(path, "r");
if (file == NULL) {
perror(path);
exit(EXIT_FAILURE);
}
char line[128];
while (fgets(line, sizeof line, file) != NULL) {
char left[NAME_SIZE];
char right[NAME_SIZE];
int value;
if (line[0] == '#' || sscanf(line, "%7s %7s %d", left, right, &value) != 3) {
continue;
}
if (value <= 0) {
fprintf(stderr, "costo inválido: %s", line);
exit(EXIT_FAILURE);
}
int u = find_or_add(left);
int v = find_or_add(right);
if (pass == 1) {
cost[u][v] = value;
cost[v][u] = value;
links++;
}
}
if (fclose(file) != 0) {
perror("fclose");
exit(EXIT_FAILURE);
}
if (pass == 0) {
qsort(names, (size_t)node_count, NAME_SIZE, compare_names);
}
}
return links;
}
static void print_header(int source) {
printf("%-4s %-*s", "paso", node_count + 1, "N'");
for (int v = 0; v < node_count; v++) {
if (v != source) {
printf(" %-8s", names[v]);
}
}
printf("\n");
}
/* Una celda como "6,cd": costo y predecesores en el camino más corto. */
static void print_row(int step, int source, const int *order, int done_count,
const int32_t *dist, const uint32_t *previous, const bool *done) {
printf("%-4d ", step);
int written = 0;
for (int i = 0; i < done_count; i++) {
written += printf("%s", names[order[i]]);
}
printf("%*s", node_count + 1 > written ? node_count + 1 - written : 0, "");
for (int v = 0; v < node_count; v++) {
if (v == source) {
continue;
}
char cell[64] = "-";
if (done[v]) {
snprintf(cell, sizeof cell, "*");
} else if (dist[v] != INFINITE_COST) {
size_t used = (size_t)snprintf(cell, sizeof cell, "%" PRId32 ",", dist[v]);
for (int u = 0; u < node_count; u++) {
if (previous[v] & (UINT32_C(1) << u)) {
used += (size_t)snprintf(cell + used, sizeof cell - used, "%s", names[u]);
}
}
}
printf(" %-8s", cell);
}
printf("\n");
}
static void dijkstra(int source) {
int32_t dist[MAX_NODES];
uint32_t previous[MAX_NODES] = {0}; /* predecesores, como conjunto de bits */
uint32_t first_hops[MAX_NODES] = {0}; /* next hop desde el origen, también */
bool done[MAX_NODES] = {false};
int order[MAX_NODES];
int done_count = 0;
for (int v = 0; v < node_count; v++) {
dist[v] = INFINITE_COST;
}
dist[source] = 0;
done[source] = true;
order[done_count++] = source;
for (int v = 0; v < node_count; v++) {
if (cost[source][v] > 0) {
dist[v] = cost[source][v];
previous[v] = UINT32_C(1) << source;
first_hops[v] = UINT32_C(1) << v;
}
}
print_header(source);
print_row(0, source, order, done_count, dist, previous, done);
for (int step = 1; step < node_count; step++) {
/* El más cercano que falta. Con empate, el primero en orden alfabético. */
int nearest = -1;
for (int v = 0; v < node_count; v++) {
if (!done[v] && dist[v] != INFINITE_COST && (nearest < 0 || dist[v] < dist[nearest])) {
nearest = v;
}
}
if (nearest < 0) {
break; /* lo que queda no se alcanza */
}
done[nearest] = true;
order[done_count++] = nearest;
for (int v = 0; v < node_count; v++) {
if (done[v] || cost[nearest][v] == 0) {
continue;
}
int32_t through = dist[nearest] + cost[nearest][v];
if (through < dist[v]) {
dist[v] = through;
previous[v] = UINT32_C(1) << nearest;
first_hops[v] = first_hops[nearest];
} else if (through == dist[v]) {
/* Empate: este camino se suma a los que ya había. */
previous[v] |= UINT32_C(1) << nearest;
first_hops[v] |= first_hops[nearest];
}
}
print_row(step, source, order, done_count, dist, previous, done);
}
printf("\nforwarding table de %s\n", names[source]);
printf("%-8s %-6s %s\n", "destino", "costo", "next hop");
for (int v = 0; v < node_count; v++) {
if (v == source) {
continue;
}
if (dist[v] == INFINITE_COST) {
printf("%-8s %-6s -\n", names[v], "inf");
continue;
}
printf("%-8s %-6" PRId32, names[v], dist[v]);
for (int u = 0; u < node_count; u++) {
if (first_hops[v] & (UINT32_C(1) << u)) {
printf(" %s", names[u]);
}
}
printf("\n");
}
}
int main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(stderr, "uso: %s GRAFO ORIGEN\n", argv[0]);
return EXIT_FAILURE;
}
int links = load_graph(argv[1]);
int source = -1;
for (int v = 0; v < node_count; v++) {
if (strcmp(names[v], argv[2]) == 0) {
source = v;
}
}
if (source < 0) {
fprintf(stderr, "no existe el nodo %s\n", argv[2]);
return EXIT_FAILURE;
}
printf("%d nodos, %d enlaces, origen %s\n\n", node_count, links, argv[2]);
dijkstra(source);
return EXIT_SUCCESS;
}/* dv_sim.c: vector de distancias, ronda por ronda.
*
* Simula routers que solo conocen sus enlaces y lo que les cuentan sus vecinos.
* Cada router guarda, por destino, un costo y un next hop, y aplica dos reglas:
* 1. Lo que diga su next hop actual se acepta siempre, aunque sea peor.
* 2. Lo que diga otro vecino se acepta solo si mejora el costo.
*
* ./dv_sim GRAFO [--mode plain|split|poison] [--dest NODO] [--inf N] [--max N]
* [--fail U V | --set U V COSTO | --lie NODO DESTINO COSTO]
* [--order U:V,U:V,...]
*
* Primero converge y después aplica el evento. Sin --order, en cada ronda todos
* anuncian a la vez. Con --order, cada paso es un solo anuncio U -> V, en ese orden,
* que se repite: sirve para fijar cuál mensaje llega antes.
*/
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_NODES 16
#define NAME_SIZE 8
#define NO_HOP (-1)
#define MAX_ORDER 64
#define EXPIRY_ROUNDS 6 /* sin noticias del next hop por tantas rondas, la ruta vence */
enum mode { MODE_PLAIN, MODE_SPLIT, MODE_POISON };
struct message {
int from;
int to;
};
static char names[MAX_NODES][NAME_SIZE];
static int node_count;
static int link_cost[MAX_NODES][MAX_NODES]; /* 0 = sin enlace */
static int dist[MAX_NODES][MAX_NODES];
static int hop[MAX_NODES][MAX_NODES];
static int age[MAX_NODES][MAX_NODES];
static int infinity = 16;
static enum mode mode = MODE_PLAIN;
static long announcements;
static bool stopped_by_limit; /* run terminó por --max, no porque nada cambiara */
/* Un router que miente: anuncia este costo sin importar lo que sepa. */
static int liar = NO_HOP;
static int lie_dest = NO_HOP;
static int lie_cost;
static int find_or_add(const char *name) {
for (int i = 0; i < node_count; i++) {
if (strcmp(names[i], name) == 0) {
return i;
}
}
if (node_count == MAX_NODES) {
fprintf(stderr, "demasiados nodos (máximo %d)\n", MAX_NODES);
exit(EXIT_FAILURE);
}
snprintf(names[node_count], NAME_SIZE, "%s", name);
return node_count++;
}
static int require_node(const char *name) {
for (int i = 0; i < node_count; i++) {
if (strcmp(names[i], name) == 0) {
return i;
}
}
fprintf(stderr, "no existe el nodo %s\n", name);
exit(EXIT_FAILURE);
}
static int compare_names(const void *left, const void *right) {
return strcmp(left, right);
}
static void load_graph(const char *path) {
for (int pass = 0; pass < 2; pass++) {
FILE *file = fopen(path, "r");
if (file == NULL) {
perror(path);
exit(EXIT_FAILURE);
}
char line[128];
while (fgets(line, sizeof line, file) != NULL) {
char left[NAME_SIZE];
char right[NAME_SIZE];
int value;
if (line[0] == '#' || sscanf(line, "%7s %7s %d", left, right, &value) != 3) {
continue;
}
if (value <= 0) {
fprintf(stderr, "costo inválido: %s", line);
exit(EXIT_FAILURE);
}
int u = find_or_add(left);
int v = find_or_add(right);
if (pass == 1) {
link_cost[u][v] = value;
link_cost[v][u] = value;
}
}
if (fclose(file) != 0) {
perror("fclose");
exit(EXIT_FAILURE);
}
if (pass == 0) {
qsort(names, (size_t)node_count, NAME_SIZE, compare_names);
}
}
}
/* Al inicio, cada router conoce solo sus enlaces directos. */
static void init_tables(void) {
for (int u = 0; u < node_count; u++) {
for (int d = 0; d < node_count; d++) {
bool direct = link_cost[u][d] > 0 && link_cost[u][d] < infinity;
dist[u][d] = u == d ? 0 : (direct ? link_cost[u][d] : infinity);
hop[u][d] = u == d ? u : (direct ? d : NO_HOP);
}
}
}
/* Qué le anuncia from a to sobre un destino. Devuelve false si no anuncia nada. */
static bool advertised(int from, int to, int d, int *value) {
if (from == liar && d == lie_dest) {
*value = lie_cost;
return true;
}
if (hop[from][d] == to && d != from) {
if (mode == MODE_SPLIT) {
return false; /* split horizon: no se lo cuento a quien me lo enseñó */
}
if (mode == MODE_POISON) {
*value = infinity; /* poison reverse: se lo cuento como infinito */
return true;
}
}
*value = dist[from][d];
return true;
}
/* Aplica el anuncio de un vecino sobre un destino. Devuelve si cambió la tabla. */
static bool apply(int router, int neighbor, int d, int value) {
if (d == router || (router == liar && d == lie_dest)) {
return false;
}
int through = value + link_cost[router][neighbor];
if (through > infinity) {
through = infinity;
}
if (hop[router][d] == neighbor) {
age[router][d] = 0;
if (through != dist[router][d]) {
dist[router][d] = through;
if (through == infinity) {
hop[router][d] = NO_HOP;
}
return true;
}
} else if (through < dist[router][d]) {
dist[router][d] = through;
hop[router][d] = neighbor;
age[router][d] = 0;
return true;
}
return false;
}
/* Las rutas de quien calla envejecen. Solo pasa con split horizon. */
static bool expire_routes(void) {
bool changed = false;
for (int u = 0; u < node_count; u++) {
for (int d = 0; d < node_count; d++) {
if (u == d || hop[u][d] == NO_HOP || (u == liar && d == lie_dest)) {
continue;
}
if (++age[u][d] > EXPIRY_ROUNDS) {
dist[u][d] = infinity;
hop[u][d] = NO_HOP;
age[u][d] = 0;
changed = true;
}
}
}
return changed;
}
static bool any_aging(void) {
for (int u = 0; u < node_count; u++) {
for (int d = 0; d < node_count; d++) {
if (age[u][d] > 0) {
return true;
}
}
}
return false;
}
/* Una ronda: todos arman sus anuncios con la tabla de antes y luego todos los
* aplican. El next hop actual se atiende primero, para que el resultado no dependa
* del orden de los vecinos. */
static bool round_all_at_once(void) {
static int inbox[MAX_NODES][MAX_NODES][MAX_NODES];
static bool present[MAX_NODES][MAX_NODES][MAX_NODES];
for (int from = 0; from < node_count; from++) {
for (int to = 0; to < node_count; to++) {
if (link_cost[from][to] == 0) {
continue;
}
announcements++;
for (int d = 0; d < node_count; d++) {
present[from][to][d] = advertised(from, to, d, &inbox[from][to][d]);
}
}
}
bool changed = expire_routes();
for (int to = 0; to < node_count; to++) {
for (int d = 0; d < node_count; d++) {
int current = hop[to][d];
if (current != NO_HOP && current != to && link_cost[current][to] > 0
&& present[current][to][d]) {
changed |= apply(to, current, d, inbox[current][to][d]);
}
}
for (int from = 0; from < node_count; from++) {
if (link_cost[from][to] == 0) {
continue;
}
for (int d = 0; d < node_count; d++) {
if (present[from][to][d] && hop[to][d] != from) {
changed |= apply(to, from, d, inbox[from][to][d]);
}
}
}
}
return changed;
}
/* Un solo anuncio, que el receptor aplica de inmediato. */
static bool single_message(struct message message) {
announcements++;
bool changed = false;
for (int d = 0; d < node_count; d++) {
int value;
if (advertised(message.from, message.to, d, &value)) {
changed |= apply(message.to, message.from, d, value);
}
}
return changed;
}
static void format_entry(char *cell, size_t size, int router, int d) {
if (dist[router][d] >= infinity) {
snprintf(cell, size, "inf");
} else if (router == d) {
snprintf(cell, size, "0");
} else if (hop[router][d] == NO_HOP) {
snprintf(cell, size, "%d", dist[router][d]);
} else {
snprintf(cell, size, "%d/%s", dist[router][d], names[hop[router][d]]);
}
}
static void print_matrix(void) {
printf(" ");
for (int d = 0; d < node_count; d++) {
printf(" %-6s", names[d]);
}
printf("\n");
for (int u = 0; u < node_count; u++) {
printf("%-4s", names[u]);
for (int d = 0; d < node_count; d++) {
char cell[32];
format_entry(cell, sizeof cell, u, d);
printf(" %-6s", cell);
}
printf("\n");
}
}
/* Busca un ciclo de forwarding hacia d. Lo imprime si lo hay. */
static void print_loop(int d) {
for (int start = 0; start < node_count; start++) {
if (start == d) {
continue;
}
int visits[MAX_NODES] = {0};
int current = start;
while (current != d && current != NO_HOP && visits[current] == 0) {
visits[current] = 1;
current = hop[current][d];
}
if (current == d || current == NO_HOP) {
continue;
}
/* current está en el ciclo; se imprime una vez, desde su nodo menor */
int lowest = current;
for (int walk = hop[current][d]; walk != current; walk = hop[walk][d]) {
if (walk < lowest) {
lowest = walk;
}
}
printf(" ciclo: %s", names[lowest]);
for (int walk = hop[lowest][d]; walk != lowest; walk = hop[walk][d]) {
printf(" > %s", names[walk]);
}
printf(" > %s", names[lowest]);
return;
}
}
static void print_line(const char *label, int d) {
printf("%-14s", label);
for (int u = 0; u < node_count; u++) {
if (u == d) {
continue;
}
char cell[32];
format_entry(cell, sizeof cell, u, d);
printf(" %s=%-6s", names[u], cell);
}
print_loop(d);
printf("\n");
}
/* Corre hasta que nada cambie. Devuelve cuántas rondas o pasos hicieron cambios. */
static int run(int dest, bool show, const struct message *order, int order_size, int max_rounds) {
int last_change = 0;
char label[64];
if (order_size > 0) {
int unchanged_steps = 0;
for (int step = 1; step <= max_rounds; step++) {
struct message message = order[(step - 1) % order_size];
bool changed = single_message(message);
if (changed) {
last_change = step;
unchanged_steps = 0;
} else {
unchanged_steps++;
}
if (show && dest != NO_HOP) {
snprintf(label, sizeof label, "paso %d %s>%s", step, names[message.from],
names[message.to]);
print_line(label, dest);
}
if (unchanged_steps >= order_size) {
return last_change;
}
}
stopped_by_limit = true;
return last_change;
}
for (int round = 1; round <= max_rounds; round++) {
bool changed = round_all_at_once();
if (changed) {
last_change = round;
}
if (show && changed) {
if (dest != NO_HOP) {
snprintf(label, sizeof label, "ronda %d", round);
print_line(label, dest);
} else {
printf("ronda %d\n", round);
print_matrix();
printf("\n");
}
}
if (!changed && !any_aging()) {
return last_change;
}
}
stopped_by_limit = true;
return last_change;
}
static void fail_link(int u, int v) {
link_cost[u][v] = 0;
link_cost[v][u] = 0;
for (int d = 0; d < node_count; d++) {
if (hop[u][d] == v) {
dist[u][d] = infinity;
hop[u][d] = NO_HOP;
}
if (hop[v][d] == u) {
dist[v][d] = infinity;
hop[v][d] = NO_HOP;
}
}
}
/* El costo cambia en los dos extremos. Las rutas por ese enlace suben o bajan igual. */
static void set_link(int u, int v, int value) {
int delta = value - link_cost[u][v];
link_cost[u][v] = value;
link_cost[v][u] = value;
for (int d = 0; d < node_count; d++) {
if (hop[u][d] == v) {
dist[u][d] += delta;
}
if (hop[v][d] == u) {
dist[v][d] += delta;
}
}
}
static int parse_order(char *text, struct message *order) {
int size = 0;
for (char *pair = strtok(text, ","); pair != NULL; pair = strtok(NULL, ",")) {
char *colon = strchr(pair, ':');
if (colon == NULL || size == MAX_ORDER) {
fprintf(stderr, "--order mal formado: %s\n", pair);
exit(EXIT_FAILURE);
}
*colon = '\0';
order[size].from = require_node(pair);
order[size].to = require_node(colon + 1);
size++;
}
return size;
}
static void usage(const char *program) {
fprintf(stderr,
"uso: %s GRAFO [--mode plain|split|poison] [--dest NODO] [--inf N] [--max N]\n"
" [--fail U V | --set U V COSTO | --lie NODO DESTINO COSTO]\n"
" [--order U:V,U:V,...]\n",
program);
exit(EXIT_FAILURE);
}
int main(int argc, char *argv[]) {
if (argc < 2) {
usage(argv[0]);
}
load_graph(argv[1]);
int dest = NO_HOP;
int max_rounds = 200;
const char *fail_args[2] = {NULL, NULL};
const char *set_args[3] = {NULL, NULL, NULL};
const char *lie_args[3] = {NULL, NULL, NULL};
char *order_text = NULL;
for (int i = 2; i < argc; i++) {
const char *flag = argv[i];
int remaining = argc - i - 1;
if (strcmp(flag, "--mode") == 0 && remaining >= 1) {
const char *value = argv[++i];
if (strcmp(value, "plain") == 0) {
mode = MODE_PLAIN;
} else if (strcmp(value, "split") == 0) {
mode = MODE_SPLIT;
} else if (strcmp(value, "poison") == 0) {
mode = MODE_POISON;
} else {
usage(argv[0]);
}
} else if (strcmp(flag, "--dest") == 0 && remaining >= 1) {
dest = require_node(argv[++i]);
} else if (strcmp(flag, "--inf") == 0 && remaining >= 1) {
infinity = atoi(argv[++i]);
} else if (strcmp(flag, "--max") == 0 && remaining >= 1) {
max_rounds = atoi(argv[++i]);
} else if (strcmp(flag, "--fail") == 0 && remaining >= 2) {
fail_args[0] = argv[++i];
fail_args[1] = argv[++i];
} else if (strcmp(flag, "--set") == 0 && remaining >= 3) {
set_args[0] = argv[++i];
set_args[1] = argv[++i];
set_args[2] = argv[++i];
} else if (strcmp(flag, "--lie") == 0 && remaining >= 3) {
lie_args[0] = argv[++i];
lie_args[1] = argv[++i];
lie_args[2] = argv[++i];
} else if (strcmp(flag, "--order") == 0 && remaining >= 1) {
order_text = argv[++i];
} else {
usage(argv[0]);
}
}
if (infinity <= 0 || max_rounds <= 0) {
usage(argv[0]);
}
struct message order[MAX_ORDER];
int order_size = order_text != NULL ? parse_order(order_text, order) : 0;
bool has_event = fail_args[0] != NULL || set_args[0] != NULL || lie_args[0] != NULL;
init_tables();
if (!has_event) {
if (dest != NO_HOP) {
print_line("ronda 0", dest);
} else {
printf("ronda 0\n");
print_matrix();
printf("\n");
}
}
int rounds = run(dest, !has_event, NULL, 0, max_rounds);
printf("convergió: %d %s, %ld anuncios\n", rounds, rounds == 1 ? "ronda" : "rondas",
announcements);
if (!has_event) {
return EXIT_SUCCESS;
}
if (dest != NO_HOP) {
print_line("antes", dest);
} else {
print_matrix();
}
announcements = 0;
if (fail_args[0] != NULL) {
fail_link(require_node(fail_args[0]), require_node(fail_args[1]));
printf("--- cae el enlace %s-%s\n", fail_args[0], fail_args[1]);
} else if (set_args[0] != NULL) {
set_link(require_node(set_args[0]), require_node(set_args[1]), atoi(set_args[2]));
printf("--- el enlace %s-%s pasa a costar %s\n", set_args[0], set_args[1], set_args[2]);
} else {
liar = require_node(lie_args[0]);
lie_dest = require_node(lie_args[1]);
lie_cost = atoi(lie_args[2]);
dist[liar][lie_dest] = lie_cost;
hop[liar][lie_dest] = NO_HOP;
printf("--- %s anuncia %s con costo %s\n", lie_args[0], lie_args[1], lie_args[2]);
}
if (dest != NO_HOP) {
print_line("evento", dest);
}
rounds = run(dest, true, order, order_size, max_rounds);
const char *unit = order_size > 0 ? (rounds == 1 ? "paso" : "pasos")
: (rounds == 1 ? "ronda" : "rondas");
printf("%s: %d %s, %ld anuncios\n", stopped_by_limit ? "detenido por --max" : "volvió a converger",
rounds, unit, announcements);
return EXIT_SUCCESS;
}"""Ayudas de las pruebas de la semana 5: levantar topologías y correr comandos."""
import json
import os
import subprocess
import time
import unittest
HERE = os.path.dirname(os.path.abspath(__file__))
# La solución no se publica. Sin ella, las pruebas usan lo que ustedes escriben en lab/.
ANSWERS = os.path.join(HERE, "solution")
if not os.path.isdir(ANSWERS):
ANSWERS = os.path.join(HERE, "lab")
def answer_path(name):
"""Devuelve la ruta de un archivo del laboratorio, o salta si sigue con el TODO."""
path = os.path.join(ANSWERS, name)
with open(path, encoding="utf-8") as file:
if "TODO" in file.read():
raise unittest.SkipTest(f"falta completar lab/{name}")
return path
def run(args, timeout=240, stdin=None):
return subprocess.run(args, capture_output=True, text=True, timeout=timeout, stdin=stdin)
class Lab:
"""Una topología de Containerlab que se levanta y se destruye desde las pruebas."""
def __init__(self, folder, name, network):
self.folder = os.path.join(HERE, folder)
self.topology = f"{folder}.clab.yml"
self.prefix = f"clab-{name}-"
self.network = network
def deploy(self, **variables):
done = subprocess.run(
["containerlab", "deploy", "-t", self.topology, "--reconfigure"],
cwd=self.folder, capture_output=True, text=True, timeout=300,
env={**os.environ, **variables})
if done.returncode != 0:
raise RuntimeError(done.stdout + done.stderr)
def destroy(self):
subprocess.run(["containerlab", "destroy", "-t", self.topology, "--cleanup"],
cwd=self.folder, capture_output=True, text=True, timeout=300)
subprocess.run(["docker", "network", "rm", self.network], capture_output=True, text=True)
def sh(self, node, *command, timeout=60):
"""Corre un comando en un nodo y devuelve el resultado completo."""
return run(["docker", "exec", self.prefix + node, *command], timeout=timeout)
def out(self, node, *command, timeout=60):
"""Corre un comando en un nodo y devuelve stdout y stderr juntos."""
done = self.sh(node, *command, timeout=timeout)
return done.stdout + done.stderr
def daemon(self, node, command):
"""Deja un comando corriendo en segundo plano dentro de un nodo."""
run(["docker", "exec", "-d", self.prefix + node, "sh", "-c", command])
def vtysh(self, node, *commands):
"""Corre comandos de vtysh en un nodo y devuelve su salida."""
arguments = [part for command in commands for part in ("-c", command)]
return self.out(node, "vtysh", *arguments)
def configure(self, node, *commands):
"""Aplica líneas de configuración, como en conf t."""
return self.vtysh(node, "conf t", *commands)
def route(self, node, prefix):
"""Devuelve la entrada de la tabla de routing de un prefijo, en JSON de FRRouting."""
output = self.vtysh(node, f"show ip route {prefix} json")
return json.loads(output or "{}").get(prefix, [])
def neighbor_states(self, node):
"""Devuelve el estado de cada vecino OSPF: {router id: estado}."""
data = json.loads(self.vtysh(node, "show ip ospf neighbor json") or "{}")
states = {}
for router_id, entries in data.get("neighbors", {}).items():
for entry in entries:
states[router_id] = entry.get("nbrState", "")
return states
def full_with(self, node, router_id):
return self.neighbor_states(node).get(router_id, "").startswith("Full")
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()# Tres áreas en línea para ver qué cruza de una a otra:
#
# área 1 área 0 área 2
# x1 ---- abr1 ---- core ---- abr2 ---- y1
#
# abr1 y abr2 son area border routers. x1 e y1 tienen LAN simuladas con interfaces
# dummy, para poder apagarlas sin tocar los enlaces.
#
# Con ABR1_CONF=frr-range.conf, abr1 arranca resumiendo el área 1 en un solo prefijo.
name: w05-areas
mgmt:
network: redes-w05-areas
ipv4-subnet: 172.90.5.128/26
ipv6-subnet: 3fff:172:90:5:2::/80
topology:
defaults:
kind: linux
image: redes-router
exec:
- ip route del default
- ip -6 route del default
- sysctl -w net.ipv6.conf.all.disable_ipv6=1
nodes:
x1:
binds: [x1/frr.conf:/etc/frr/frr.conf:ro]
exec:
- ip route del default
- ip -6 route del default
- sysctl -w net.ipv6.conf.all.disable_ipv6=1
- ip link add lan1 type dummy
- ip link add lan2 type dummy
- ip link set lan1 up
- ip link set lan2 up
abr1:
binds: [abr1/${ABR1_CONF:=frr.conf}:/etc/frr/frr.conf]
core:
binds: [core/frr.conf:/etc/frr/frr.conf:ro]
abr2:
binds: [abr2/frr.conf:/etc/frr/frr.conf:ro]
y1:
binds: [y1/frr.conf:/etc/frr/frr.conf:ro]
exec:
- ip route del default
- ip -6 route del default
- sysctl -w net.ipv6.conf.all.disable_ipv6=1
- ip link add lan1 type dummy
- ip link set lan1 up
links:
- endpoints: ["x1:eth1", "abr1:eth1"]
mtu: 1500
- endpoints: ["abr1:eth2", "core:eth1"]
mtu: 1500
- endpoints: ["core:eth2", "abr2:eth1"]
mtu: 1500
- endpoints: ["abr2:eth2", "y1:eth1"]
mtu: 1500# Para el ejemplo de vector de distancias, iteración por iteración.
w x 2
w y 9
x y 3
x z 8
y z 1# Tres nodos en línea, para el conteo al infinito.
x y 1
y z 1# El grafo de seis nodos del capítulo: nodo nodo costo. Es el mismo de six/.
a b 3
a c 1
b c 4
b d 2
c d 5
c e 5
d e 1
d f 7
e f 2# Un triángulo y una cola: aquí split horizon y poison reverse no alcanzan.
a b 1
a c 1
b c 1
c d 1# Triángulo con costos desiguales, para buenas y malas noticias.
p q 2
q r 1
p r 30# Topología del laboratorio: cinco routers con caminos alternativos y un host en
# cada extremo. Los routers traen sus direcciones, pero no OSPF: configurarlo es el
# trabajo del laboratorio.
#
# h1
# |
# r1
# / \
# r2 -- r3
# | |
# r4 -- r5
# |
# h4
name: w05-ospf
mgmt:
network: redes-w05-ospf
ipv4-subnet: 172.90.5.64/26
ipv6-subnet: 3fff:172:90:5:1::/80
topology:
defaults:
kind: linux
image: redes-router
exec:
- ip route del default
- ip -6 route del default
- sysctl -w net.ipv6.conf.all.disable_ipv6=1
nodes:
r1:
binds: [r1/frr.conf:/etc/frr/frr.conf:ro]
r2:
binds: [r2/frr.conf:/etc/frr/frr.conf:ro]
r3:
binds: [r3/frr.conf:/etc/frr/frr.conf:ro]
r4:
binds: [r4/frr.conf:/etc/frr/frr.conf:ro]
r5:
binds: [r5/frr.conf:/etc/frr/frr.conf:ro]
h1:
exec:
- ip route del default
- ip -6 route del default
- sysctl -w net.ipv6.conf.all.disable_ipv6=1
- ip addr add 10.205.101.10/24 dev eth1
- ip route add default via 10.205.101.1
h4:
exec:
- ip route del default
- ip -6 route del default
- sysctl -w net.ipv6.conf.all.disable_ipv6=1
- ip addr add 10.205.104.10/24 dev eth1
- ip route add default via 10.205.104.1
links:
- endpoints: ["r1:eth1", "r2:eth1"]
mtu: 1500
- endpoints: ["r1:eth2", "r3:eth1"]
mtu: 1500
- endpoints: ["r2:eth2", "r3:eth2"]
mtu: 1500
- endpoints: ["r2:eth3", "r4:eth1"]
mtu: 1500
- endpoints: ["r3:eth3", "r5:eth1"]
mtu: 1500
- endpoints: ["r4:eth2", "r5:eth2"]
mtu: 1500
- endpoints: ["h1:eth1", "r1:eth3"]
mtu: 1500
- endpoints: ["h4:eth1", "r4:eth3"]
mtu: 1500# El grafo de seis nodos del capítulo, hecho red: cada nodo es un router y cada
# arista es un enlace cuyo costo de OSPF es el peso del grafo.
#
# 3 2 7
# a ---- b ----- d ------ f
# \ | / | /
# \1 |4 5/ |1 /2
# \ | / | /
# \ | / | /
# c ------ e
# 5
name: w05-six
mgmt:
network: redes-w05-six
ipv4-subnet: 172.90.5.0/26
ipv6-subnet: 3fff:172:90:5::/80
topology:
defaults:
kind: linux
image: redes-router
# Sin ruta por defecto de gestión: lo que un router no sabe a dónde mandar debe
# fallar, no irse por Docker. Sin IPv6 para que no aparezca ruido en las capturas.
exec:
- ip route del default
- ip -6 route del default
- sysctl -w net.ipv6.conf.all.disable_ipv6=1
nodes:
a:
binds: [a/frr.conf:/etc/frr/frr.conf:ro]
b:
binds: [b/frr.conf:/etc/frr/frr.conf:ro]
c:
binds: [c/frr.conf:/etc/frr/frr.conf:ro]
d:
binds: [d/frr.conf:/etc/frr/frr.conf:ro]
e:
binds: [e/frr.conf:/etc/frr/frr.conf:ro]
f:
binds: [f/frr.conf:/etc/frr/frr.conf:ro]
# Containerlab usa MTU 9500 por defecto; aquí queremos el valor de Ethernet.
links:
- endpoints: ["a:eth1", "b:eth1"]
mtu: 1500
- endpoints: ["a:eth2", "c:eth1"]
mtu: 1500
- endpoints: ["b:eth2", "c:eth2"]
mtu: 1500
- endpoints: ["b:eth3", "d:eth1"]
mtu: 1500
- endpoints: ["c:eth3", "d:eth2"]
mtu: 1500
- endpoints: ["c:eth4", "e:eth1"]
mtu: 1500
- endpoints: ["d:eth3", "e:eth2"]
mtu: 1500
- endpoints: ["d:eth4", "f:eth1"]
mtu: 1500
- endpoints: ["e:eth3", "f:eth2"]
mtu: 1500frr defaults traditional
hostname abr1
log file /tmp/ospfd.log
!
interface eth1
ip address 10.205.16.2/30
ip ospf network point-to-point
ip ospf cost 10
!
interface eth2
ip address 10.205.1.1/30
ip ospf network point-to-point
ip ospf cost 10
!
router ospf
ospf router-id 10.205.0.12
network 10.205.16.0/20 area 1
network 10.205.1.0/30 area 0
! un solo prefijo hacia afuera por todo lo del área 1
area 1 range 10.205.16.0/20frr defaults traditional
hostname abr1
log file /tmp/ospfd.log
!
interface eth1
ip address 10.205.16.2/30
ip ospf network point-to-point
ip ospf cost 10
!
interface eth2
ip address 10.205.1.1/30
ip ospf network point-to-point
ip ospf cost 10
!
router ospf
ospf router-id 10.205.0.12
network 10.205.16.0/20 area 1
network 10.205.1.0/30 area 0frr defaults traditional
hostname abr2
log file /tmp/ospfd.log
!
interface eth1
ip address 10.205.2.2/30
ip ospf network point-to-point
ip ospf cost 10
!
interface eth2
ip address 10.205.32.1/30
ip ospf network point-to-point
ip ospf cost 10
!
router ospf
ospf router-id 10.205.0.14
network 10.205.2.0/30 area 0
network 10.205.32.0/20 area 2frr defaults traditional
hostname core
log file /tmp/ospfd.log
!
interface eth1
ip address 10.205.1.2/30
ip ospf network point-to-point
ip ospf cost 10
!
interface eth2
ip address 10.205.2.1/30
ip ospf network point-to-point
ip ospf cost 10
!
router ospf
ospf router-id 10.205.0.13
network 10.205.1.0/30 area 0
network 10.205.2.0/30 area 0frr defaults traditional
hostname x1
log file /tmp/ospfd.log
!
interface eth1
ip address 10.205.16.1/30
ip ospf network point-to-point
ip ospf cost 10
!
! dos LAN de área 1, simuladas con interfaces dummy
interface lan1
ip address 10.205.17.1/24
!
interface lan2
ip address 10.205.18.1/24
!
router ospf
ospf router-id 10.205.0.11
network 10.205.16.0/20 area 1frr defaults traditional
hostname y1
log file /tmp/ospfd.log
!
interface eth1
ip address 10.205.32.2/30
ip ospf network point-to-point
ip ospf cost 10
!
! una LAN de área 2
interface lan1
ip address 10.205.33.1/24
!
router ospf
ospf router-id 10.205.0.15
network 10.205.32.0/20 area 2frr defaults traditional
hostname r1
log file /tmp/ospfd.log
!
interface lo
ip address 10.205.0.1/32
!
! hacia r2
interface eth1
ip address 10.205.12.1/30
ip ospf network point-to-point
!
! hacia r3
interface eth2
ip address 10.205.13.1/30
ip ospf network point-to-point
!
! la LAN de h1
interface eth3
ip address 10.205.101.1/24
ip ospf cost 10
!frr defaults traditional
hostname r2
log file /tmp/ospfd.log
!
interface lo
ip address 10.205.0.2/32
!
! hacia r1
interface eth1
ip address 10.205.12.2/30
ip ospf network point-to-point
!
! hacia r3
interface eth2
ip address 10.205.23.1/30
ip ospf network point-to-point
!
! hacia r4
interface eth3
ip address 10.205.24.1/30
ip ospf network point-to-point
!frr defaults traditional
hostname r3
log file /tmp/ospfd.log
!
interface lo
ip address 10.205.0.3/32
!
! hacia r1
interface eth1
ip address 10.205.13.2/30
ip ospf network point-to-point
!
! hacia r2
interface eth2
ip address 10.205.23.2/30
ip ospf network point-to-point
!
! hacia r5
interface eth3
ip address 10.205.35.1/30
ip ospf network point-to-point
!frr defaults traditional
hostname r4
log file /tmp/ospfd.log
!
interface lo
ip address 10.205.0.4/32
!
! hacia r2
interface eth1
ip address 10.205.24.2/30
ip ospf network point-to-point
!
! hacia r5
interface eth2
ip address 10.205.45.1/30
ip ospf network point-to-point
!
! la LAN de h4
interface eth3
ip address 10.205.104.1/24
ip ospf cost 10
!frr defaults traditional
hostname r5
log file /tmp/ospfd.log
!
interface lo
ip address 10.205.0.5/32
!
! hacia r3
interface eth1
ip address 10.205.35.2/30
ip ospf network point-to-point
!
! hacia r4
interface eth2
ip address 10.205.45.2/30
ip ospf network point-to-point
!frr defaults traditional
hostname a
log file /tmp/ospfd.log
!
interface lo
ip address 10.205.0.1/32
!
! hacia b
interface eth1
ip address 10.205.12.1/30
ip ospf network point-to-point
ip ospf cost 3
!
! hacia c
interface eth2
ip address 10.205.13.1/30
ip ospf network point-to-point
ip ospf cost 1
!
router ospf
ospf router-id 10.205.0.1
log-adjacency-changes detail
network 10.205.0.0/16 area 0frr defaults traditional
hostname b
log file /tmp/ospfd.log
!
interface lo
ip address 10.205.0.2/32
!
! hacia a
interface eth1
ip address 10.205.12.2/30
ip ospf network point-to-point
ip ospf cost 3
!
! hacia c
interface eth2
ip address 10.205.23.1/30
ip ospf network point-to-point
ip ospf cost 4
!
! hacia d
interface eth3
ip address 10.205.24.1/30
ip ospf network point-to-point
ip ospf cost 2
!
router ospf
ospf router-id 10.205.0.2
log-adjacency-changes detail
network 10.205.0.0/16 area 0frr defaults traditional
hostname c
log file /tmp/ospfd.log
!
interface lo
ip address 10.205.0.3/32
!
! hacia a
interface eth1
ip address 10.205.13.2/30
ip ospf network point-to-point
ip ospf cost 1
!
! hacia b
interface eth2
ip address 10.205.23.2/30
ip ospf network point-to-point
ip ospf cost 4
!
! hacia d
interface eth3
ip address 10.205.34.1/30
ip ospf network point-to-point
ip ospf cost 5
!
! hacia e
interface eth4
ip address 10.205.35.1/30
ip ospf network point-to-point
ip ospf cost 5
!
router ospf
ospf router-id 10.205.0.3
log-adjacency-changes detail
network 10.205.0.0/16 area 0frr defaults traditional
hostname d
log file /tmp/ospfd.log
!
interface lo
ip address 10.205.0.4/32
!
! hacia b
interface eth1
ip address 10.205.24.2/30
ip ospf network point-to-point
ip ospf cost 2
!
! hacia c
interface eth2
ip address 10.205.34.2/30
ip ospf network point-to-point
ip ospf cost 5
!
! hacia e
interface eth3
ip address 10.205.45.1/30
ip ospf network point-to-point
ip ospf cost 1
!
! hacia f
interface eth4
ip address 10.205.46.1/30
ip ospf network point-to-point
ip ospf cost 7
!
router ospf
ospf router-id 10.205.0.4
log-adjacency-changes detail
network 10.205.0.0/16 area 0frr defaults traditional
hostname e
log file /tmp/ospfd.log
!
interface lo
ip address 10.205.0.5/32
!
! hacia c
interface eth1
ip address 10.205.35.2/30
ip ospf network point-to-point
ip ospf cost 5
!
! hacia d
interface eth2
ip address 10.205.45.2/30
ip ospf network point-to-point
ip ospf cost 1
!
! hacia f
interface eth3
ip address 10.205.56.1/30
ip ospf network point-to-point
ip ospf cost 2
!
router ospf
ospf router-id 10.205.0.5
log-adjacency-changes detail
network 10.205.0.0/16 area 0frr defaults traditional
hostname f
log file /tmp/ospfd.log
!
interface lo
ip address 10.205.0.6/32
!
! hacia d
interface eth1
ip address 10.205.46.2/30
ip ospf network point-to-point
ip ospf cost 7
!
! hacia e
interface eth2
ip address 10.205.56.2/30
ip ospf network point-to-point
ip ospf cost 2
!
router ospf
ospf router-id 10.205.0.6
log-adjacency-changes detail
network 10.205.0.0/16 area 0Pruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
"""Pruebas de las áreas de OSPF en areas/areas.clab.yml: qué cruza de un área a otra."""
import time
import unittest
from netlab import Lab, wait_until
LAB = Lab("areas", "w05-areas", "redes-w05-areas")
X1_ID = "10.205.0.11"
ROUTERS = {"x1": 1, "abr1": 2, "core": 2, "abr2": 2, "y1": 1}
def setUpModule():
unittest.addModuleCleanup(LAB.destroy)
def converged(prefix):
return all(len(LAB.neighbor_states(node)) == count
and all(state.startswith("Full") for state in LAB.neighbor_states(node).values())
for node, count in ROUTERS.items()) and bool(LAB.route("y1", prefix))
def spf_count(node):
"""Cuántas veces corrió SPF en cada área del router, sumadas."""
text = LAB.vtysh(node, "show ip ospf")
return sum(int(line.split()[3]) for line in text.splitlines() if "SPF algorithm executed" in line)
def x1_router_lsa_seq(node):
text = LAB.vtysh(node, f"show ip ospf database router {X1_ID}")
return [line.split()[-1] for line in text.splitlines() if "LS Seq Number" in line]
class A_WithoutSummary(unittest.TestCase):
@classmethod
def setUpClass(cls):
LAB.deploy()
if not wait_until(lambda: converged("10.205.17.0/24"), 60):
raise RuntimeError("OSPF no convergió")
def setUp(self):
# Una prueba apaga lan1 y la vuelve a prender: hay que esperar a que reaparezca.
self.assertTrue(wait_until(lambda: LAB.route("y1", "10.205.17.0/24"), 30))
def test_each_area_keeps_its_own_router_lsas(self):
database = LAB.vtysh("y1", "show ip ospf database")
self.assertIn("10.205.0.15", database)
self.assertIn("Summary Link States", database)
self.assertEqual(x1_router_lsa_seq("y1"), [])
self.assertNotEqual(x1_router_lsa_seq("abr1"), [])
def test_summaries_carry_each_prefix_of_the_other_area(self):
database = LAB.vtysh("y1", "show ip ospf database summary")
for prefix in ("10.205.16.0", "10.205.17.0", "10.205.18.0"):
self.assertIn(prefix, database)
self.assertTrue(LAB.route("y1", "10.205.17.0/24"))
def test_abr_keeps_one_database_per_area(self):
database = LAB.vtysh("abr1", "show ip ospf database")
self.assertIn("Router Link States (Area 0.0.0.0)", database)
self.assertIn("Router Link States (Area 0.0.0.1)", database)
def test_a_change_in_area_1_reaches_area_2_only_as_a_summary(self):
before = x1_router_lsa_seq("abr1")
LAB.sh("x1", "ip", "link", "set", "lan1", "down")
self.addCleanup(LAB.sh, "x1", "ip", "link", "set", "lan1", "up")
self.assertTrue(wait_until(lambda: not LAB.route("y1", "10.205.17.0/24"), 30))
self.assertNotEqual(x1_router_lsa_seq("abr1"), before)
self.assertEqual(x1_router_lsa_seq("y1"), [])
class B_WithSummary(unittest.TestCase):
@classmethod
def setUpClass(cls):
LAB.destroy()
LAB.deploy(ABR1_CONF="frr-range.conf")
if not wait_until(lambda: converged("10.205.16.0/20"), 60):
raise RuntimeError("OSPF no convergió con el resumen")
def test_one_prefix_replaces_the_detail(self):
database = LAB.vtysh("y1", "show ip ospf database summary")
self.assertIn("10.205.16.0/20", LAB.vtysh("y1", "show ip route ospf"))
self.assertNotIn("10.205.17.0", database)
self.assertNotIn("10.205.18.0", database)
def test_a_change_inside_does_not_leave_the_area(self):
# La red recién convergió: que termine de calcular antes de tomar la línea base.
before = spf_count("y1")
for _ in range(10):
time.sleep(8)
current = spf_count("y1")
if current == before:
break
before = current
database = LAB.vtysh("y1", "show ip ospf database summary")
LAB.sh("x1", "ip", "link", "set", "lan1", "down")
self.addCleanup(LAB.sh, "x1", "ip", "link", "set", "lan1", "up")
self.assertTrue(wait_until(lambda: not LAB.route("abr1", "10.205.17.0/24"), 30))
time.sleep(4) # si algo fuera a salir del área, daría tiempo a que llegue
self.assertTrue(LAB.route("y1", "10.205.16.0/20"))
self.assertEqual(spf_count("y1"), before)
self.assertEqual(self._seqs(LAB.vtysh("y1", "show ip ospf database summary")),
self._seqs(database))
# El precio: y1 sigue mandando hacia allá, y el paquete se pierde adentro.
self.assertNotEqual(LAB.sh("y1", "ping", "-c", "1", "-W", "1", "10.205.17.1").returncode, 0)
self.assertEqual(LAB.sh("y1", "ping", "-c", "2", "-W", "2", "10.205.18.1").returncode, 0)
@staticmethod
def _seqs(text):
return [line.split()[-1] for line in text.splitlines() if "LS Seq Number" in line]
if __name__ == "__main__":
unittest.main()"""Pruebas del laboratorio de OSPF en ospf/ospf.clab.yml."""
import subprocess
import time
import unittest
from netlab import Lab, answer_path, wait_until
LAB = Lab("ospf", "w05-ospf", "redes-w05-ospf")
R1 = "10.205.0.1"
R2 = "10.205.0.2"
ROUTERS = {"r1": 2, "r2": 3, "r3": 3, "r4": 2, "r5": 2}
def setUpModule():
unittest.addModuleCleanup(LAB.destroy)
LAB.deploy()
def run_answer(name):
done = subprocess.run(["sh", answer_path(name)], capture_output=True, text=True, timeout=120)
if done.returncode != 0:
raise AssertionError(f"{name} falló: {done.stdout}{done.stderr}")
def all_full():
for router, count in ROUTERS.items():
states = LAB.neighbor_states(router)
if len(states) != count or not all(s.startswith("Full") for s in states.values()):
return False
return True
def adjacency_log(node):
return [line for line in LAB.out(node, "cat", "/tmp/ospfd.log").splitlines() if "AdjChg" in line]
class A_Starting(unittest.TestCase):
"""Se corre antes que todo lo demás: los routers aún no hablan OSPF."""
def test_1_no_ospf_at_start(self):
self.assertNotIn("router ospf", LAB.vtysh("r1", "show running-config"))
self.assertNotEqual(LAB.sh("h1", "ping", "-c", "1", "-W", "1", "10.205.104.10").returncode, 0)
def test_2_links_use_ethernet_mtu(self):
self.assertIn("mtu 1500", LAB.out("r1", "ip", "link", "show", "eth1"))
def test_3_two_routers_reach_full_through_every_state(self):
for node, router_id in (("r1", R1), ("r2", R2)):
LAB.configure(node, "router ospf", f"ospf router-id {router_id}",
"log-adjacency-changes detail", "network 10.205.12.0/30 area 0")
self.assertTrue(wait_until(lambda: LAB.full_with("r1", R2) and LAB.full_with("r2", R1), 40))
log = "\n".join(adjacency_log("r1"))
# En un enlace point-to-point no se elige DR: de Init se pasa directo a ExStart.
for change in ("Down -> Init", "Init -> ExStart", "ExStart -> Exchange"):
self.assertIn(change, log)
self.assertRegex(log, r"-> Full")
class B_Lab(unittest.TestCase):
"""Los pasos 2, 3 y 5, con lo que ustedes escriben en lab/."""
@classmethod
def setUpClass(cls):
run_answer("ospf.sh")
if not wait_until(all_full, 60):
raise RuntimeError("no todas las adyacencias llegaron a Full")
wait_until(lambda: LAB.route("r1", "10.205.104.0/24"), 30)
def test_1_hosts_reach_each_other(self):
self.assertEqual(LAB.sh("h1", "ping", "-c", "2", "-W", "2", "10.205.104.10").returncode, 0)
def test_2_lan_is_passive(self):
for node in ("r1", "r4"):
self.assertIn("No Hellos (Passive interface)",
LAB.vtysh(node, "show ip ospf interface eth3"))
def test_3_hand_table_matches_the_router(self):
with open(answer_path("r1_table.txt"), encoding="utf-8") as file:
lines = [line.split() for line in file if line.strip() and not line.startswith("#")]
expected = {}
for prefix, cost, hop in lines:
expected.setdefault(prefix, (int(cost), set()))[1].add(hop)
for prefix in ("10.205.0.2/32", "10.205.0.3/32", "10.205.0.4/32", "10.205.0.5/32",
"10.205.104.0/24"):
self.assertIn(prefix, expected, "falta en la tabla")
for prefix, (cost, hops) in expected.items():
entries = [e for e in LAB.route("r1", prefix) if e["protocol"] == "ospf"]
self.assertEqual(len(entries), 1, prefix)
self.assertEqual(entries[0]["metric"], cost, prefix)
self.assertEqual({hop["ip"] for hop in entries[0]["nexthops"]}, hops, prefix)
def test_4_changed_cost_moves_both_directions(self):
run_answer("cost.sh")
def forward():
return LAB.out("h1", "traceroute", "-n", "-q", "1", "-w", "1", "10.205.104.10")
def backward():
return LAB.out("h4", "traceroute", "-n", "-q", "1", "-w", "1", "10.205.101.10")
self.assertTrue(wait_until(lambda: "10.205.35." in forward() and "10.205.24." not in forward(), 30),
forward())
self.assertTrue(wait_until(lambda: "10.205.35." in backward() and "10.205.24." not in backward(), 30),
backward())
def test_5_short_timers_recover_quickly_from_a_silent_cut(self):
# El corte es en r3-r5, que solo lleva el tráfico después del paso 3.
answer_path("cost.sh")
run_answer("timers.sh")
for router, interface in (("r3", "eth3"), ("r5", "eth1"), ("r1", "eth1")):
self.assertIn("Hello 2s, Dead 8s", LAB.vtysh(router, f"show ip ospf interface {interface}"))
# Mientras se aplican uno por uno hay vecinos con valores distintos: mejor esperar
# a que pase un Dead completo y las adyacencias estén firmes.
time.sleep(10)
self.assertTrue(wait_until(all_full, 30))
ends = (("r3", "eth3"), ("r5", "eth1"))
for node, interface in ends:
LAB.sh(node, "tc", "qdisc", "add", "dev", interface, "root", "netem", "loss", "100%")
def repair():
for node, interface in ends:
LAB.sh(node, "tc", "qdisc", "del", "dev", interface, "root")
self.addCleanup(repair)
def recovered():
return LAB.sh("h1", "ping", "-c", "1", "-W", "1", "10.205.104.10").returncode == 0
# Con OSPF sin enterarse el camino sigue roto; con Dead de 8 s debe volver en poco.
self.assertFalse(recovered())
self.assertTrue(wait_until(recovered, 30, 0.5))
route = LAB.vtysh("r1", "show ip route 10.205.104.0/24")
self.assertRegex(route, r"10\.205\.12\.2")
if __name__ == "__main__":
unittest.main()"""Pruebas de OSPF sobre el grafo del capítulo, six/six.clab.yml."""
import json
import os
import re
import subprocess
import time
import unittest
from netlab import HERE, Lab, wait_until
LAB = Lab("six", "w05-six", "redes-w05-six")
NODES = "abcdef"
EDGES = [("a", "b", 3), ("a", "c", 1), ("b", "c", 4), ("b", "d", 2), ("c", "d", 5),
("c", "e", 5), ("d", "e", 1), ("d", "f", 7), ("e", "f", 2)]
def number(node):
return NODES.index(node) + 1
def loopback(node):
return f"10.205.0.{number(node)}"
def peer_addresses():
"""Dirección de cada extremo de cada enlace: {ip: router}."""
owners = {}
for left, right, _ in EDGES:
owners[f"10.205.{number(left)}{number(right)}.1"] = left
owners[f"10.205.{number(left)}{number(right)}.2"] = right
return owners
def neighbor_count(node):
return sum(node in edge[:2] for edge in EDGES)
def all_full():
for node in NODES:
states = LAB.neighbor_states(node)
if len(states) != neighbor_count(node) or not all(s.startswith("Full") for s in states.values()):
return False
return True
def setUpModule():
unittest.addModuleCleanup(LAB.destroy)
LAB.deploy()
if not wait_until(all_full, 60):
raise RuntimeError("las adyacencias no llegaron a Full")
wait_until(lambda: LAB.route("a", "10.205.0.6/32"), 30)
def dijkstra_table(source):
out = subprocess.run([os.path.join(HERE, "dijkstra"), "graphs/six.txt", source],
capture_output=True, text=True, cwd=HERE, timeout=10).stdout
table = {}
for line in out.split("forwarding table")[1].splitlines()[2:]:
fields = line.split()
table[fields[0]] = (int(fields[1]), set(fields[2:]))
return table
def via(node, destination):
"""El next hop de la forwarding table del kernel hacia una dirección."""
match = re.search(r"via (\S+)", LAB.out(node, "ip", "route", "get", destination))
return match.group(1) if match else None
def cut_silently(*ends):
"""Corta un enlace sin que se note en la interfaz: todo se pierde, el carrier sigue."""
for node, interface in ends:
LAB.sh(node, "tc", "qdisc", "add", "dev", interface, "root", "netem", "loss", "100%")
def repair(*ends):
for node, interface in ends:
LAB.sh(node, "tc", "qdisc", "del", "dev", interface, "root")
BD_LINK = (("b", "eth3"), ("d", "eth1"))
THROUGH_B = "10.205.12.2"
THROUGH_C = "10.205.13.2"
def time_until_reroute(limit):
start = time.monotonic()
if not wait_until(lambda: via("a", loopback("d")) == THROUGH_C, limit, 0.2):
return None
return time.monotonic() - start
def back_to_normal():
return wait_until(lambda: LAB.full_with("b", loopback("d")) and LAB.full_with("d", loopback("b"))
and via("a", loopback("d")) == THROUGH_B, 60)
class A_MatchesTheGraph(unittest.TestCase):
def test_routes_match_dijkstra_from_every_node(self):
owners = peer_addresses()
for source in NODES:
for destination, (cost, hops) in dijkstra_table(source).items():
entries = [e for e in LAB.route(source, f"{loopback(destination)}/32")
if e["protocol"] == "ospf"]
self.assertEqual(len(entries), 1, (source, destination))
self.assertEqual(entries[0]["metric"], cost, (source, destination))
found = {owners[hop["ip"]] for hop in entries[0]["nexthops"]}
self.assertEqual(found, hops, (source, destination))
def test_tie_is_installed_in_the_kernel_as_ecmp(self):
table = LAB.out("a", "ip", "route", "show", "10.205.0.5")
self.assertIn(f"nexthop via {THROUGH_B}", table)
self.assertIn(f"nexthop via {THROUGH_C}", table)
def test_every_router_holds_the_same_map(self):
def lsas(node):
text = LAB.vtysh(node, "show ip ospf database")
return sorted((f[0], f[1], f[3], f[4]) for f in
(line.split() for line in text.splitlines())
if len(f) >= 5 and f[3].startswith("0x8"))
self.assertTrue(wait_until(lambda: all(lsas(n) == lsas("a") for n in NODES), 20))
self.assertEqual(len(lsas("a")), 6)
def test_router_lsa_lists_the_costs_of_its_links(self):
text = LAB.vtysh("a", f"show ip ospf database router {loopback('b')}")
self.assertIn("Number of Links: 7", text) # loopback y 2 por cada uno de sus 3 enlaces
self.assertIn(f"Neighboring Router ID: {loopback('d')}", text)
self.assertRegex(text, r"TOS 0 Metric: 2")
self.assertRegex(text, r"TOS 0 Metric: 4")
def test_point_to_point_means_no_dr(self):
self.assertTrue(LAB.neighbor_states("a")[loopback("b")].startswith("Full/-"))
self.assertIn("Network Type POINTOPOINT", LAB.vtysh("a", "show ip ospf interface eth1"))
self.assertNotIn("Net Link States", LAB.vtysh("a", "show ip ospf database"))
class B_RoutingVersusForwarding(unittest.TestCase):
def test_kernel_metric_is_not_the_ospf_cost(self):
self.assertEqual(LAB.route("a", "10.205.0.4/32")[0]["metric"], 5)
self.assertIn("metric 20", LAB.out("a", "ip", "route", "show", "10.205.0.4"))
def test_connected_beats_ospf_for_own_link(self):
entries = LAB.route("a", "10.205.12.0/30")
selected = {e["protocol"]: e.get("selected", False) for e in entries}
self.assertEqual(selected.get("connected"), True)
self.assertFalse(selected.get("ospf"))
def test_static_route_wins_and_ospf_keeps_its_own_answer(self):
self.addCleanup(LAB.configure, "a", "no ip route 10.205.0.4/32 10.205.13.2")
LAB.configure("a", "ip route 10.205.0.4/32 10.205.13.2")
self.assertTrue(wait_until(lambda: via("a", loopback("d")) == THROUGH_C, 10))
entries = {e["protocol"]: e for e in LAB.route("a", "10.205.0.4/32")}
self.assertTrue(entries["static"].get("selected"))
self.assertFalse(entries["ospf"].get("selected"))
self.assertEqual(entries["ospf"]["metric"], 5)
ospf_table = LAB.vtysh("a", "show ip ospf route")
self.assertRegex(ospf_table, r"10\.205\.0\.4/32\s+\[5\][^\n]*\n\s+via 10\.205\.12\.2")
# Sin nombre para ese número en esta imagen: 196 es el protocolo "static" de FRRouting.
self.assertRegex(LAB.out("a", "ip", "route", "show", "10.205.0.4"), r"proto (196|static)")
LAB.configure("a", "no ip route 10.205.0.4/32 10.205.13.2")
self.assertTrue(wait_until(lambda: via("a", loopback("d")) == THROUGH_B, 10))
class C_Failures(unittest.TestCase):
"""El orden importa: primero los temporizadores por defecto, después los cortos."""
def setUp(self):
self.assertTrue(back_to_normal())
def test_1_link_down_is_noticed_at_once(self):
self.addCleanup(LAB.sh, "b", "ip", "link", "set", "eth3", "up")
LAB.sh("b", "ip", "link", "set", "eth3", "down")
self.assertIn("NO-CARRIER", LAB.out("d", "ip", "link", "show", "eth1"))
elapsed = time_until_reroute(8)
self.assertIsNotNone(elapsed)
self.assertLess(elapsed, 8)
self.assertEqual(LAB.sh("a", "ping", "-c", "2", "-W", "2", "-I", loopback("a"),
loopback("d")).returncode, 0)
def test_2_silent_failure_waits_for_dead_interval(self):
self.addCleanup(repair, *BD_LINK)
cut_silently(*BD_LINK)
self.assertIn("LOWER_UP", LAB.out("d", "ip", "link", "show", "eth1"))
# Por ahora OSPF no se da cuenta, y lo que va por ese camino se pierde.
self.assertEqual(via("a", loopback("d")), THROUGH_B)
self.assertNotEqual(LAB.sh("a", "ping", "-c", "2", "-W", "1", "-I", loopback("a"),
loopback("d")).returncode, 0)
elapsed = time_until_reroute(70)
self.assertIsNotNone(elapsed, "no hubo cambio de ruta")
self.assertGreater(elapsed, 20)
self.assertEqual(LAB.sh("a", "ping", "-c", "2", "-W", "2", "-I", loopback("a"),
loopback("d")).returncode, 0)
def test_3_short_blip_does_not_drop_a_default_adjacency(self):
self.addCleanup(repair, *BD_LINK)
cut_silently(*BD_LINK)
time.sleep(12)
repair(*BD_LINK)
self.assertTrue(LAB.full_with("b", loopback("d")))
self.assertEqual(via("a", loopback("d")), THROUGH_B)
def set_short_timers(self):
for node, interface in BD_LINK:
LAB.configure(node, f"interface {interface}", "ip ospf hello-interval 2",
"ip ospf dead-interval 8")
self.addCleanup(LAB.configure, node, f"interface {interface}",
"no ip ospf hello-interval", "no ip ospf dead-interval")
self.assertIn("Hello 2s, Dead 8s", LAB.vtysh("b", "show ip ospf interface eth3"))
self.assertTrue(wait_until(lambda: LAB.full_with("b", loopback("d")), 15))
def test_4_short_timers_detect_sooner_and_also_trip_on_a_blip(self):
self.set_short_timers()
self.addCleanup(repair, *BD_LINK)
cut_silently(*BD_LINK)
elapsed = time_until_reroute(30)
self.assertIsNotNone(elapsed, "no hubo cambio de ruta")
self.assertLess(elapsed, 15)
repair(*BD_LINK)
self.assertTrue(back_to_normal())
# El mismo corte de 12 s del que el valor por defecto ni se entera.
cut_silently(*BD_LINK)
self.assertTrue(wait_until(lambda: not LAB.full_with("b", loopback("d")), 14))
repair(*BD_LINK)
class D_AdjacencyProblems(unittest.TestCase):
"""Cada caso rompe el enlace a-b, lo diagnostica y lo deja como estaba."""
def setUp(self):
self.assertTrue(wait_until(lambda: LAB.full_with("a", loopback("b"))
and LAB.full_with("b", loopback("a")), 60))
LAB.sh("a", "sh", "-c", "> /tmp/ospfd.log")
def log_has(self, text, seconds=25):
return wait_until(lambda: text in LAB.out("a", "cat", "/tmp/ospfd.log"), seconds)
def test_different_area(self):
def restore():
LAB.configure("a", "router ospf", "no network 10.205.12.0/30 area 1",
"no network 10.205.13.0/30 area 0", "no network 10.205.0.1/32 area 0",
"network 10.205.0.0/16 area 0")
self.addCleanup(restore)
LAB.configure("a", "router ospf", "no network 10.205.0.0/16 area 0",
"network 10.205.12.0/30 area 1", "network 10.205.13.0/30 area 0",
"network 10.205.0.1/32 area 0")
self.assertTrue(self.log_has("invalid Area ID"))
self.assertNotIn(loopback("b"), LAB.neighbor_states("a"))
self.assertIn("Area 0.0.0.1", LAB.vtysh("a", "show ip ospf interface eth1"))
self.assertIn("Area 0.0.0.0", LAB.vtysh("b", "show ip ospf interface eth1"))
def test_different_timers(self):
self.addCleanup(LAB.configure, "a", "interface eth1", "no ip ospf dead-interval")
LAB.configure("a", "interface eth1", "ip ospf dead-interval 20")
self.assertTrue(self.log_has("RouterDeadInterval mismatch"))
# La adyacencia ya formada no cae enseguida: cae cuando vence el Dead de a.
self.assertTrue(wait_until(lambda: loopback("b") not in LAB.neighbor_states("a"), 30))
self.assertIn("Dead 20s", LAB.vtysh("a", "show ip ospf interface eth1"))
self.assertIn("Dead 40s", LAB.vtysh("b", "show ip ospf interface eth1"))
def set_mask(self, new, old):
LAB.configure("a", "interface eth1", f"no ip address 10.205.12.1/{old}",
f"ip address 10.205.12.1/{new}")
def test_different_mask_is_ignored_on_point_to_point(self):
self.addCleanup(self.set_mask, 30, 29)
self.set_mask(29, 30)
self.assertTrue(wait_until(lambda: LAB.full_with("a", loopback("b")), 30))
self.assertNotIn("NetworkMask mismatch", LAB.out("a", "cat", "/tmp/ospfd.log"))
def test_different_mask_blocks_on_broadcast(self):
def restore():
self.set_mask(30, 29)
for node in ("a", "b"):
LAB.configure(node, "interface eth1", "ip ospf network point-to-point")
self.addCleanup(restore)
for node in ("a", "b"):
LAB.configure(node, "interface eth1", "no ip ospf network")
self.set_mask(29, 30)
self.assertTrue(self.log_has("NetworkMask mismatch"))
self.assertNotIn(loopback("b"), LAB.neighbor_states("a"))
def test_different_mtu_stops_at_exstart(self):
def restore():
LAB.sh("a", "ip", "link", "set", "eth1", "mtu", "1500")
self.addCleanup(restore)
LAB.sh("a", "ip", "link", "set", "eth1", "mtu", "1400")
LAB.sh("b", "ip", "link", "set", "eth1", "down")
LAB.sh("b", "ip", "link", "set", "eth1", "up")
self.assertTrue(self.log_has("MTU 1500 is larger", 40))
self.assertTrue(wait_until(
lambda: LAB.neighbor_states("a").get(loopback("b"), "").startswith("ExStart"), 20))
# Sigue atascado: el intercambio no avanza mientras el MTU no coincida.
time.sleep(6)
self.assertTrue(LAB.neighbor_states("a")[loopback("b")].startswith("ExStart"))
class E_DefaultCost(unittest.TestCase):
def test_default_cost_depends_on_bandwidth(self):
def restore():
LAB.configure("a", "interface eth1", "no bandwidth", "ip ospf cost 3")
self.addCleanup(restore)
LAB.configure("a", "interface eth1", "no ip ospf cost")
self.assertIn("Cost: 10", LAB.vtysh("a", "show ip ospf interface eth1"))
self.assertIn("BW 10000 Mbit", LAB.vtysh("a", "show ip ospf interface eth1"))
LAB.configure("a", "interface eth1", "bandwidth 1000")
self.assertTrue(wait_until(lambda: "Cost: 100" in LAB.vtysh("a", "show ip ospf interface eth1"), 5))
if __name__ == "__main__":
unittest.main()"""Pruebas de los programas en C: dijkstra y dv_sim."""
import os
import random
import re
import subprocess
import tempfile
import unittest
from netlab import HERE
def tool(name, *args):
return subprocess.run([os.path.join(HERE, name), *args], capture_output=True, text=True,
timeout=10, cwd=HERE)
def forwarding_table(output):
"""Convierte la tabla final de dijkstra en {destino: (costo, conjunto de next hop)}."""
table = {}
for line in output.split("forwarding table")[1].splitlines()[2:]:
fields = line.split()
table[fields[0]] = (fields[1], set(fields[2:]))
return table
def rounds_of(output):
return int(re.search(r"volvió a converger: (\d+)", output).group(1))
def final_costs(output, dest_names):
"""Lee el último renglón de dv_sim --dest: {nodo: costo}."""
lines = [line for line in output.splitlines() if "=" in line and not line.startswith("---")]
return {name: re.search(rf"\b{name}=(\w+)", lines[-1]).group(1) for name in dest_names}
class DijkstraTest(unittest.TestCase):
def test_table_of_the_chapter(self):
table = forwarding_table(tool("dijkstra", "graphs/six.txt", "a").stdout)
self.assertEqual(table, {
"b": ("3", {"b"}),
"c": ("1", {"c"}),
"d": ("5", {"b"}),
"e": ("6", {"b", "c"}),
"f": ("8", {"b", "c"}),
})
def test_iterations_of_the_chapter(self):
out = tool("dijkstra", "graphs/six.txt", "a").stdout
self.assertRegex(out, r"(?m)^1\s+ac\s")
self.assertRegex(out, r"(?m)^2\s+acb\s")
self.assertRegex(out, r"(?m)^3\s+acbd\s.*6,cd")
self.assertRegex(out, r"(?m)^4\s+acbde\s.*8,e")
def test_against_exhaustive_search_on_random_graphs(self):
"""Compara costos y next hop contra una búsqueda de todos los caminos simples."""
rng = random.Random(5)
names = list("abcdefg")
for _ in range(20):
links = {}
for i, left in enumerate(names):
for right in names[i + 1:]:
if rng.random() < 0.5:
links[(left, right)] = rng.randint(1, 6)
links[("a", "b")] = links.get(("a", "b"), 2)
def neighbors(node):
for (left, right), value in links.items():
if left == node:
yield right, value
elif right == node:
yield left, value
best = {}
def explore(node, cost, path):
if node != "a":
known = best.get(node)
if known is None or cost < known[0]:
best[node] = (cost, {path[1]})
elif cost == known[0]:
known[1].add(path[1])
for other, value in neighbors(node):
if other not in path:
explore(other, cost + value, path + [other])
explore("a", 0, ["a"])
with tempfile.NamedTemporaryFile("w", suffix=".txt") as graph:
graph.writelines(f"{u} {v} {c}\n" for (u, v), c in links.items())
graph.flush()
table = forwarding_table(tool("dijkstra", graph.name, "a").stdout)
for node, (cost, hops) in best.items():
self.assertEqual(table[node], (str(cost), hops), f"{links} hacia {node}")
def test_errors_are_reported(self):
self.assertNotEqual(tool("dijkstra", "graphs/six.txt", "z").returncode, 0)
missing = tool("dijkstra", "graphs/no_existe.txt", "a")
self.assertNotEqual(missing.returncode, 0)
self.assertIn("graphs/no_existe.txt", missing.stderr)
class DistanceVectorTest(unittest.TestCase):
def test_worked_example_rounds(self):
out = tool("dv_sim", "graphs/four.txt").stdout
self.assertIn("convergió: 2 rondas", out)
# Después de la primera ronda, w cree que z le cuesta 10; después de la segunda, 6.
first = out.split("ronda 1")[1].split("ronda 2")[0]
second = out.split("ronda 2")[1]
self.assertRegex(first, r"(?m)^w\s+0\s+2/x\s+5/x\s+10/x")
self.assertRegex(second, r"(?m)^w\s+0\s+2/x\s+5/x\s+6/x")
self.assertRegex(second, r"(?m)^z\s+6/y\s+4/y\s+1/y\s+0")
def test_converges_to_the_same_costs_as_dijkstra(self):
out = tool("dv_sim", "graphs/six.txt").stdout
last = out.split("ronda ")[-1]
row_a = re.search(r"(?m)^a\s+(.*)$", last).group(1).split()
costs = [cell.split("/")[0] for cell in row_a]
self.assertEqual(costs, ["0", "3", "1", "5", "6", "8"])
def test_count_to_infinity_in_a_line(self):
out = tool("dv_sim", "graphs/line.txt", "--dest", "z", "--fail", "y", "z",
"--order", "x:y,y:x").stdout
self.assertIn("ciclo: x > y > x", out)
self.assertGreaterEqual(rounds_of(out), 10)
self.assertEqual(final_costs(out, "xy"), {"x": "inf", "y": "inf"})
def test_split_horizon_and_poison_reverse_fix_the_line(self):
for mode in ("split", "poison"):
out = tool("dv_sim", "graphs/line.txt", "--dest", "z", "--fail", "y", "z",
"--order", "x:y,y:x", "--mode", mode).stdout
self.assertNotIn("ciclo", out, mode)
self.assertLessEqual(rounds_of(out), 2, mode)
def test_poison_reverse_does_not_fix_the_triangle(self):
for mode in ("split", "poison"):
out = tool("dv_sim", "graphs/tail.txt", "--dest", "d", "--fail", "c", "d",
"--mode", mode).stdout
self.assertGreaterEqual(rounds_of(out), 10, mode)
self.assertEqual(final_costs(out, "abc"), {"a": "inf", "b": "inf", "c": "inf"}, mode)
def test_loop_of_three_survives_poison_reverse(self):
out = tool("dv_sim", "graphs/tail.txt", "--dest", "d", "--fail", "c", "d",
"--mode", "poison", "--order", "c:a,b:a,a:c,c:b").stdout
self.assertIn("ciclo: a > b > c > a", out)
def test_good_news_fast_bad_news_slow(self):
good = tool("dv_sim", "graphs/triangle.txt", "--dest", "p", "--inf", "100",
"--set", "p", "q", "1").stdout
bad = tool("dv_sim", "graphs/triangle.txt", "--dest", "p", "--inf", "100",
"--set", "p", "q", "20").stdout
self.assertLessEqual(rounds_of(good), 2)
self.assertGreaterEqual(rounds_of(bad), 8)
self.assertEqual(final_costs(bad, "qr"), {"q": "20", "r": "21"})
def test_lie_pulls_traffic_toward_the_liar(self):
out = tool("dv_sim", "graphs/six.txt", "--dest", "a", "--lie", "e", "a", "0").stdout
self.assertRegex(out.splitlines()[-2], r"d=1/e\s+e=0\s+f=2/e")
def test_errors_are_reported(self):
self.assertNotEqual(tool("dv_sim", "graphs/no_existe.txt").returncode, 0)
self.assertNotEqual(tool("dv_sim", "graphs/line.txt", "--mode", "x").returncode, 0)
self.assertNotEqual(tool("dv_sim", "graphs/line.txt", "--fail", "y", "w").returncode, 0)
if __name__ == "__main__":
unittest.main()Laboratorio
Archivos de partida del laboratorio de la semana.
#!/bin/sh
# Paso 3 del laboratorio: cambien costos para que el tráfico entre h1 y h4 pase por
# r3 y r5, en las dos direcciones, sin apagar nada. Cuando terminen, borren la línea TODO.
set -e
conf() {
docker exec -i "clab-w05-ospf-$1" sh -c \
'cat > /tmp/fragment.conf && out=$(vtysh -f /tmp/fragment.conf 2>&1) || { echo "$out"; exit 1; }'
}
# TODO: el o los cambios de costo#!/bin/sh
# Paso 2 del laboratorio: OSPF en los cinco routers, con los costos de la tabla del
# capítulo. r1 ya está como ejemplo: lo que va entre las líneas CONFIG es el mismo texto
# que iría en su frr.conf. Se corre en el contenedor del curso, con ospf.clab.yml
# levantada. Cuando terminen, borren la línea TODO: make test usa este archivo.
set -e
conf() {
docker exec -i "clab-w05-ospf-$1" sh -c \
'cat > /tmp/fragment.conf && out=$(vtysh -f /tmp/fragment.conf 2>&1) || { echo "$out"; exit 1; }'
}
conf r1 <<'CONFIG'
interface eth1
ip ospf cost 2
interface eth2
ip ospf cost 3
router ospf
ospf router-id 10.205.0.1
log-adjacency-changes detail
passive-interface eth3
network 10.205.0.0/16 area 0
CONFIG
# TODO: r2, r3, r4 y r5# Paso 2: la tabla de r1, calculada a mano con Dijkstra antes de mirar el router.
# Una línea por destino: prefijo, costo y dirección del next hop. Si un destino
# empata, escriban una línea por cada next hop. Cuando terminen, borren la línea TODO.
# Ejemplo de la forma, con un destino que no existe:
# 10.205.99.0/24 7 10.205.12.2
#
# TODO: los loopback de r2 a r5 y la LAN de h4#!/bin/sh
# Paso 5 del laboratorio: Hello de 2 s y Dead de 8 s en todos los enlaces entre
# routers. Cuando terminen, borren la línea TODO.
set -e
conf() {
docker exec -i "clab-w05-ospf-$1" sh -c \
'cat > /tmp/fragment.conf && out=$(vtysh -f /tmp/fragment.conf 2>&1) || { echo "$out"; exit 1; }'
}
# TODO: los temporizadores de r1 a r5Compilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 5: routing dentro de una red. Se usa dentro del contenedor del curso.
#
# make compila dijkstra y dv_sim
# make test compila, levanta las tres topologías una tras otra y corre las
# pruebas (test_*.py); siempre destruye lo que levantó. Lo que depende
# del laboratorio usa lab/ y se salta hasta que lo completen
# make up-six levanta el grafo de seis nodos con OSPF (six/six.clab.yml)
# make up-ospf levanta la red del laboratorio, sin OSPF (ospf/ospf.clab.yml)
# make up-areas levanta la red de tres áreas (areas/areas.clab.yml)
# make up-range la misma red, con abr1 resumiendo el área 1
# make down destruye las tres topologías y sus redes de Docker
# make clean lo anterior, y borra los binarios
CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -O2 -g
PROGRAMS = dijkstra dv_sim
TOPOLOGIES = six ospf areas
.PHONY: all test up-six up-ospf up-areas up-range down clean
all: $(PROGRAMS)
%: %.c
$(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-%:
cd $* && containerlab deploy -t $*.clab.yml --reconfigure
up-range:
cd areas && ABR1_CONF=frr-range.conf containerlab deploy -t areas.clab.yml --reconfigure
down:
@for t in $(TOPOLOGIES); do \
(cd $$t && containerlab destroy -t $$t.clab.yml --cleanup >/dev/null 2>&1 || true); \
docker network rm redes-w05-$$t >/dev/null 2>&1 || true; \
done
clean: down
rm -f $(PROGRAMS)
rm -rf __pycache__ six/clab-w05-six ospf/clab-w05-ospf areas/clab-w05-areas