Semana 6 — BGP y políticas entre redes

Objetivos

  • Explicar por qué entre sistemas autónomos manda la política y no el camino más corto.
  • Describir cómo se anuncian las rutas en BGP y cómo se elige la mejor.
  • Distinguir eBGP de iBGP.
  • Aplicar relaciones de cliente, proveedor y peer con políticas de importación y exportación.

Sesión A — BGP I: sistemas autónomos, eBGP e iBGP, atributos

Lecturas:

  • Kurose y Ross, secs. 5.4.1 (The Role of BGP), 5.4.2 (Advertising BGP Route Information) y 5.4.3 (Determining the Best Routes).
  • CS168, BGP y BGP Implementation and Issues.

Lecturas complementarias:

Temas:

  • Por qué existe BGP: alcanzar prefijos de otras redes, respetando la política de cada una.
  • Vector de caminos: el AS_PATH evita ciclos y expone el recorrido.
  • Sesiones sobre TCP: OPEN, UPDATE, KEEPALIVE y NOTIFICATION.
  • eBGP e iBGP: NEXT_HOP y cómo se relaciona BGP con OSPF dentro de un AS.
  • Elección de la mejor ruta: LOCAL_PREF, largo de AS_PATH, origen, MED, eBGP antes que iBGP e IGP más cercano (hot potato).
  • Error común: creer que BGP busca el camino de menor retardo. El AS_PATH más corto ni siquiera es el primer criterio, y no mide retardo.

Sesión B — BGP II: clientes, proveedores, peers y políticas

Lecturas:

  • Kurose y Ross, secs. 5.4.4 (IP-Anycast), 5.4.5 (Routing Policy) y 5.4.6 (Putting the Pieces Together).
  • CS168, BGP.
  • FRRouting, BGP.

Temas:

  • Quién le paga a quién: cliente, proveedor y peer.
  • Regla de importación: preferir rutas de clientes, luego de peers y por último de proveedores, porque así se gana o se ahorra dinero.
  • Regla de exportación: las rutas de clientes se anuncian a todos. Las de peers y proveedores, solo a clientes.
  • Caminos valle-libre: por qué estas reglas, si todos las cumplen y no hay ciclos de clientes y proveedores, llevan a una situación estable.
  • Políticas en FRRouting: route-map, prefix-list y local-preference.
  • Anycast: la misma dirección en muchos lugares.
  • Cómo una organización obtiene presencia en Internet: prefijo, número de AS, proveedores y DNS.

Laboratorio — Conectar sistemas autónomos con políticas

Trabajen en code/week-06/, con seis sistemas autónomos de números privados.

  1. Levantar la topología y abrir una sesión eBGP. Seguir con show bgp summary hasta que la sesión quede establecida. Capturar el OPEN y un UPDATE.
  2. Anunciar un prefijo propio y leer su AS_PATH desde otro AS con show bgp ipv4.
  3. Dejar todo sin políticas y encontrar un AS que da tránsito gratis a sus dos proveedores. Explicar el problema.
  4. Con route-map, marcar cada ruta según su origen y aplicar las reglas de importación y exportación. Volver a mirar el caso anterior.
  5. Hacer que un AS con dos proveedores prefiera uno usando local-preference, y comprobarlo con traceroute.
  6. Entregar P1 y leer el enunciado de P2.

Entregas

  • P1 entrega.
  • P2 publicado: Qhapaq Ñan (BGP con clientes, proveedores y peers). Entrega en semana 9. Ver projects/README.md.

Preguntas de repaso

  1. Un AS recibe dos rutas al mismo prefijo: una de un proveedor con AS_PATH corto y otra de un cliente con AS_PATH largo. ¿Cuál debería preferir y por qué?
  2. ¿A quién debe anunciar un AS una ruta que aprendió de un peer? Justifiquen en términos de dinero.
  3. ¿Por qué iBGP no vuelve a anunciar a otros routers iBGP una ruta aprendida por iBGP?
  4. Expliquen la diferencia entre hot potato y cold potato, y quién gana con cada una.
  5. Dibujen un camino que no sea valle-libre y expliquen qué AS está perdiendo dinero.

Código de la semana

Compilar y probar dentro del contenedor del curso con make y make test.

Ejemplos (27)Pruebas (5)Laboratorio (7)Compilación (1)

Ejemplos

Los programas que usan los apuntes y la clase.

netlab.pyDescargar netlab.py
"""Ayudas de las pruebas de la semana 6: levantar topologías y preguntarle a FRRouting."""
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=120, **options):
    return subprocess.run(args, capture_output=True, text=True, timeout=timeout, **options)


class Lab:
    """Una topología de Containerlab que se levanta y se destruye desde las pruebas."""

    def __init__(self, folder, name, network):
        self.folder = os.path.join(HERE, folder)
        self.topology = f"{folder}.clab.yml"
        self.prefix = f"clab-{name}-"
        self.network = network

    def deploy(self):
        done = run(["containerlab", "deploy", "-t", self.topology, "--reconfigure"],
                   timeout=300, cwd=self.folder)
        if done.returncode != 0:
            raise RuntimeError(done.stdout + done.stderr)

    def destroy(self):
        run(["containerlab", "destroy", "-t", self.topology, "--cleanup"],
            timeout=300, cwd=self.folder)
        run(["docker", "network", "rm", self.network])

    def out(self, node, *command, timeout=60):
        """Corre un comando en un nodo y devuelve stdout y stderr juntos."""
        done = run(["docker", "exec", self.prefix + node, *command], timeout=timeout)
        return done.stdout + done.stderr

    def ok(self, node, *command, timeout=60):
        """Corre un comando en un nodo y dice si terminó bien."""
        done = run(["docker", "exec", self.prefix + node, *command], timeout=timeout)
        return done.returncode == 0

    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; varios seguidos sirven para configurar."""
        arguments = []
        for command in commands:
            arguments += ["-c", command]
        return self.out(node, "vtysh", *arguments)

    def load_conf(self, node, path):
        """Carga un archivo de configuración en un router que ya está corriendo."""
        done = run(["docker", "cp", path, f"{self.prefix}{node}:/tmp/loaded.conf"])
        if done.returncode != 0:
            raise RuntimeError(done.stderr)
        done = run(["docker", "exec", self.prefix + node, "vtysh", "-f", "/tmp/loaded.conf"])
        if done.returncode != 0:
            raise RuntimeError(done.stdout + done.stderr)

    def bgp(self, node):
        """Devuelve la tabla BGP de un router: prefijo -> lista de caminos."""
        text = self.vtysh(node, "show bgp ipv4 json")
        return json.loads(text).get("routes", {})

    def best_path(self, node, prefix):
        """Devuelve el AS_PATH del mejor camino como lista de enteros, o None."""
        for path in self.bgp(node).get(prefix, []):
            if path.get("bestpath"):
                return [int(asn) for asn in path.get("path", "").split()]
        return None

    def advertised(self, node, neighbor):
        """Devuelve los prefijos que un router le anuncia a un vecino."""
        text = self.vtysh(node, f"show bgp ipv4 neighbors {neighbor} advertised-routes json")
        return set(json.loads(text).get("advertisedRoutes", {}))

    def sessions_up(self, node, count):
        """Dice si un router tiene al menos count sesiones en Established."""
        text = self.vtysh(node, "show bgp ipv4 summary json")
        peers = json.loads(text).get("ipv4Unicast", {}).get("peers", {})
        return sum(peer.get("state") == "Established" for peer in peers.values()) >= count

    def hops(self, node, target, source=None):
        """Devuelve las direcciones de un traceroute, salto por salto."""
        command = ["traceroute", "-n", "-w", "1", "-q", "1"]
        if source:
            command += ["-s", source]
        output = self.out(node, *command, target)
        hops = []
        for line in output.splitlines()[1:]:
            parts = line.split()
            if parts and parts[0].isdigit():
                hops.append(parts[1])
        return hops

    def assert_hops(self, test, node, target, expected, source=None, seconds=20):
        """Exige que el traceroute empiece con expected: el kernel tarda un poco tras BGP."""
        def matches():
            return self.hops(node, target, source=source)[:len(expected)] == list(expected)
        if not wait_until(matches, seconds=seconds):
            test.fail(f"{node} -> {target}: {self.hops(node, target, source=source)}, "
                      f"se esperaba {expected}")

    def wait_for_table(self, nodes, prefixes):
        """Espera a que cada nodo tenga una ruta a cada prefijo."""
        def ready():
            return all(self.best_path(node, prefix) is not None
                       for node in nodes for prefix in prefixes)
        return wait_until(ready, seconds=90, step=1)


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()
spvp.pyDescargar spvp.py
#!/usr/bin/env python3
"""Vector de caminos en miniatura: ¿llega a un estado estable, o gira para siempre?

Cada AS guarda un solo camino hacia un destino. Cuando le toca, mira lo que sus
vecinos le pueden ofrecer, descarta los caminos que ya lo contienen (así BGP evita
ciclos) y se queda con el que más le gusta. Si en una vuelta completa nadie cambia,
la red es estable. Si vuelve a un estado ya visto, en ese orden nunca se detendrá.

