#!/usr/bin/env python3
"""Lo que ocurre en la topología bgp/: ataques, fugas y defensas, uno por uno.

    python3 scenarios.py <escenario>     aplica un escenario
    python3 scenarios.py reset           vuelve al estado inicial
    python3 scenarios.py                 lista los escenarios

Cada escenario es la lista de comandos de vtysh o de shell que se corren en cada nodo:
lean este archivo, porque los comandos son la mitad de lo que hay que aprender.
"""
import os
import sys

from netlab import ANSWERS, Lab, run

LAB = Lab("bgp", "w07-bgp", "redes-w07-bgp")

A_R1, A_R6, A_R4 = "10.207.4.1", "10.207.5.1", "10.207.6.1"
B_R4, B_R3 = "10.207.7.1", "10.207.8.1"
R4_A, R4_B = "10.207.6.2", "10.207.7.2"
VICTIM = "100.64.3.0/24"
HALVES = ("100.64.3.0/25", "100.64.3.128/25")
SERVER = "100.64.3.10"
PORT_STRICT, PORT_LAX = 8282, 8283


def conf(node, *lines):
    """Corre líneas de configuración en vtysh, en una sola sesión."""
    args = ["vtysh", "-c", "configure terminal"]
    for line in lines:
        args += ["-c", line]
    return LAB.out(node, *args)


def in_bgp(asn, *lines):
    return ("router bgp " + str(asn), "address-family ipv4 unicast", *lines, "end")


# --- El atacante, r6 (AS 64666) ---------------------------------------------------

def same():
    """r6 origina el mismo /24 de la víctima y contesta en la dirección del servidor."""
    LAB.sh("r6", "ip", "addr", "add", SERVER + "/32", "dev", "dum0")
    conf("r6", f"ip route {VICTIM} Null0", *in_bgp(64666, f"network {VICTIM}"))


def specific():
    """r6 origina solo la mitad del /24 donde está el servidor: un /25."""
    LAB.sh("r6", "ip", "addr", "add", SERVER + "/32", "dev", "dum0")
    conf("r6", f"ip route {HALVES[0]} Null0", *in_bgp(64666, f"network {HALVES[0]}"))


def forged():
    """El /24 otra vez, pero con el AS de la víctima al final del AS_PATH."""
    conf("r6", "ip prefix-list ROBADO seq 5 permit " + VICTIM,
         "route-map FORJAR permit 10", "match ip address prefix-list ROBADO",
         "set as-path prepend 64603", "route-map FORJAR permit 20", "exit",
         *in_bgp(64666, "neighbor 10.207.5.2 route-map FORJAR out"))
    same()


def forged_specific():
    """El /25 con el origen forjado: aquí debería importar el largo máximo del ROA."""
    conf("r6", "ip prefix-list ROBADO seq 5 permit " + HALVES[0],
         "route-map FORJAR permit 10", "match ip address prefix-list ROBADO",
         "set as-path prepend 64603", "route-map FORJAR permit 20", "exit",
         *in_bgp(64666, "neighbor 10.207.5.2 route-map FORJAR out"))
    specific()


# --- La víctima, r3 (AS 64603) ----------------------------------------------------

def counter():
    """r3 responde anunciando dos /25: más específicos que el /24 del atacante."""
    conf("r3", *[f"ip route {half} eth1" for half in HALVES],
         *in_bgp(64603, *[f"network {half}" for half in HALVES]))


# --- La fuga, r4 (AS 64604) -------------------------------------------------------

def leak():
    """r4 pierde su regla de exportación: cada proveedor recibe las rutas del otro."""
    conf("r4", *in_bgp(64604, f"no neighbor {R4_A} route-map SOLO-PROPIAS out",
                       f"no neighbor {R4_B} route-map SOLO-PROPIAS out"))


def slow_r4():
    """r4 es una red chica: 5 ms y 20 Mbit/s por enlace. Sirve para ver el costo de la fuga."""
    for dev in ("eth1", "eth2"):
        LAB.sh("r4", "tc", "qdisc", "replace", "dev", dev, "root", "netem",
               "delay", "5ms", "rate", "20mbit", "limit", "200")


# --- Defensas que se muestran en clase --------------------------------------------

def max_prefix():
    """Los proveedores de r4 toleran hasta dos prefijos en esa sesión; luego la cierran."""
    conf("a", *in_bgp(64610, f"neighbor {A_R4} maximum-prefix 2"))
    conf("b", *in_bgp(64620, f"neighbor {B_R4} maximum-prefix 2"))


ROV_NEIGHBORS = {
    "a": (64610, "10.207.9.2", ("10.207.2.2", A_R1, A_R6, A_R4)),
    "d": (64640, "10.207.10.2", ("10.207.1.1", "10.207.3.1")),
}


def rov_on(node, port=PORT_STRICT):
    """El AS se conecta al cache y descarta lo inválido en todas sus sesiones."""
    asn, cache, neighbors = ROV_NEIGHBORS[node]
    conf(node, "rpki", "rpki polling_period 5", f"rpki cache tcp {cache} {port} preference 1",
         "exit", "route-map ROV deny 10", "match rpki invalid", "route-map ROV permit 20", "exit",
         *in_bgp(asn, *[f"neighbor {peer} route-map ROV in" for peer in neighbors]))


