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:

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:

Lecturas complementarias:

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 neighbor y show 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.

  1. Levantar la topología y configurar OSPF en dos routers. Seguir con vtysh los estados de la adyacencia hasta llegar a Full.
  2. Terminar la configuración. Calcular a mano con Dijkstra la tabla de un router y compararla con show ip route ospf.
  3. Cambiar un costo para mover el tráfico por otro camino, y confirmarlo con traceroute.
  4. 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, con tc qdisc add dev ethN root netem loss 100% en los dos extremos, y comparar: ahora solo los temporizadores delatan la falla.
  5. Repetir con temporizadores más cortos y comparar. ¿Qué se gana y qué se arriesga?
  6. Provocar una adyacencia que no sube (por ejemplo, con áreas distintas) y diagnosticarla con show ip ospf interface.
  7. Avanzar con P1.

Preguntas de repaso

  1. Corran Dijkstra desde un nodo en un grafo dado y escriban su tabla de forwarding.
  2. Muestren con tres nodos en línea cómo aparece el conteo al infinito. ¿Por qué poison reverse no resuelve todos los casos?
  3. 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.
  4. Un enlace falla, y tres routers ya actualizaron su tabla y dos todavía no. ¿Qué pueden sufrir los paquetes en ese momento?
  5. ¿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.cDescargar dijkstra.c
/* 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.cDescargar dv_sim.c
/* 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;
}
netlab.pyDescargar netlab.py
"""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()
areas/areas.clab.ymlDescargar areas/areas.clab.yml
# 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
graphs/four.txtDescargar graphs/four.txt
# 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
graphs/line.txtDescargar graphs/line.txt
# Tres nodos en línea, para el conteo al infinito.
x y 1
y z 1
graphs/six.txtDescargar graphs/six.txt
# 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
graphs/tail.txtDescargar graphs/tail.txt
# 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
graphs/triangle.txtDescargar graphs/triangle.txt
# Triángulo con costos desiguales, para buenas y malas noticias.
p q 2
q r 1
p r 30
ospf/ospf.clab.ymlDescargar ospf/ospf.clab.yml
# 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
six/six.clab.ymlDescargar six/six.clab.yml
# 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: 1500
areas/abr1/frr-range.confDescargar areas/abr1/frr-range.conf
frr 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/20
areas/abr1/frr.confDescargar areas/abr1/frr.conf
frr 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
areas/abr2/frr.confDescargar areas/abr2/frr.conf
frr 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 2
areas/core/frr.confDescargar areas/core/frr.conf
frr 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 0
areas/x1/frr.confDescargar areas/x1/frr.conf
frr 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 1
areas/y1/frr.confDescargar areas/y1/frr.conf
frr 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 2
ospf/r1/frr.confDescargar ospf/r1/frr.conf
frr 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
!
ospf/r2/frr.confDescargar ospf/r2/frr.conf
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
!
ospf/r3/frr.confDescargar ospf/r3/frr.conf
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
!
ospf/r4/frr.confDescargar ospf/r4/frr.conf
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
!
ospf/r5/frr.confDescargar ospf/r5/frr.conf
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
!
six/a/frr.confDescargar six/a/frr.conf
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 0
six/b/frr.confDescargar six/b/frr.conf
frr 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 0
six/c/frr.confDescargar six/c/frr.conf
frr 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 0
six/d/frr.confDescargar six/d/frr.conf
frr 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 0
six/e/frr.confDescargar six/e/frr.conf
frr 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 0
six/f/frr.confDescargar six/f/frr.conf
frr 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 0

Pruebas

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

test_areas.pyDescargar test_areas.py
"""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()
test_ospf.pyDescargar test_ospf.py
"""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()
test_six.pyDescargar test_six.py
"""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()
test_tools.pyDescargar test_tools.py
"""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.

lab/cost.shDescargar lab/cost.sh
#!/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
lab/ospf.shDescargar lab/ospf.sh
#!/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
lab/r1_table.txtDescargar lab/r1_table.txt
# 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
lab/timers.shDescargar lab/timers.sh
#!/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 r5

Compilación

Cómo se compilan y ejecutan los ejemplos.

MakefileDescargar Makefile
# 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