Uso:  python3 spvp.py         muestra los tres ejemplos del capítulo
"""
import random

# Las relaciones de los seis AS de ases/ases.clab.yml.
ASES_PROVIDERS = {          # cliente: sus proveedores
    64601: [64602, 64604],
    64602: [64605],
    64603: [64606],
    64604: [64606],
    64605: [],
    64606: [],
}
ASES_PEERS = [(64605, 64606)]


class Graph:
    """Relaciones entre AS: quién es proveedor de quién y quién es peer de quién."""

    def __init__(self, providers, peers=()):
        self.providers = {node: list(ups) for node, ups in providers.items()}
        self.peers = {frozenset(pair) for pair in peers}
        self.nodes = sorted(self.providers)

    def relation(self, node, other):
        """Qué es other para node: customer, peer, provider o None si no son vecinos."""
        if other in self.providers[node]:
            return "provider"
        if node in self.providers[other]:
            return "customer"
        if frozenset((node, other)) in self.peers:
            return "peer"
        return None

    def neighbors(self, node):
        return [other for other in self.nodes
                if other != node and self.relation(node, other) is not None]


def is_valley_free(graph, path):
    """Dice si un camino, leído del que lo usa al que lo originó, es valle-libre.

    El anuncio viaja del origen al primero de la lista. Subiendo de cliente a
    proveedor cualquier número de veces, a lo sumo un enlace de peers, y después
    solo bajando de proveedor a cliente.
    """
    climbing = True
    for sender, receiver in zip(reversed(path), list(reversed(path))[1:]):
        step = graph.relation(receiver, sender)     # qué es el que anuncia para el que recibe
        if step == "customer":                      # sube: el cliente le anuncia al proveedor
            if not climbing:
                return False
        elif step == "peer":
            if not climbing:
                return False
            climbing = False
        elif step == "provider":                    # baja: el proveedor le anuncia al cliente
            climbing = False
        else:
            return False
    return True


def gao_rexford_export(graph, node, path, receiver):
    """Rutas propias y de clientes, a todos. Las demás, solo a clientes."""
    if len(path) == 1 or graph.relation(node, path[1]) == "customer":
        return True
    return graph.relation(node, receiver) == "customer"


def gao_rexford_rank(graph, node, path):
    """Primero clientes, luego peers, luego proveedores. Después, el camino más corto."""
    order = {"customer": 2, "peer": 1, "provider": 0}
    return (order[graph.relation(node, path[1])], -len(path), -path[1])


def simulate(graph, destination, rank, export, order=None, rounds=200):
    """Activa los AS por turnos. Devuelve (estable, caminos, historia de estados)."""
    best = {node: None for node in graph.nodes}
    best[destination] = (destination,)
    order = order or [node for node in graph.nodes if node != destination]
    history = []
    seen = set()
    for _ in range(rounds):
        changed = False
        for node in order:
            candidates = []
            for other in graph.neighbors(node):
                offered = best[other]
                if offered is None or node in offered:
                    continue
                if not export(graph, other, offered, node):
                    continue
                candidates.append((node,) + offered)
            new = max(candidates, key=lambda path: rank(graph, node, path), default=None)
            if new != best[node]:
                best[node] = new
                changed = True
        state = tuple(sorted(best.items()))
        history.append(dict(best))
        if not changed:
            return True, best, history
        if state in seen:
            return False, best, history
        seen.add(state)
    return False, best, history


def ring_graph(cycle=True):
    """Tres AS y un destino 0, que es cliente de los tres.

    Con cycle=True, 1 es proveedor de 2, 2 de 3, y 3 de 1: un ciclo de proveedores.
    Con cycle=False, el último enlace se invierte: 1 es proveedor de 3 y no hay ciclo.
    """
    providers = {0: [1, 2, 3], 1: [], 2: [1], 3: [2]}
    if cycle:
        providers[1] = [3]
    else:
        providers[3] = [2, 1]
    return Graph(providers)


def ring_rank(graph, node, path):
    """Todos son clientes, así que cada AS es libre de ordenarlos: prefiere al del anillo."""
    return (gao_rexford_rank(graph, node, path)[0], path[1] != 0, -len(path))


def ases_graph():
    return Graph(ASES_PROVIDERS, ASES_PEERS)


def predicted_paths(destination):
    """Camino de cada AS hacia destination, si los seis cumplen las reglas de Gao y Rexford."""
    graph = ases_graph()
    stable, best, _ = simulate(graph, destination, gao_rexford_rank, gao_rexford_export)
    assert stable
    return best


def show_ring(cycle):
    graph = ring_graph(cycle)
    title = "con un ciclo de proveedores" if cycle else "sin ciclo de proveedores"
    print(f"Anillo {title}:")
    stable, _, history = simulate(graph, 0, ring_rank, gao_rexford_export, [1, 2, 3])
    for number, state in enumerate(history, start=1):
        paths = "   ".join(" ".join(map(str, state[node])) for node in (1, 2, 3))
        print(f"  vuelta {number}:  {paths}")
    print("  estable" if stable else "  repite un estado ya visto: en este orden no termina nunca")
    print()


def show_ases():
    graph = ases_graph()
    print("Seis AS con las reglas de Gao y Rexford (camino de cada uno hacia 64603):")
    best = predicted_paths(64603)
    for node in graph.nodes:
        print(f"  {node}: {' '.join(map(str, best[node][1:])) or '(origen)'}")
    others = [node for node in graph.nodes if node != 64603]
    results = set()
    for seed in range(50):
        order = others[:]
        random.Random(seed).shuffle(order)
        _, paths, _ = simulate(graph, 64603, gao_rexford_rank, gao_rexford_export, order)
        results.add(tuple(sorted(paths.items())))
    print(f"  con 50 órdenes de activación al azar: {len(results)} resultado distinto")


if __name__ == "__main__":
    show_ring(True)
    show_ring(False)
    show_ases()
ases/anycast.shDescargar ases/anycast.sh
#!/bin/sh
# Pone o quita una instancia de un servicio anycast en un router de los seis AS.
# Varios AS originan el mismo prefijo, 100.64.200.0/24, con la misma dirección de
# servicio, 100.64.200.1. El servicio responde en el puerto 5300 con su nombre.
#
# Uso:  ./anycast.sh r2 64602 on     desde el contenedor del curso
#       ./anycast.sh r2 64602 off
set -e
node="clab-w06-ases-$1"
asn="$2"
case "$3" in
on)
  docker exec "$node" ip addr add 100.64.200.1/32 dev lo
  docker exec -d "$node" sh -c 'nc -lk -p 5300 -e /bin/hostname'
  docker exec "$node" vtysh -c 'configure terminal' \
    -c 'ip route 100.64.200.0/24 Null0' \
    -c "router bgp $asn" -c 'address-family ipv4 unicast' -c 'network 100.64.200.0/24'
  ;;
off)
  docker exec "$node" vtysh -c 'configure terminal' \
    -c "router bgp $asn" -c 'address-family ipv4 unicast' -c 'no network 100.64.200.0/24' \
    -c 'exit' -c 'exit' -c 'no ip route 100.64.200.0/24 Null0'
  docker exec "$node" pkill -f 'nc -lk -p 5300' || true
  docker exec "$node" ip addr del 100.64.200.1/32 dev lo
  ;;
*)
  echo "uso: $0 <router> <as> on|off" >&2
  exit 1
  ;;
esac
ases/ases.clab.ymlDescargar ases/ases.clab.yml
# Seis sistemas autónomos, un router por AS. El router rN es el AS 6460N.
#
#                r5 (64605) ======== r6 (64606)        ===== peer
#                  |                  /      \         |     proveedor arriba,
#                  |                 /        \        |     cliente abajo
#              r2 (64602)       r4 (64604)   r3 (64603) --- h3
#                     \           /
#                      \         /
#                       r1 (64601) --- h1
#
# Cada AS es owner de 100.64.N.0/24 y lo anuncia entero. Dentro de ese bloque, la
# parte baja (100.64.N.0/25) es para hosts y la alta (100.64.N.128/25) para enlaces.
# Un enlace entre dos AS se numera con el bloque del proveedor; en el enlace de
# peers, con el de r5. 100.64.N.1 siempre es una dirección del router rN.
#
# Esta topología no tiene políticas: los frr.conf solo abren las sesiones.
name: w06-ases
mgmt:
  network: redes-w06-ases
  ipv4-subnet: 172.90.6.0/26
  ipv6-subnet: 3fff:172:90:6:1::/80
topology:
  defaults:
    kind: linux
    image: redes-router
    # Se borra la ruta por defecto de gestión (eth0): sin esto, lo que un nodo no
    # sabe a dónde mandar se iría por Docker en vez de fallar.
    exec:
      - ip route del default
      - ip -6 route del default
  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]
    r6:
      binds: [r6/frr.conf:/etc/frr/frr.conf:ro]
    h1:
      exec:
        - ip route del default
        - ip -6 route del default
        - ip addr add 100.64.1.10/25 dev eth1
        - ip route add default via 100.64.1.1
    h3:
      exec:
        - ip route del default
        - ip -6 route del default
        - ip addr add 100.64.3.10/25 dev eth1
        - ip route add default via 100.64.3.1
  # Containerlab usa MTU 9500 por defecto; aquí queremos el valor de Ethernet.
  links:
    - endpoints: ["r1:eth1", "h1:eth1"]
      mtu: 1500
    - endpoints: ["r1:eth2", "r2:eth1"]
      mtu: 1500
    - endpoints: ["r1:eth3", "r4:eth1"]
      mtu: 1500
    - endpoints: ["r2:eth2", "r5:eth1"]
      mtu: 1500
    - endpoints: ["r4:eth2", "r6:eth1"]
      mtu: 1500
    - endpoints: ["r5:eth2", "r6:eth2"]
      mtu: 1500
    - endpoints: ["r3:eth1", "r6:eth3"]
      mtu: 1500
    - endpoints: ["r3:eth2", "h3:eth1"]
      mtu: 1500
ibgp/ibgp.clab.ymlDescargar ibgp/ibgp.clab.yml
# Un AS de tránsito (65100) con cuatro routers en fila y un vecino (65200) que se
# conecta a los dos extremos. Sirve para ver iBGP, NEXT_HOP y hot potato.
#
#        +----- x (65200) ------+
#        |                      |
#       b1 ---- b2 ---- b3 ---- b4        AS 65100: OSPF e iBGP entre los cuatro
#
# Dentro del AS, OSPF lleva las direcciones de loopback (100.64.100.N/32) y los
# enlaces internos. Los dos enlaces hacia x (10.207.1.0/30 y 10.207.4.0/30) no
# están en OSPF: los routers del medio no saben llegar a ellos.
name: w06-ibgp
mgmt:
  network: redes-w06-ibgp
  ipv4-subnet: 172.90.6.64/26
  ipv6-subnet: 3fff:172:90:6:2::/80
topology:
  defaults:
    kind: linux
    image: redes-router
    exec:
      - ip route del default
      - ip -6 route del default
  nodes:
    b1:
      binds: [b1/frr.conf:/etc/frr/frr.conf:ro]
    b2:
      binds: [b2/frr.conf:/etc/frr/frr.conf:ro]
    b3:
      binds: [b3/frr.conf:/etc/frr/frr.conf:ro]
    b4:
      binds: [b4/frr.conf:/etc/frr/frr.conf:ro]
    x:
      binds: [x/frr.conf:/etc/frr/frr.conf:ro]
  links:
    - endpoints: ["b1:eth1", "b2:eth1"]
      mtu: 1500
    - endpoints: ["b2:eth2", "b3:eth1"]
      mtu: 1500
    - endpoints: ["b3:eth2", "b4:eth1"]
      mtu: 1500
    - endpoints: ["b1:eth2", "x:eth1"]
      mtu: 1500
    - endpoints: ["b4:eth2", "x:eth2"]
      mtu: 1500
ibgp/med_x.confDescargar ibgp/med_x.conf
! En x: pide que el tráfico hacia su prefijo entre por b4 (MED menor gana).
route-map MED_LOW permit 10
 set metric 10
route-map MED_HIGH permit 10
 set metric 50
router bgp 65200
 address-family ipv4 unicast
  neighbor 10.207.4.1 route-map MED_LOW out
  neighbor 10.207.1.1 route-map MED_HIGH out
 exit-address-family
ibgp/nhs_b1.confDescargar ibgp/nhs_b1.conf
! En b1: las rutas que pasa por iBGP llevan como NEXT_HOP su propia loopback,
! que los demás conocen por OSPF.
router bgp 65100
 address-family ipv4 unicast
  neighbor 100.64.100.2 next-hop-self
  neighbor 100.64.100.3 next-hop-self
  neighbor 100.64.100.4 next-hop-self
 exit-address-family
ibgp/nhs_b4.confDescargar ibgp/nhs_b4.conf
! En b4: lo mismo que en b1.
router bgp 65100
 address-family ipv4 unicast
  neighbor 100.64.100.1 next-hop-self
  neighbor 100.64.100.2 next-hop-self
  neighbor 100.64.100.3 next-hop-self
 exit-address-family
poli/poli.clab.ymlDescargar poli/poli.clab.yml
# Un AS mediano, m (65300), con un proveedor, un peer y un cliente. Sirve para
# aprender route-map, local-preference y comunidades, de a una pieza a la vez.
#
#       p (65310) ---- q (65320)        q también es cliente de p
#         |  proveedor  /  peer
#         |            /
#         +--- m (65300)
#               |  cliente
#              c (65330)
#
# Prefijos: m 100.64.130.0/24, p 100.64.131.0/24, q 100.64.132.0/24, c 100.64.133.0/24.
name: w06-poli
mgmt:
  network: redes-w06-poli
  ipv4-subnet: 172.90.6.128/26
  ipv6-subnet: 3fff:172:90:6:3::/80
topology:
  defaults:
    kind: linux
    image: redes-router
    exec:
      - ip route del default
      - ip -6 route del default
  nodes:
    m:
      binds: [m/frr.conf:/etc/frr/frr.conf:ro]
    p:
      binds: [p/frr.conf:/etc/frr/frr.conf:ro]
    q:
      binds: [q/frr.conf:/etc/frr/frr.conf:ro]
    c:
      binds: [c/frr.conf:/etc/frr/frr.conf:ro]
  links:
    - endpoints: ["m:eth1", "p:eth1"]
      mtu: 1500
    - endpoints: ["m:eth2", "q:eth1"]
      mtu: 1500
    - endpoints: ["m:eth3", "c:eth1"]
      mtu: 1500
    - endpoints: ["p:eth2", "q:eth2"]
      mtu: 1500
poli/step1_localpref.confDescargar poli/step1_localpref.conf
! Paso 1, en m: lo que llega de q vale más que el valor por defecto (100).
route-map FROM_Q permit 10
 set local-preference 150
router bgp 65300
 address-family ipv4 unicast
  neighbor 100.64.130.130 route-map FROM_Q in
 exit-address-family
poli/step2_community.confDescargar poli/step2_community.conf
! Paso 2, en m: lo que llega de c se marca con una comunidad.
route-map FROM_C permit 10
 set community 65300:30
router bgp 65300
 address-family ipv4 unicast
  neighbor 100.64.130.134 route-map FROM_C in
 exit-address-family
poli/step3_export.confDescargar poli/step3_export.conf
! Paso 3, en m: hacia p salen el prefijo propio y lo marcado en el paso 2.
ip prefix-list MINE seq 5 permit 100.64.130.0/24
bgp community-list standard TAGGED_C permit 65300:30
route-map TO_P permit 10
 match ip address prefix-list MINE
route-map TO_P permit 20
 match community TAGGED_C
router bgp 65300
 address-family ipv4 unicast
  neighbor 100.64.131.129 route-map TO_P out
 exit-address-family
ases/r1/frr.confDescargar ases/r1/frr.conf
frr defaults traditional
hostname r1
! AS 64601: cliente de r2 y de r4. Es un AS pequeño, sin clientes propios.
interface lo
 ip address 100.64.1.1/32
interface eth1
 description LAN de h1
 ip address 100.64.1.1/25
interface eth2
 description hacia r2 (proveedor)
 ip address 100.64.2.130/30
interface eth3
 description hacia r4 (proveedor)
 ip address 100.64.4.130/30
! La ruta a Null0 sostiene el anuncio: network solo anuncia lo que está en la tabla.
ip route 100.64.1.0/24 Null0
router bgp 64601
 bgp router-id 100.64.1.1
 ! Sin esto, FRRouting no intercambia rutas por eBGP si no hay políticas.
 no bgp ebgp-requires-policy
 neighbor 100.64.2.129 remote-as 64602
 neighbor 100.64.4.129 remote-as 64604
 address-family ipv4 unicast
  network 100.64.1.0/24
 exit-address-family
ases/r2/frr.confDescargar ases/r2/frr.conf
frr defaults traditional
hostname r2
! AS 64602: proveedor de r1 y cliente de r5.
interface lo
 ip address 100.64.2.1/32
interface eth1
 description hacia r1 (cliente)
 ip address 100.64.2.129/30
interface eth2
 description hacia r5 (proveedor)
 ip address 100.64.5.134/30
ip route 100.64.2.0/24 Null0
router bgp 64602
 bgp router-id 100.64.2.1
 no bgp ebgp-requires-policy
 neighbor 100.64.2.130 remote-as 64601
 neighbor 100.64.5.133 remote-as 64605
 address-family ipv4 unicast
  network 100.64.2.0/24
 exit-address-family
ases/r3/frr.confDescargar ases/r3/frr.conf
frr defaults traditional
hostname r3
! AS 64603: cliente de r6. Aquí vive el servidor h3.
interface lo
 ip address 100.64.3.1/32
interface eth1
 description hacia r6 (proveedor)
 ip address 100.64.6.134/30
interface eth2
 description LAN de h3
 ip address 100.64.3.1/25
ip route 100.64.3.0/24 Null0
router bgp 64603
 bgp router-id 100.64.3.1
 no bgp ebgp-requires-policy
 neighbor 100.64.6.133 remote-as 64606
 address-family ipv4 unicast
  network 100.64.3.0/24
 exit-address-family
ases/r4/frr.confDescargar ases/r4/frr.conf
frr defaults traditional
hostname r4
! AS 64604: proveedor de r1 y cliente de r6.
interface lo
 ip address 100.64.4.1/32
interface eth1
 description hacia r1 (cliente)
 ip address 100.64.4.129/30
interface eth2
 description hacia r6 (proveedor)
 ip address 100.64.6.130/30
ip route 100.64.4.0/24 Null0
router bgp 64604
 bgp router-id 100.64.4.1
 no bgp ebgp-requires-policy
 neighbor 100.64.4.130 remote-as 64601
 neighbor 100.64.6.129 remote-as 64606
 address-family ipv4 unicast
  network 100.64.4.0/24
 exit-address-family
ases/r5/frr.confDescargar ases/r5/frr.conf
frr defaults traditional
hostname r5
! AS 64605: proveedor de r2 y peer de r6.
interface lo
 ip address 100.64.5.1/32
interface eth1
 description hacia r2 (cliente)
 ip address 100.64.5.133/30
interface eth2
 description hacia r6 (peer)
 ip address 100.64.5.129/30
ip route 100.64.5.0/24 Null0
router bgp 64605
 bgp router-id 100.64.5.1
 no bgp ebgp-requires-policy
 neighbor 100.64.5.134 remote-as 64602
 neighbor 100.64.5.130 remote-as 64606
 address-family ipv4 unicast
  network 100.64.5.0/24
 exit-address-family
ases/r6/frr.confDescargar ases/r6/frr.conf
frr defaults traditional
hostname r6
! AS 64606: proveedor de r3 y de r4, y peer de r5.
interface lo
 ip address 100.64.6.1/32
interface eth1
 description hacia r4 (cliente)
 ip address 100.64.6.129/30
interface eth2
 description hacia r5 (peer)
 ip address 100.64.5.130/30
interface eth3
 description hacia r3 (cliente)
 ip address 100.64.6.133/30
ip route 100.64.6.0/24 Null0
router bgp 64606
 bgp router-id 100.64.6.1
 no bgp ebgp-requires-policy
 neighbor 100.64.6.130 remote-as 64604
 neighbor 100.64.5.129 remote-as 64605
 neighbor 100.64.6.134 remote-as 64603
 address-family ipv4 unicast
  network 100.64.6.0/24
 exit-address-family
ibgp/b1/frr.confDescargar ibgp/b1/frr.conf
frr defaults traditional
hostname b1
! AS 65100, router b1.
interface lo
 ip address 100.64.100.1/32
 ip ospf area 0
interface eth1
 description hacia b2
 ip address 10.207.12.1/30
 ip ospf area 0
 ip ospf network point-to-point
interface eth2
 description hacia x
 ip address 10.207.1.1/30
! Los routers de borde anuncian el bloque del AS.
ip route 100.64.100.0/24 Null0
router ospf
 ospf router-id 100.64.100.1
router bgp 65100
 bgp router-id 100.64.100.1
 no bgp ebgp-requires-policy
 ! Malla completa de iBGP: una sesión con cada otro router, entre loopbacks.
 ! Las sesiones esperan a OSPF: se reintenta cada 5 s, en vez de los 120 s por defecto.
 neighbor 100.64.100.2 remote-as 65100
 neighbor 100.64.100.2 update-source lo
 neighbor 100.64.100.2 timers connect 5
 neighbor 100.64.100.3 remote-as 65100
 neighbor 100.64.100.3 update-source lo
 neighbor 100.64.100.3 timers connect 5
 neighbor 100.64.100.4 remote-as 65100
 neighbor 100.64.100.4 update-source lo
 neighbor 100.64.100.4 timers connect 5
 neighbor 10.207.1.2 remote-as 65200
 address-family ipv4 unicast
  network 100.64.100.0/24
 exit-address-family
ibgp/b2/frr.confDescargar ibgp/b2/frr.conf
frr defaults traditional
hostname b2
! AS 65100, router b2.
interface lo
 ip address 100.64.100.2/32
 ip ospf area 0
interface eth1
 description hacia b1
 ip address 10.207.12.2/30
 ip ospf area 0
 ip ospf network point-to-point
interface eth2
 description hacia b3
 ip address 10.207.23.1/30
 ip ospf area 0
 ip ospf network point-to-point
router ospf
 ospf router-id 100.64.100.2
router bgp 65100
 bgp router-id 100.64.100.2
 no bgp ebgp-requires-policy
 ! Malla completa de iBGP: una sesión con cada otro router, entre loopbacks.
 ! Las sesiones esperan a OSPF: se reintenta cada 5 s, en vez de los 120 s por defecto.
 neighbor 100.64.100.1 remote-as 65100
 neighbor 100.64.100.1 update-source lo
 neighbor 100.64.100.1 timers connect 5
 neighbor 100.64.100.3 remote-as 65100
 neighbor 100.64.100.3 update-source lo
 neighbor 100.64.100.3 timers connect 5
 neighbor 100.64.100.4 remote-as 65100
 neighbor 100.64.100.4 update-source lo
 neighbor 100.64.100.4 timers connect 5
ibgp/b3/frr.confDescargar ibgp/b3/frr.conf
frr defaults traditional
hostname b3
! AS 65100, router b3.
interface lo
 ip address 100.64.100.3/32
 ip ospf area 0
interface eth1
 description hacia b2
 ip address 10.207.23.2/30
 ip ospf area 0
 ip ospf network point-to-point
interface eth2
 description hacia b4
 ip address 10.207.34.1/30
 ip ospf area 0
 ip ospf network point-to-point
router ospf
 ospf router-id 100.64.100.3
router bgp 65100
 bgp router-id 100.64.100.3
 no bgp ebgp-requires-policy
 ! Malla completa de iBGP: una sesión con cada otro router, entre loopbacks.
 ! Las sesiones esperan a OSPF: se reintenta cada 5 s, en vez de los 120 s por defecto.
 neighbor 100.64.100.1 remote-as 65100
 neighbor 100.64.100.1 update-source lo
 neighbor 100.64.100.1 timers connect 5
 neighbor 100.64.100.2 remote-as 65100
 neighbor 100.64.100.2 update-source lo
 neighbor 100.64.100.2 timers connect 5
 neighbor 100.64.100.4 remote-as 65100
 neighbor 100.64.100.4 update-source lo
 neighbor 100.64.100.4 timers connect 5
ibgp/b4/frr.confDescargar ibgp/b4/frr.conf
frr defaults traditional
hostname b4
! AS 65100, router b4.
interface lo
 ip address 100.64.100.4/32
 ip ospf area 0
interface eth1
 description hacia b3
 ip address 10.207.34.2/30
 ip ospf area 0
 ip ospf network point-to-point
interface eth2
 description hacia x
 ip address 10.207.4.1/30
! Los routers de borde anuncian el bloque del AS.
ip route 100.64.100.0/24 Null0
router ospf
 ospf router-id 100.64.100.4
router bgp 65100
 bgp router-id 100.64.100.4
 no bgp ebgp-requires-policy
 ! Malla completa de iBGP: una sesión con cada otro router, entre loopbacks.
 ! Las sesiones esperan a OSPF: se reintenta cada 5 s, en vez de los 120 s por defecto.
 neighbor 100.64.100.1 remote-as 65100
 neighbor 100.64.100.1 update-source lo
 neighbor 100.64.100.1 timers connect 5
 neighbor 100.64.100.2 remote-as 65100
 neighbor 100.64.100.2 update-source lo
 neighbor 100.64.100.2 timers connect 5
 neighbor 100.64.100.3 remote-as 65100
 neighbor 100.64.100.3 update-source lo
 neighbor 100.64.100.3 timers connect 5
 neighbor 10.207.4.2 remote-as 65200
 address-family ipv4 unicast
  network 100.64.100.0/24
 exit-address-family
ibgp/x/frr.confDescargar ibgp/x/frr.conf
frr defaults traditional
hostname x
! AS 65200: se conecta al AS 65100 en sus dos extremos.
interface lo
 ip address 100.64.120.1/32
interface eth1
 description hacia b1
 ip address 10.207.1.2/30
interface eth2
 description hacia b4
 ip address 10.207.4.2/30
ip route 100.64.120.0/24 Null0
router bgp 65200
 bgp router-id 100.64.120.1
 no bgp ebgp-requires-policy
 neighbor 10.207.1.1 remote-as 65100
 neighbor 10.207.4.1 remote-as 65100
 address-family ipv4 unicast
  network 100.64.120.0/24
 exit-address-family
poli/c/frr.confDescargar poli/c/frr.conf
frr defaults traditional
hostname c
! AS 65330: cliente de m.
interface lo
 ip address 100.64.133.1/32
interface eth1
 description hacia m
 ip address 100.64.130.134/30
ip route 100.64.133.0/24 Null0
router bgp 65330
 bgp router-id 100.64.133.1
 no bgp ebgp-requires-policy
 neighbor 100.64.130.133 remote-as 65300
 address-family ipv4 unicast
  network 100.64.133.0/24
 exit-address-family
poli/m/frr.confDescargar poli/m/frr.conf
frr defaults traditional
hostname m
! AS 65300: el AS de este ejemplo.
interface lo
 ip address 100.64.130.1/32
interface eth1
 description hacia p (proveedor)
 ip address 100.64.131.130/30
interface eth2
 description hacia q (peer)
 ip address 100.64.130.129/30
interface eth3
 description hacia c (cliente)
 ip address 100.64.130.133/30
ip route 100.64.130.0/24 Null0
router bgp 65300
 bgp router-id 100.64.130.1
 no bgp ebgp-requires-policy
 neighbor 100.64.131.129 remote-as 65310
 neighbor 100.64.130.130 remote-as 65320
 neighbor 100.64.130.134 remote-as 65330
 address-family ipv4 unicast
  network 100.64.130.0/24
 exit-address-family
poli/p/frr.confDescargar poli/p/frr.conf
frr defaults traditional
hostname p
! AS 65310: proveedor de m y de q.
interface lo
 ip address 100.64.131.1/32
interface eth1
 description hacia m
 ip address 100.64.131.129/30
interface eth2
 description hacia q
 ip address 100.64.131.133/30
ip route 100.64.131.0/24 Null0
router bgp 65310
 bgp router-id 100.64.131.1
 no bgp ebgp-requires-policy
 neighbor 100.64.131.130 remote-as 65300
 neighbor 100.64.131.134 remote-as 65320
 address-family ipv4 unicast
  network 100.64.131.0/24
 exit-address-family
poli/q/frr.confDescargar poli/q/frr.conf
frr defaults traditional
hostname q
! AS 65320: peer de m y cliente de p.
interface lo
 ip address 100.64.132.1/32
interface eth1
 description hacia m
 ip address 100.64.130.130/30
interface eth2
 description hacia p
 ip address 100.64.131.134/30
ip route 100.64.132.0/24 Null0
router bgp 65320
 bgp router-id 100.64.132.1
 no bgp ebgp-requires-policy
 neighbor 100.64.130.129 remote-as 65300
 neighbor 100.64.131.133 remote-as 65310
 address-family ipv4 unicast
  network 100.64.132.0/24
 exit-address-family

Pruebas

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

test_ases.pyDescargar test_ases.py
"""Pruebas de los seis AS sin políticas (ases/ases.clab.yml): sesiones, mensajes y el problema."""
import os
import shutil
import subprocess
import tempfile
import time
import unittest

import spvp
from netlab import HERE, Lab, run, wait_until

LAB = Lab("ases", "w06-ases", "redes-w06-ases")
ROUTERS = ["r1", "r2", "r3", "r4", "r5", "r6"]
PREFIXES = [f"100.64.{n}.0/24" for n in range(1, 7)]
SESSIONS = {"r1": 2, "r2": 2, "r3": 1, "r4": 2, "r5": 2, "r6": 3}


def setUpModule():
    unittest.addModuleCleanup(LAB.destroy)
    LAB.deploy()
    if not LAB.wait_for_table(ROUTERS, PREFIXES):
        raise RuntimeError("la topología no quedó lista")


class A_Sessions(unittest.TestCase):
    def test_every_session_is_established(self):
        for node, count in SESSIONS.items():
            self.assertTrue(wait_until(lambda n=node, c=count: LAB.sessions_up(n, c), seconds=30), node)

    def test_as_path_lists_who_the_route_crossed(self):
        self.assertEqual(LAB.best_path("r3", "100.64.3.0/24"), [])
        self.assertEqual(LAB.best_path("r1", "100.64.3.0/24"), [64604, 64606, 64603])
        self.assertEqual(LAB.best_path("r5", "100.64.3.0/24"), [64606, 64603])

    def test_shortest_as_path_decides_and_the_router_says_so(self):
        text = LAB.vtysh("r1", "show bgp ipv4 100.64.3.0/24")
        self.assertIn("64602 64605 64606 64603", text)
        self.assertIn("best (AS Path)", text)

    def test_hosts_reach_each_other(self):
        self.assertTrue(wait_until(lambda: LAB.ok("h1", "ping", "-c", "1", "-W", "1", "100.64.3.10")))

    def test_unknown_destination_fails(self):
        # Sin ruta por defecto, lo desconocido falla en vez de escaparse por Docker.
        self.assertFalse(LAB.ok("r1", "ping", "-c", "1", "-W", "1", "198.51.100.1"))


class B_Messages(unittest.TestCase):
    """Se apaga una sesión, se captura mientras vuelve y se leen los mensajes."""

    @classmethod
    def setUpClass(cls):
        peer = "100.64.2.129"
        LAB.vtysh("r1", "configure terminal", "router bgp 64601", f"neighbor {peer} shutdown")
        time.sleep(1)
        LAB.daemon("r1", "timeout 14 tcpdump -U -i eth2 -w /tmp/session.pcap tcp port 179")
        time.sleep(1.5)
        LAB.vtysh("r1", "configure terminal", "router bgp 64601", f"no neighbor {peer} shutdown")
        wait_until(lambda: LAB.sessions_up("r1", 2), seconds=20)
        time.sleep(4)
        LAB.vtysh("r1", "configure terminal", "router bgp 64601", f"neighbor {peer} shutdown")
        time.sleep(1.5)
        LAB.vtysh("r1", "configure terminal", "router bgp 64601", f"no neighbor {peer} shutdown")
        wait_until(lambda: LAB.sessions_up("r1", 2), seconds=20)
        time.sleep(1)
        LAB.out("r1", "pkill", "tcpdump")
        time.sleep(1)
        cls.folder = tempfile.mkdtemp(dir=HERE, prefix=".capture-")
        cls.pcap = os.path.join(cls.folder, "session.pcap")
        done = run(["docker", "cp", f"{LAB.prefix}r1:/tmp/session.pcap", cls.pcap])
        if done.returncode != 0:
            raise RuntimeError(done.stderr)

    @classmethod
    def tearDownClass(cls):
        shutil.rmtree(cls.folder, ignore_errors=True)

    def fields(self, display_filter, *names):
        command = ["tshark", "-r", self.pcap, "-Y", display_filter, "-T", "fields"]
        for name in names:
            command += ["-e", name]
        done = subprocess.run(command, capture_output=True, text=True, timeout=60)
        return [line.split("\t") for line in done.stdout.splitlines() if line.strip()]

    def test_tcp_port_179(self):
        self.assertTrue(self.fields("tcp.flags.syn == 1 and tcp.flags.ack == 0 and tcp.dstport == 179",
                                    "ip.src"))

    def test_open_from_both_sides_with_as_and_hold_time(self):
        rows = self.fields("bgp.type == 1", "ip.src", "bgp.open.myas", "bgp.open.holdtime",
                           "bgp.open.version", "bgp.open.identifier")
        by_source = {row[0]: row for row in rows}
        self.assertEqual(by_source["100.64.2.129"][1:], ["64602", "180", "4", "100.64.2.1"])
        self.assertEqual(by_source["100.64.2.130"][1:], ["64601", "180", "4", "100.64.1.1"])

    def test_bgp_messages_travel_with_ttl_1_on_ebgp(self):
        rows = self.fields("bgp", "ip.ttl")
        self.assertTrue(rows)
        self.assertEqual({row[0] for row in rows}, {"1"})

    def test_keepalive_is_only_a_header(self):
        rows = self.fields("bgp.type == 4", "bgp.length")
        self.assertTrue(rows)
        self.assertEqual({row[0] for row in rows}, {"19"})

    def test_update_carries_path_and_next_hop(self):
        done = subprocess.run(["tshark", "-r", self.pcap, "-Y", "bgp.type == 2 and ip.src == 100.64.2.130",
                               "-O", "bgp"], capture_output=True, text=True, timeout=60)
        self.assertIn("AS_PATH: 64601", done.stdout)
        self.assertIn("NEXT_HOP: 100.64.2.130", done.stdout)
        self.assertIn("100.64.1.0/24", done.stdout)

    def test_loop_prevention_a_route_with_my_own_as_is_ignored(self):
        output = LAB.out("r1", "tcpdump", "-nr", "/tmp/session.pcap", "-vv", "src host 100.64.2.129")
        self.assertRegex(output, r"AS Path[^\n]*64601")
        for path in LAB.bgp("r1").get("100.64.1.0/24", []):
            self.assertNotIn("64601", path.get("path", ""))

    def test_shutdown_sends_notification_cease(self):
        rows = self.fields("bgp.type == 3", "bgp.notify.major_error")
        self.assertTrue(rows)
        self.assertEqual(rows[0][0], "6")


class C_FreeTransit(unittest.TestCase):
    """El paso 3 del laboratorio: r1, el AS más chico, le da tránsito a sus dos proveedores."""

    @classmethod
    def setUpClass(cls):
        LAB.wait_for_table(ROUTERS, PREFIXES)
        wait_until(lambda: LAB.best_path("r2", "100.64.4.0/24") == [64601, 64604], seconds=30)

    def test_r2_reaches_its_neighbour_provider_through_its_customer(self):
        self.assertEqual(LAB.best_path("r2", "100.64.4.0/24"), [64601, 64604])
        self.assertEqual(LAB.best_path("r4", "100.64.2.0/24"), [64601, 64602])

    def test_the_path_has_a_valley(self):
        graph = spvp.ases_graph()
        self.assertFalse(spvp.is_valley_free(graph, [64602, 64601, 64604]))

    def test_r1_announces_each_provider_to_the_other(self):
        self.assertIn("100.64.4.0/24", LAB.advertised("r1", "100.64.2.129"))
        self.assertIn("100.64.2.0/24", LAB.advertised("r1", "100.64.4.129"))

    def test_traffic_really_crosses_r1(self):
        LAB.assert_hops(self, "r2", "100.64.4.1", ["100.64.2.130", "100.64.4.1"], source="100.64.2.1")

    def test_a_host_uses_the_shorter_of_its_two_providers(self):
        LAB.assert_hops(self, "h1", "100.64.3.10",
                        ["100.64.1.1", "100.64.4.129", "100.64.6.129", "100.64.6.134", "100.64.3.10"])


class D_Failure(unittest.TestCase):
    def test_withdraw_moves_traffic_and_recovery_brings_it_back(self):
        self.addCleanup(LAB.out, "r1", "ip", "link", "set", "eth3", "up")
        LAB.out("r1", "ip", "link", "set", "eth3", "down")
        moved = lambda: LAB.best_path("r1", "100.64.3.0/24") == [64602, 64605, 64606, 64603]
        self.assertTrue(wait_until(moved, seconds=20))
        self.assertTrue(wait_until(lambda: LAB.ok("h1", "ping", "-c", "1", "-W", "1", "100.64.3.10"),
                                   seconds=15))
        LAB.out("r1", "ip", "link", "set", "eth3", "up")
        back = lambda: LAB.best_path("r1", "100.64.3.0/24") == [64604, 64606, 64603]
        self.assertTrue(wait_until(back, seconds=60))


class E_Anycast(unittest.TestCase):
    """Dos AS originan el mismo prefijo. Cada quien llega a la instancia más cercana."""
    SCRIPT = os.path.join(HERE, "ases", "anycast.sh")

    def answer(self, node):
        return LAB.out(node, "sh", "-c", "nc -w 2 100.64.200.1 5300 </dev/null").strip()

    def test_each_client_reaches_the_closest_and_moves_on_withdrawal(self):
        def toggle(router, asn, action):
            done = run([self.SCRIPT, router, asn, action], cwd=HERE)
            self.assertEqual(done.returncode, 0, done.stderr)

        self.addCleanup(subprocess.run, [self.SCRIPT, "r6", "64606", "off"], capture_output=True)
        toggle("r2", "64602", "on")
        toggle("r6", "64606", "on")
        prefix = "100.64.200.0/24"
        self.assertTrue(wait_until(lambda: LAB.best_path("r3", prefix) == [64606] and
                                   LAB.best_path("r1", prefix) == [64602], seconds=30))
        self.assertTrue(wait_until(lambda: self.answer("h1") == "r2", seconds=15))
        self.assertTrue(wait_until(lambda: self.answer("h3") == "r6", seconds=15))
        toggle("r2", "64602", "off")
        self.assertTrue(wait_until(lambda: self.answer("h1") == "r6", seconds=30))
        self.assertEqual(self.answer("h3"), "r6")


if __name__ == "__main__":
    unittest.main()
test_ibgp.pyDescargar test_ibgp.py
"""Pruebas de iBGP, NEXT_HOP, hot potato y MED (ibgp/ibgp.clab.yml)."""
import os
import unittest

from netlab import HERE, Lab, wait_until

LAB = Lab("ibgp", "w06-ibgp", "redes-w06-ibgp")
PREFIX = "100.64.120.0/24"
BORDER_B1 = "100.64.100.1"
BORDER_B4 = "100.64.100.4"


def conf(name):
    return os.path.join(HERE, "ibgp", name)


def setUpModule():
    unittest.addModuleCleanup(LAB.destroy)
    LAB.deploy()
    ready = lambda: all(LAB.sessions_up(f"b{n}", 4 if n in (1, 4) else 3) for n in range(1, 5))
    if not wait_until(ready, seconds=120, step=2):
        raise RuntimeError("la topología no quedó lista")


def show(node):
    return LAB.vtysh(node, f"show bgp ipv4 {PREFIX}")


class A_NextHop(unittest.TestCase):
    def test_ibgp_keeps_the_next_hop_and_the_route_is_useless(self):
        ok = wait_until(lambda: show("b2").count("inaccessible") == 2, seconds=30)
        self.assertTrue(ok, show("b2"))
        self.assertIn("10.207.1.2 (inaccessible", show("b2"))
        self.assertIn("no best path", show("b2"))
        self.assertNotIn("100.64.120.0/24", LAB.out("b2", "ip", "route"))

    def test_bgp_routes_do_not_go_into_ospf(self):
        self.assertNotIn("100.64.120", LAB.vtysh("b2", "show ip ospf route"))
        self.assertIn("100.64.100.1/32", LAB.vtysh("b2", "show ip ospf route"))

    def test_ebgp_border_sees_the_route_fine(self):
        self.assertIn("best", show("b1"))


class B_HotPotato(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        LAB.load_conf("b1", conf("nhs_b1.conf"))
        LAB.load_conf("b4", conf("nhs_b4.conf"))
        wait_until(lambda: BORDER_B1 in show("b2") and BORDER_B4 in show("b2")
                   and BORDER_B1 in show("b3") and BORDER_B4 in show("b3"), seconds=30)

    def test_next_hop_self_puts_the_border_loopback(self):
        self.assertNotIn("inaccessible", show("b2"))

    def test_each_router_goes_to_its_closest_exit(self):
        for node, border, first_hop in (("b2", BORDER_B1, "10.207.12.1"),
                                        ("b3", BORDER_B4, "10.207.34.2")):
            text = show(node)
            best = [block for block in text.split("\n  65200") if "best (IGP Metric)" in block]
            self.assertEqual(len(best), 1, text)
            self.assertIn(border, best[0])
            source = "100.64.100." + node[1]
            LAB.assert_hops(self, node, "100.64.120.1", [first_hop], source=source)

    def test_border_prefers_its_own_ebgp_route(self):
        self.assertIn("best (Peer Type)", show("b1"))


class C_Mesh(unittest.TestCase):
    """iBGP no reenvía a iBGP: si a b3 solo le queda la sesión con b2, se queda sin la ruta."""

    def test_route_learned_by_ibgp_is_not_passed_to_ibgp(self):
        peers = ("100.64.100.1", "100.64.100.4")
        off = ["configure terminal", "router bgp 65100"] + [f"neighbor {p} shutdown" for p in peers]
        on = ["configure terminal", "router bgp 65100"] + [f"no neighbor {p} shutdown" for p in peers]
        self.addCleanup(lambda: wait_until(lambda: LAB.sessions_up("b3", 3), seconds=60))
        self.addCleanup(LAB.vtysh, "b3", *on)
        LAB.vtysh("b3", *off)
        self.assertTrue(wait_until(lambda: "Network not in table" in show("b3"), seconds=15))
        self.assertIn("best", show("b2"))
        self.assertNotIn(PREFIX, LAB.advertised("b2", "100.64.100.3"))


class D_Med(unittest.TestCase):
    """x pide entrar por b4 con un MED menor. MED se compara antes que la distancia."""

    def test_med_beats_hot_potato_and_ebgp_preference(self):
        wait_until(lambda: LAB.sessions_up("b3", 3), seconds=60)
        wait_until(lambda: "best (IGP Metric)" in show("b2"), seconds=30)
        LAB.load_conf("x", conf("med_x.conf"))
        # b1 ya compara el MED: su mejor camino pasa a ser el de b4, aunque tenga un enlace directo.
        self.assertTrue(wait_until(lambda: "best (MED)" in show("b1"), seconds=30), show("b1"))
        LAB.assert_hops(self, "b1", "100.64.120.1", ["10.207.12.2"], source="100.64.100.1")
        # Y como b1 solo pasa por iBGP su mejor camino si es externo, b2 se queda con uno solo.
        self.assertTrue(wait_until(lambda: BORDER_B1 not in show("b2"), seconds=30), show("b2"))
        self.assertIn(BORDER_B4, show("b2"))
        LAB.assert_hops(self, "b2", "100.64.120.1", ["10.207.23.2", "10.207.34.2"],
                        source="100.64.100.2")


if __name__ == "__main__":
    unittest.main()
test_lab.pyDescargar test_lab.py
"""Pruebas del laboratorio: las políticas de lab/ sobre los seis AS (se saltan si hay TODO)."""
import unittest

import spvp
from netlab import Lab, answer_path, wait_until

LAB = Lab("ases", "w06-ases", "redes-w06-ases")
ROUTERS = {f"r{n}": 64600 + n for n in range(1, 7)}
PREFIXES = {64600 + n: f"100.64.{n}.0/24" for n in range(1, 7)}
GRAPH = spvp.ases_graph()


def full_path(node, as_path):
    return [ROUTERS[node]] + as_path


def setUpModule():
    # Antes de levantar nada: sin archivos listos no hay qué probar.
    for name in [f"r{n}.conf" for n in range(1, 7)] + ["prefer.conf"]:
        answer_path(name)
    unittest.addModuleCleanup(LAB.destroy)
    LAB.deploy()
    if not LAB.wait_for_table(ROUTERS, PREFIXES.values()):
        raise RuntimeError("la topología no quedó lista")
    for number in range(1, 7):
        LAB.load_conf(f"r{number}", answer_path(f"r{number}.conf"))
    if not wait_until(tables_match_the_prediction, seconds=60, step=2):
        raise RuntimeError("las tablas no llegaron a lo esperado:\n" + describe_tables())
    # Un route-map nuevo tarda unos segundos en aplicarse a lo que ya estaba en la tabla.
    if not wait_until(every_route_is_tagged, seconds=60, step=2):
        raise RuntimeError("hay rutas sin comunidad: marquen cada ruta según su origen")


def expected(node, origin):
    best = spvp.predicted_paths(origin)[ROUTERS[node]]
    return list(best[1:])


def tables_match_the_prediction():
    return all(LAB.best_path(node, PREFIXES[origin]) == expected(node, origin)
               for node in ROUTERS for origin in PREFIXES)


def every_route_is_tagged():
    for node, asn in ROUTERS.items():
        for prefix in PREFIXES.values():
            if LAB.best_path(node, prefix) in (None, []):
                continue
            if f"Community: {asn}:" not in LAB.vtysh(node, f"show bgp ipv4 {prefix}"):
                return False
    return True


def describe_tables():
    lines = []
    for node in ROUTERS:
        for origin, prefix in PREFIXES.items():
            lines.append(f"{node} {prefix}: {LAB.best_path(node, prefix)}, "
                         f"se esperaba {expected(node, origin)}")
    return "\n".join(lines)


class A_Export(unittest.TestCase):
    def test_small_as_announces_only_its_own_prefix(self):
        self.assertEqual(LAB.advertised("r1", "100.64.2.129"), {"100.64.1.0/24"})
        self.assertEqual(LAB.advertised("r1", "100.64.4.129"), {"100.64.1.0/24"})

    def test_no_as_passes_a_peer_or_provider_route_upward(self):
        self.assertEqual(LAB.advertised("r2", "100.64.5.133"), {"100.64.1.0/24", "100.64.2.0/24"})
        self.assertEqual(LAB.advertised("r4", "100.64.6.129"), {"100.64.1.0/24", "100.64.4.0/24"})
        self.assertEqual(LAB.advertised("r3", "100.64.6.133"), {"100.64.3.0/24"})

    def test_peers_exchange_only_customers_and_own(self):
        self.assertEqual(LAB.advertised("r5", "100.64.5.130"),
                         {"100.64.1.0/24", "100.64.2.0/24", "100.64.5.0/24"})
        self.assertEqual(LAB.advertised("r6", "100.64.5.129"),
                         {"100.64.1.0/24", "100.64.3.0/24", "100.64.4.0/24", "100.64.6.0/24"})

    def test_customers_receive_everything(self):
        self.assertEqual(LAB.advertised("r6", "100.64.6.134") | {"100.64.3.0/24"},
                         set(PREFIXES.values()))


class B_Import(unittest.TestCase):
    def local_pref(self, node, prefix, as_path):
        """El LOCAL_PREF de un camino. Si nadie lo cambió, vale 100."""
        for path in LAB.bgp(node)[prefix]:
            if path.get("path") == as_path:
                return path.get("locPrf", 100)
        self.fail(f"{node} no tiene el camino {as_path} hacia {prefix}")

    def test_routes_are_tagged_with_a_community_of_the_receiver(self):
        self.assertTrue(every_route_is_tagged())

    def test_customer_beats_peer_even_with_longer_or_equal_path(self):
        text = LAB.vtysh("r6", "show bgp ipv4 100.64.1.0/24")
        self.assertIn("64605 64602 64601", text, "r6 también debe conocer el camino por su peer")
        self.assertEqual(LAB.best_path("r6", "100.64.1.0/24"), [64604, 64601])
        self.assertGreater(self.local_pref("r6", "100.64.1.0/24", "64604 64601"),
                           self.local_pref("r6", "100.64.1.0/24", "64605 64602 64601"))

    def test_customer_above_peer_above_provider(self):
        # Cada valor se mira donde esa relación existe: r5 tiene cliente y peer, y r2 proveedor.
        customer = self.local_pref("r5", "100.64.2.0/24", "64602")
        peer = self.local_pref("r5", "100.64.6.0/24", "64606")
        provider = self.local_pref("r2", "100.64.5.0/24", "64605")
        self.assertGreater(customer, peer)
        self.assertGreater(peer, provider)


class C_Valley(unittest.TestCase):
    def test_every_best_path_everywhere_is_valley_free_and_matches_the_simulation(self):
        for node in ROUTERS:
            for origin, prefix in PREFIXES.items():
                path = LAB.best_path(node, prefix)
                self.assertIsNotNone(path, f"{node} no tiene ruta a {prefix}")
                self.assertTrue(spvp.is_valley_free(GRAPH, full_path(node, path)), (node, path))
                self.assertEqual(path, expected(node, origin), (node, prefix))

    def test_the_old_shortcut_is_gone(self):
        self.assertEqual(LAB.best_path("r2", "100.64.4.0/24"), [64605, 64606, 64604])
        LAB.assert_hops(self, "r2", "100.64.4.1", ["100.64.5.133", "100.64.5.130", "100.64.4.1"],
                        source="100.64.2.1")

    def test_everybody_still_reaches_everybody(self):
        for node, *arguments in (("h1", "100.64.3.10"), ("h3", "100.64.1.10"),
                                 ("r2", "-I", "100.64.2.1", "100.64.4.1")):
            reached = wait_until(lambda: LAB.ok(node, "ping", "-c", "1", "-W", "2", *arguments),
                                 seconds=20)
            self.assertTrue(reached, node)


class D_Prefer(unittest.TestCase):
    """Paso 5: r1 prefiere salir por r2. Corre al final: cambia la tabla de r1."""
    TARGET = "100.64.3.0/24"

    @classmethod
    def setUpClass(cls):
        LAB.load_conf("r1", answer_path("prefer.conf"))
        wait_until(lambda: LAB.best_path("r1", cls.TARGET) == [64602, 64605, 64606, 64603], seconds=30)

    def test_longer_path_through_r2_wins(self):
        self.assertEqual(LAB.best_path("r1", self.TARGET), [64602, 64605, 64606, 64603])
        self.assertIn("best (Local Pref)", LAB.vtysh("r1", f"show bgp ipv4 {self.TARGET}"))

    def test_traceroute_goes_through_r2(self):
        LAB.assert_hops(self, "h1", "100.64.3.10", ["100.64.1.1", "100.64.2.129"])

    def test_the_other_direction_does_not_change(self):
        # local-preference decide por dónde sale el tráfico de r1, no por dónde le entra.
        LAB.assert_hops(self, "h3", "100.64.1.10", ["100.64.3.1", "100.64.6.133", "100.64.6.130"])

    def test_export_rules_survive_the_change(self):
        self.assertEqual(LAB.advertised("r1", "100.64.2.129"), {"100.64.1.0/24"})
        self.assertEqual(LAB.advertised("r1", "100.64.4.129"), {"100.64.1.0/24"})

    def test_backup_takes_over_and_primary_returns(self):
        self.addCleanup(LAB.out, "r1", "ip", "link", "set", "eth2", "up")
        LAB.out("r1", "ip", "link", "set", "eth2", "down")
        self.assertTrue(wait_until(lambda: LAB.hops("h1", "100.64.3.10")[1:2] == ["100.64.4.129"],
                                   seconds=30))
        LAB.out("r1", "ip", "link", "set", "eth2", "up")
        self.assertTrue(wait_until(lambda: LAB.hops("h1", "100.64.3.10")[1:2] == ["100.64.2.129"],
                                   seconds=60))


if __name__ == "__main__":
    unittest.main()
test_poli.pyDescargar test_poli.py
"""Pruebas de route-map, local-preference y comunidades, pieza por pieza (poli/poli.clab.yml)."""
import os
import unittest

from netlab import HERE, Lab, wait_until

LAB = Lab("poli", "w06-poli", "redes-w06-poli")
ALL = {f"100.64.{n}.0/24" for n in (130, 131, 132, 133)}
TO_P, TO_Q, TO_C = "100.64.131.129", "100.64.130.130", "100.64.130.134"


def conf(name):
    return os.path.join(HERE, "poli", name)


def setUpModule():
    unittest.addModuleCleanup(LAB.destroy)
    LAB.deploy()
    if not LAB.wait_for_table(["m", "p", "q", "c"], ALL):
        raise RuntimeError("la topología no quedó lista")


class A_NoPolicy(unittest.TestCase):
    def test_shorter_path_wins_without_policy(self):
        self.assertEqual(LAB.best_path("m", "100.64.131.0/24"), [65310])

    def test_m_passes_its_peer_prefix_to_its_provider(self):
        self.assertIn("100.64.132.0/24", LAB.advertised("m", TO_P))

    def test_frr_requires_policy_on_ebgp_unless_told_otherwise(self):
        on = ("configure terminal", "router bgp 65330", "bgp ebgp-requires-policy")
        off = ("configure terminal", "router bgp 65330", "no bgp ebgp-requires-policy")
        self.addCleanup(lambda: wait_until(lambda: LAB.best_path("c", "100.64.131.0/24"), seconds=30))
        self.addCleanup(LAB.vtysh, "c", "clear bgp *")
        self.addCleanup(LAB.vtysh, "c", *off)
        LAB.vtysh("c", *on)
        LAB.vtysh("c", "clear bgp *")
        self.assertTrue(wait_until(lambda: "(Policy)" in LAB.vtysh("c", "show bgp summary"),
                                   seconds=30), LAB.vtysh("c", "show bgp summary"))


class B_LocalPref(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        LAB.load_conf("m", conf("step1_localpref.conf"))

    def test_local_pref_beats_a_shorter_as_path(self):
        self.assertTrue(wait_until(lambda: LAB.best_path("m", "100.64.131.0/24") == [65320, 65310],
                                   seconds=20))
        self.assertTrue(wait_until(lambda: "best (Local Pref)" in
                                   LAB.vtysh("m", "show bgp ipv4 100.64.131.0/24"), seconds=20))

    def test_local_pref_does_not_leave_the_as(self):
        self.assertTrue(wait_until(lambda: LAB.best_path("m", "100.64.131.0/24") == [65320, 65310],
                                   seconds=20))
        self.assertNotIn("localpref", LAB.vtysh("c", "show bgp ipv4 100.64.131.0/24"))


class C_Community(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        LAB.load_conf("m", conf("step2_community.conf"))

    def test_community_tags_the_route_inside_m(self):
        self.assertTrue(wait_until(lambda: "Community: 65300:30" in
                                   LAB.vtysh("m", "show bgp ipv4 100.64.133.0/24"), seconds=20))

    def test_community_travels_to_other_ases(self):
        self.assertTrue(wait_until(lambda: "65300:30" in
                                   LAB.vtysh("p", "show bgp ipv4 100.64.133.0/24"), seconds=20))

    def test_other_routes_are_not_tagged(self):
        self.assertNotIn("Community", LAB.vtysh("m", "show bgp ipv4 100.64.132.0/24"))


class D_Export(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        wait_until(lambda: "65300:30" in LAB.vtysh("m", "show bgp ipv4 100.64.133.0/24"), seconds=20)
        LAB.load_conf("m", conf("step3_export.conf"))
        wait_until(lambda: LAB.advertised("m", TO_P) == {"100.64.130.0/24", "100.64.133.0/24"},
                   seconds=30)

    def test_only_own_and_tagged_routes_go_to_p(self):
        self.assertEqual(LAB.advertised("m", TO_P), {"100.64.130.0/24", "100.64.133.0/24"})

    def test_p_lost_the_path_through_m_to_q(self):
        ok = wait_until(lambda: "65300" not in LAB.vtysh("p", "show bgp ipv4 100.64.132.0/24"),
                        seconds=20)
        self.assertTrue(ok, LAB.vtysh("p", "show bgp ipv4 100.64.132.0/24"))

    def test_other_neighbours_are_not_affected(self):
        self.assertIn("100.64.131.0/24", LAB.advertised("m", TO_C))
        self.assertIn("100.64.133.0/24", LAB.advertised("m", TO_Q))


class E_ImplicitDeny(unittest.TestCase):
    """Lo que un route-map no deja pasar de forma explícita, se bota."""

    def apply_to_q(self, name):
        LAB.vtysh("m", "configure terminal", "router bgp 65300", "address-family ipv4 unicast",
                  f"neighbor {TO_Q} route-map {name} out")
        self.addCleanup(LAB.vtysh, "m", "configure terminal", "router bgp 65300",
                        "address-family ipv4 unicast", f"no neighbor {TO_Q} route-map {name} out")

    def test_route_map_that_matches_nothing_announces_nothing(self):
        LAB.vtysh("m", "configure terminal", "bgp community-list standard NOBODY permit 65300:99",
                  "route-map ONLY_NOBODY permit 10", "match community NOBODY")
        self.apply_to_q("ONLY_NOBODY")
        self.assertTrue(wait_until(lambda: LAB.advertised("m", TO_Q) == set(), seconds=30))

    def test_undefined_route_map_announces_nothing(self):
        wait_until(lambda: LAB.advertised("m", TO_Q) != set(), seconds=30)
        self.apply_to_q("TYPO_IN_THE_NAME")
        self.assertTrue(wait_until(lambda: LAB.advertised("m", TO_Q) == set(), seconds=30))


if __name__ == "__main__":
    unittest.main()
test_spvp.pyDescargar test_spvp.py
"""Pruebas del simulador de vector de caminos (spvp.py). No levantan ninguna red."""
import random
import unittest

import spvp


class ValleyFree(unittest.TestCase):
    def setUp(self):
        self.graph = spvp.ases_graph()

    def test_up_peer_down_is_valley_free(self):
        # r1 -> r2 -> r5 -> r6 -> r3: sube, sube, peer, baja.
        self.assertTrue(spvp.is_valley_free(self.graph, [64601, 64602, 64605, 64606, 64603]))

    def test_provider_through_customer_is_a_valley(self):
        # r2 -> r1 -> r4: r1 le da tránsito gratis a sus dos proveedores.
        self.assertFalse(spvp.is_valley_free(self.graph, [64602, 64601, 64604]))

    def test_up_after_a_peer_link_is_not_valley_free(self):
        # 3 es peer de 2, y 2 es cliente de 1. Que 2 le pase a su proveedor una ruta de su
        # peer, o a su peer una ruta de su proveedor, es lo que prohíbe la exportación.
        graph = spvp.Graph({1: [], 2: [1], 3: []}, [(2, 3)])
        self.assertFalse(spvp.is_valley_free(graph, [1, 2, 3]))
        self.assertFalse(spvp.is_valley_free(graph, [3, 2, 1]))
        self.assertTrue(spvp.is_valley_free(graph, [3, 2]))

    def test_two_peer_links_are_not_valley_free(self):
        graph = spvp.Graph({1: [], 2: [], 3: []}, [(1, 2), (2, 3)])
        self.assertFalse(spvp.is_valley_free(graph, [1, 2, 3]))

    def test_unknown_link_is_rejected(self):
        self.assertFalse(spvp.is_valley_free(self.graph, [64603, 64605]))


class Convergence(unittest.TestCase):
    def test_provider_cycle_with_free_ranking_never_settles(self):
        graph = spvp.ring_graph(cycle=True)
        stable, _, _ = spvp.simulate(graph, 0, spvp.ring_rank, spvp.gao_rexford_export, [1, 2, 3])
        self.assertFalse(stable)

    def test_same_ranking_without_cycle_settles_in_any_order(self):
        graph = spvp.ring_graph(cycle=False)
        for seed in range(30):
            order = [1, 2, 3]
            random.Random(seed).shuffle(order)
            stable, best, _ = spvp.simulate(graph, 0, spvp.ring_rank,
                                            spvp.gao_rexford_export, order)
            self.assertTrue(stable, order)
            self.assertEqual(best[3], (3, 0))

    def test_six_ases_settle_on_valley_free_paths(self):
        graph = spvp.ases_graph()
        for destination in graph.nodes:
            best = spvp.predicted_paths(destination)
            for node in graph.nodes:
                self.assertIsNotNone(best[node], (node, destination))
                self.assertTrue(spvp.is_valley_free(graph, list(best[node])))

    def test_the_final_state_does_not_depend_on_the_order(self):
        graph = spvp.ases_graph()
        others = [node for node in graph.nodes if node != 64603]
        results = set()
        for seed in range(30):
            order = others[:]
            random.Random(seed).shuffle(order)
            stable, best, _ = spvp.simulate(graph, 64603, spvp.gao_rexford_rank,
                                            spvp.gao_rexford_export, order)
            self.assertTrue(stable)
            results.add(tuple(sorted(best.items())))
        self.assertEqual(len(results), 1)


if __name__ == "__main__":
    unittest.main()

Laboratorio

Archivos de partida del laboratorio de la semana.

lab/prefer.confDescargar lab/prefer.conf
! Laboratorio, paso 5: r1 (AS 64601) prefiere salir por r2 mientras exista.
!
! TODO: escriban aquí el cambio de local-preference, y borren esta línea cuando
! terminen. No debe romper lo que hicieron en r1.conf: recuerden que un vecino tiene
! un solo route-map de entrada. Se carga con make prefer, después de make policies.
lab/r1.confDescargar lab/r1.conf
! Laboratorio, paso 4: políticas de r1, AS 64601. Prefijo propio: 100.64.1.0/24.
! Vecinos: r2 (100.64.2.129) y r4 (100.64.4.129), los dos son proveedores.
!
! TODO: escriban aquí las políticas de importación y de exportación de este AS, y
! borren esta línea cuando terminen. Usen comunidades de la forma 64601:xxx para marcar
! de dónde vino cada ruta, y local-preference para ordenar las preferencias. Se
! cargan con make policies. El ejemplo de poli/ muestra cada pieza por separado.
lab/r2.confDescargar lab/r2.conf
! Laboratorio, paso 4: políticas de r2, AS 64602. Prefijo propio: 100.64.2.0/24.
! Vecinos: r1 (100.64.2.130) es cliente; r5 (100.64.5.133) es proveedor.
!
! TODO: escriban aquí las políticas de importación y de exportación de este AS, y
! borren esta línea cuando terminen. Usen comunidades de la forma 64602:xxx para marcar
! de dónde vino cada ruta, y local-preference para ordenar las preferencias. Se
! cargan con make policies. El ejemplo de poli/ muestra cada pieza por separado.
lab/r3.confDescargar lab/r3.conf
! Laboratorio, paso 4: políticas de r3, AS 64603. Prefijo propio: 100.64.3.0/24.
! Vecinos: r6 (100.64.6.133) es proveedor.
!
! TODO: escriban aquí las políticas de importación y de exportación de este AS, y
! borren esta línea cuando terminen. Usen comunidades de la forma 64603:xxx para marcar
! de dónde vino cada ruta, y local-preference para ordenar las preferencias. Se
! cargan con make policies. El ejemplo de poli/ muestra cada pieza por separado.
lab/r4.confDescargar lab/r4.conf
! Laboratorio, paso 4: políticas de r4, AS 64604. Prefijo propio: 100.64.4.0/24.
! Vecinos: r1 (100.64.4.130) es cliente; r6 (100.64.6.129) es proveedor.
!
! TODO: escriban aquí las políticas de importación y de exportación de este AS, y
! borren esta línea cuando terminen. Usen comunidades de la forma 64604:xxx para marcar
! de dónde vino cada ruta, y local-preference para ordenar las preferencias. Se
! cargan con make policies. El ejemplo de poli/ muestra cada pieza por separado.
lab/r5.confDescargar lab/r5.conf
! Laboratorio, paso 4: políticas de r5, AS 64605. Prefijo propio: 100.64.5.0/24.
! Vecinos: r2 (100.64.5.134) es cliente; r6 (100.64.5.130) es peer.
!
! TODO: escriban aquí las políticas de importación y de exportación de este AS, y
! borren esta línea cuando terminen. Usen comunidades de la forma 64605:xxx para marcar
! de dónde vino cada ruta, y local-preference para ordenar las preferencias. Se
! cargan con make policies. El ejemplo de poli/ muestra cada pieza por separado.
lab/r6.confDescargar lab/r6.conf
! Laboratorio, paso 4: políticas de r6, AS 64606. Prefijo propio: 100.64.6.0/24.
! Vecinos: r4 (100.64.6.130) y r3 (100.64.6.134) son clientes; r5 (100.64.5.129) es peer.
!
! TODO: escriban aquí las políticas de importación y de exportación de este AS, y
! borren esta línea cuando terminen. Usen comunidades de la forma 64606:xxx para marcar
! de dónde vino cada ruta, y local-preference para ordenar las preferencias. Se
! cargan con make policies. El ejemplo de poli/ muestra cada pieza por separado.

Compilación

Cómo se compilan y ejecutan los ejemplos.

MakefileDescargar Makefile
# Semana 6: BGP y políticas entre redes. Se usa dentro del contenedor del curso.
#
#   make             comprueba que estén las herramientas (aquí no se compila nada)
#   make test        levanta las topologías una tras otra y corre las pruebas
#                    (test_*.py); siempre destruye lo que levantó. Las pruebas del
#                    laboratorio usan lab/ y se saltan hasta que lo completen
#   make up-ases     levanta los seis AS, sin políticas (ases/ases.clab.yml)
#   make up-ibgp     levanta el AS con iBGP y OSPF (ibgp/ibgp.clab.yml)
#   make up-poli     levanta el ejemplo de políticas (poli/poli.clab.yml)
#   make policies    carga lab/r1.conf ... lab/r6.conf en los seis AS
#   make prefer      carga lab/prefer.conf en r1
#   make down        destruye las tres topologías y sus redes de Docker
#   make clean       lo anterior, y borra lo generado

POLICY_DIR ?= lab

.PHONY: all test up-ases up-ibgp up-poli policies prefer down clean

all:
	@command -v containerlab >/dev/null || { echo "Falta containerlab: usen el contenedor del curso"; exit 1; }
	@command -v python3 >/dev/null || { echo "Falta python3: usen el contenedor del curso"; exit 1; }
	@echo "Listo. Sigan con make test o make up-ases."

test: all
	@status=0; PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -p 'test_*.py' -v || status=$$?; \
	  $(MAKE) --no-print-directory down; exit $$status

up-ases:
	cd ases && containerlab deploy -t ases.clab.yml --reconfigure

up-ibgp:
	cd ibgp && containerlab deploy -t ibgp.clab.yml --reconfigure

up-poli:
	cd poli && containerlab deploy -t poli.clab.yml --reconfigure

# vtysh -f lee un archivo que esté dentro del nodo: por eso primero se copia.
policies:
	@for n in 1 2 3 4 5 6; do \
	  docker cp $(POLICY_DIR)/r$$n.conf clab-w06-ases-r$$n:/tmp/policy.conf && \
	  docker exec clab-w06-ases-r$$n vtysh -f /tmp/policy.conf >/dev/null 2>&1 && echo "r$$n cargado" || exit 1; \
	done

prefer:
	docker cp $(POLICY_DIR)/prefer.conf clab-w06-ases-r1:/tmp/prefer.conf
	docker exec clab-w06-ases-r1 vtysh -f /tmp/prefer.conf >/dev/null 2>&1

down:
	@cd ases && containerlab destroy -t ases.clab.yml --cleanup >/dev/null 2>&1 || true
	@cd ibgp && containerlab destroy -t ibgp.clab.yml --cleanup >/dev/null 2>&1 || true
	@cd poli && containerlab destroy -t poli.clab.yml --cleanup >/dev/null 2>&1 || true
	@docker network rm redes-w06-ases >/dev/null 2>&1 || true
	@docker network rm redes-w06-ibgp >/dev/null 2>&1 || true
	@docker network rm redes-w06-poli >/dev/null 2>&1 || true

clean: down
	rm -rf __pycache__ ases/clab-w06-ases ibgp/clab-w06-ibgp poli/clab-w06-poli
	rm -rf *.pcap .capture-*