def rov_a():
    """Solo a, el proveedor del atacante, aplica validación de origen."""
    rov_on("a")


def rov_a_lax():
    """Como rov_a, pero con un ROA descuidado: el largo máximo de la víctima es 25."""
    rov_on("a", PORT_LAX)


def rov_d():
    """Solo d, en la cima y lejos del atacante, aplica validación de origen."""
    rov_on("d")


# --- Volver al inicio -------------------------------------------------------------

def undo(node, asn, *lines):
    """Borra cada cosa en su propia llamada: vtysh se detiene en el primer error, y es
    normal que lo que se quiere borrar no exista."""
    for line in lines:
        conf(node, *in_bgp(asn, line))


def undo_global(node, *lines):
    for line in lines:
        conf(node, line)


def reset():
    undo("r6", 64666, f"no network {VICTIM}", *[f"no network {h}" for h in HALVES],
         "no neighbor 10.207.5.2 route-map FORJAR out")
    undo_global("r6", f"no ip route {VICTIM} Null0", *[f"no ip route {h} Null0" for h in HALVES],
                "no route-map FORJAR", "no ip prefix-list ROBADO")
    LAB.sh("r6", "ip", "addr", "del", SERVER + "/32", "dev", "dum0")
    undo("r3", 64603, *[f"no network {h}" for h in HALVES])
    undo_global("r3", *[f"no ip route {h} eth1" for h in HALVES])
    conf("r4", *in_bgp(64604, f"neighbor {R4_A} route-map SOLO-PROPIAS out",
                       f"neighbor {R4_B} route-map SOLO-PROPIAS out"))
    for dev in ("eth1", "eth2"):
        LAB.sh("r4", "tc", "qdisc", "del", "dev", dev, "root")
    undo("a", 64610, f"no neighbor {A_R4} maximum-prefix 2",
         *[f"no neighbor {p} route-map ROV in" for p in ROV_NEIGHBORS["a"][2]],
         f"no neighbor {A_R1} prefix-list CLIENTE-R1 in",
         f"no neighbor {A_R6} prefix-list CLIENTE-R6 in",
         f"no neighbor {A_R4} prefix-list CLIENTE-R4 in")
    undo("b", 64620, f"no neighbor {B_R4} maximum-prefix 2",
         f"no neighbor {B_R3} prefix-list CLIENTE-R3 in",
         f"no neighbor {B_R4} prefix-list CLIENTE-R4 in")
    undo("d", 64640, *[f"no neighbor {p} route-map ROV in" for p in ROV_NEIGHBORS["d"][2]])
    for node in ("a", "d"):
        cache = ROV_NEIGHBORS[node][1]
        undo_global(node, "no route-map ROV")
        for port in (PORT_STRICT, PORT_LAX):
            conf(node, "rpki", f"no rpki cache tcp {cache} {port} preference 1")
    undo_global("a", "no ip prefix-list CLIENTE-R1", "no ip prefix-list CLIENTE-R6",
                "no ip prefix-list CLIENTE-R4")
    undo_global("b", "no ip prefix-list CLIENTE-R3", "no ip prefix-list CLIENTE-R4")
    # Una sesión cerrada por maximum-prefix no vuelve sola.
    LAB.out("a", "vtysh", "-c", "clear bgp ipv4 unicast " + A_R4)
    LAB.out("b", "vtysh", "-c", "clear bgp ipv4 unicast " + B_R4)


SCENARIOS = {f.__name__: f for f in (same, specific, forged, forged_specific, counter, leak, slow_r4,
                                     max_prefix, rov_a, rov_a_lax, rov_d, reset)}


def filters(folder):
    """Carga los filtros del laboratorio en los dos proveedores y reaplica la importación."""
    for node, name in (("a", "filters-a.conf"), ("b", "filters-b.conf")):
        path = os.path.join(folder, name)
        done = run(["docker", "cp", path, f"{LAB.prefix}{node}:/tmp/filters.conf"])
        if done.returncode != 0:
            raise RuntimeError(done.stderr)
        done = LAB.sh(node, "vtysh", "-f", "/tmp/filters.conf")
        if done.returncode != 0:
            raise RuntimeError(done.stdout + done.stderr)
        LAB.out(node, "vtysh", "-c", "clear bgp ipv4 unicast * soft in")


def main():
    if len(sys.argv) == 2 and sys.argv[1] == "filters":
        filters(ANSWERS)
        return 0
    if len(sys.argv) != 2 or sys.argv[1] not in SCENARIOS:
        for name, function in SCENARIOS.items():
            print(f"{name:16} {(function.__doc__ or 'vuelve al estado inicial').strip()}")
        print(f"{'filters':16} carga sus filtros de lab/ en a y en b")
        return 1
    SCENARIOS[sys.argv[1]]()
    return 0


if __name__ == "__main__":
    sys.exit(main())
