Semana 4 — IP, forwarding y NAT
Objetivos
- Distinguir el plano de datos del plano de control.
- Leer un datagrama IPv4 y explicar sus campos.
- Dividir una red con CIDR y aplicar longest prefix match.
- Explicar qué hay dentro de un router y dónde se forman las colas.
- Explicar ICMP, DHCP y NAT, y qué cambia con IPv6.
Sesión A — IP: datagramas, CIDR y forwarding · Dentro de un router
Lecturas:
- Kurose y Ross, secs. 4.1 (Overview of Network Layer), 4.2 (What’s Inside a Router?), 4.3.1 (IPv4 Datagram Format) y la parte de CIDR de 4.3.2 (IPv4 Addressing).
- CS168, IP Header, Addressing y Router Hardware.
Lecturas complementarias:
Temas:
- Forwarding y routing: uno es local y rápido, el otro es global y más lento.
- Campos de IPv4: TTL, protocolo, checksum, direcciones y fragmentación.
- Direccionamiento: subredes, máscaras y CIDR. Ejemplo resuelto: dividir
10.20.0.0/22en cuatro subredes. - Agregación de rutas y longest prefix match. Ejemplo con una tabla de cuatro entradas.
- Error común: elegir la ruta “más general” o la primera de la tabla. Siempre gana el prefijo más largo.
- Dentro de un router: puertos de entrada, fabric de conmutación, puertos de salida y plano de control. Colas y scheduling.
Sesión B — ICMP, DHCP, NAT e IPv6
Lecturas:
- Kurose y Ross, secs. 4.3.2 (DHCP), 4.3.3 (Network Address Translation), 4.3.4 (IPv6) y 5.6 (ICMP).
- CS168, DHCP y NAT.
Lecturas complementarias:
Temas:
- ICMP: echo, destino inalcanzable y TTL excedido. Cómo funciona
traceroute, que ya usaron en P0. - DHCP: discover, offer, request y ack. Por qué se envía por broadcast.
- NAT: la tabla de traducción, direcciones privadas y cómo complica recibir conexiones entrantes.
- Por qué se acabaron las direcciones IPv4. IPv6: direcciones de 128 bits, encabezado fijo y sin fragmentación en routers.
- Convivencia de IPv4 e IPv6: doble pila y túneles.
- Recorrido completo de un paquete, de un host a otro, con cada traducción y cada encabezado.
Laboratorio — Subredes y NAT con Containerlab
Trabajen en code/week-04/.
- Levantar una topología de tres routers con rutas estáticas. Agregar las rutas que faltan hasta que todos los hosts se alcancen.
- Agregar a un router una ruta más específica para un destino y predecir qué camino se usa. Comprobarlo con
ip route getytraceroute. - Usar
tcpdumpen cada salto y comparar TTL, direcciones MAC y direcciones IP. Explicar qué cambia en cada salto. - Configurar NAT con
nftablesen el router de salida. Capturar el mismo paquete a cada lado del router y leer las reglas connft list rulesety la tabla de traducción conconntrack -L. - Intentar abrir una conexión desde afuera hacia un host privado, y luego agregar una redirección de puerto.
- Asignar direcciones IPv6 de ejemplo (
2001:db8::/32) y hacer ping por IPv6. - Avanzar con P1.
Preguntas de repaso
- Dividan
192.168.8.0/24en subredes para 100, 50 y 20 hosts. Escriban prefijo, máscara y rango de cada una. - Con las rutas
10.0.0.0/8,10.1.0.0/16,10.1.2.0/24y0.0.0.0/0, ¿por dónde sale10.1.2.3? ¿Y10.2.0.1? - Un paquete cruza tres routers. ¿Qué campos de los encabezados de enlace e IP cambian en cada salto?
- ¿Por qué
traceroutenecesita ICMP? ¿Qué pasa si un router no lo envía? - Dos hosts detrás de un NAT abren una conexión al mismo servidor, desde el mismo puerto local. ¿Cómo los distingue el NAT?
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (16)Pruebas (3)Laboratorio (3)Compilación (1)
Ejemplos
Los programas que usan los apuntes y la clase.
// cidr.c — calcula los datos de un prefijo IPv4 y lo divide en subredes iguales.
//
// ./cidr 10.20.0.0/22 muestra máscara, red, broadcast y rango de hosts
// ./cidr 10.20.0.0/22 4 lo divide en 4 subredes (una potencia de 2)
#define _POSIX_C_SOURCE 200809L
#include <arpa/inet.h>
#include <errno.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Máscara de un prefijo de largo len. El caso 0 va aparte porque desplazar un
// entero de 32 bits en 32 posiciones no está definido en C.
static uint32_t mask_of(int len) {
return len == 0 ? 0 : UINT32_MAX << (32 - len);
}
// Convierte "a.b.c.d/n" a una dirección en orden de host y un largo.
static int parse_prefix(const char *text, uint32_t *addr, int *len) {
char buffer[32];
if (strlen(text) >= sizeof buffer) {
return -1;
}
strcpy(buffer, text);
char *slash = strchr(buffer, '/');
if (slash == NULL) {
return -1;
}
*slash = '\0';
char *end;
errno = 0;
long value = strtol(slash + 1, &end, 10);
if (errno != 0 || end == slash + 1 || *end != '\0' || value < 0 || value > 32) {
return -1;
}
struct in_addr parsed;
if (inet_pton(AF_INET, buffer, &parsed) != 1) {
return -1;
}
*addr = ntohl(parsed.s_addr);
*len = (int)value;
return 0;
}
// Escribe una dirección en orden de host como texto. Devuelve un buffer estático
// distinto en cada una de las cuatro llamadas más recientes.
static const char *ip_text(uint32_t addr) {
static char buffers[4][INET_ADDRSTRLEN];
static int next = 0;
char *out = buffers[next];
next = (next + 1) % 4;
struct in_addr raw = {.s_addr = htonl(addr)};
if (inet_ntop(AF_INET, &raw, out, INET_ADDRSTRLEN) == NULL) {
perror("inet_ntop");
exit(1);
}
return out;
}
// Direcciones que se pueden asignar a hosts. En /31 (RFC 3021) son las dos, y en
// /32 es la única; en lo demás se restan la de red y la de broadcast.
static uint64_t host_count(int len) {
if (len == 32) {
return 1;
}
if (len == 31) {
return 2;
}
return (UINT64_C(1) << (32 - len)) - 2;
}
static void print_info(uint32_t addr, int len) {
uint32_t mask = mask_of(len);
uint32_t network = addr & mask;
uint32_t broadcast = network | ~mask;
uint32_t first = len >= 31 ? network : network + 1;
uint32_t last = len >= 31 ? broadcast : broadcast - 1;
if (addr != network) {
printf("aviso %s tiene bits de host encendidos\n", ip_text(addr));
}
printf("prefijo %s/%d\n", ip_text(network), len);
printf("máscara %s\n", ip_text(mask));
printf("total %" PRIu64 " direcciones\n", UINT64_C(1) << (32 - len));
if (len < 31) {
printf("broadcast %s\n", ip_text(broadcast));
}
printf("hosts %s - %s (%" PRIu64 ")\n", ip_text(first), ip_text(last),
host_count(len));
}
static int split(uint32_t addr, int len, long parts) {
if (parts > (1L << 32)) {
fprintf(stderr, "cidr: /%d no alcanza para %ld subredes\n", len, parts);
return 1;
}
int extra = 0;
while ((1L << extra) < parts) {
extra++;
}
if ((1L << extra) != parts) {
fprintf(stderr, "cidr: %ld no es una potencia de 2\n", parts);
return 1;
}
if (len + extra > 32) {
fprintf(stderr, "cidr: /%d no alcanza para %ld subredes\n", len, parts);
return 1;
}
int new_len = len + extra;
uint32_t network = addr & mask_of(len);
uint64_t step = UINT64_C(1) << (32 - new_len);
printf("%s/%d en %ld subredes de /%d (%" PRIu64 " direcciones cada una):\n",
ip_text(network), len, parts, new_len, step);
for (long i = 0; i < parts; i++) {
uint32_t sub = network + (uint32_t)((uint64_t)i * step);
uint32_t broadcast = sub | ~mask_of(new_len);
uint32_t first = new_len >= 31 ? sub : sub + 1;
uint32_t last = new_len >= 31 ? broadcast : broadcast - 1;
char label[32];
int written = snprintf(label, sizeof label, "%s/%d", ip_text(sub), new_len);
if (written < 0 || (size_t)written >= sizeof label) {
fprintf(stderr, "cidr: error al formatear la subred\n");
return 1;
}
printf(" %-18s hosts %s - %s (%" PRIu64 ")\n", label, ip_text(first), ip_text(last),
host_count(new_len));
}
return 0;
}
int main(int argc, char *argv[]) {
if (argc != 2 && argc != 3) {
fprintf(stderr, "uso: %s a.b.c.d/n [subredes]\n", argv[0]);
return 2;
}
uint32_t addr;
int len;
if (parse_prefix(argv[1], &addr, &len) != 0) {
fprintf(stderr, "cidr: prefijo inválido: %s\n", argv[1]);
return 2;
}
if (argc == 2) {
print_info(addr, len);
return 0;
}
char *end;
errno = 0;
long parts = strtol(argv[2], &end, 10);
if (errno != 0 || end == argv[2] || *end != '\0' || parts < 1) {
fprintf(stderr, "cidr: número de subredes inválido: %s\n", argv[2]);
return 2;
}
return split(addr, len, parts);
}// ipv4_header.c — lee un paquete de la salida de "tcpdump -x" y muestra los
// campos de su encabezado IPv4, byte a byte. Se usa así:
//
// docker exec clab-w04-fwd-r1 tcpdump -l -n -x -c 1 -i eth3 icmp | ./ipv4_header
//
// Solo mira el primer paquete que recibe.
#define _POSIX_C_SOURCE 200809L
#include <arpa/inet.h>
#include <ctype.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#define HEADER_MIN 20
#define MAX_BYTES 65535
static int hex_value(char c) {
if (c >= '0' && c <= '9') {
return c - '0';
}
if (c >= 'a' && c <= 'f') {
return c - 'a' + 10;
}
if (c >= 'A' && c <= 'F') {
return c - 'A' + 10;
}
return -1;
}
// Agrega a bytes lo que hay después de "0xNNNN:" en una línea de tcpdump -x.
// Devuelve cuántos bytes hay en total; las líneas sin ese inicio no cuentan.
static size_t read_hex_line(const char *line, uint8_t *bytes, size_t count) {
const char *p = line;
while (isspace((unsigned char)*p)) {
p++;
}
if (strncmp(p, "0x", 2) != 0) {
return count;
}
p = strchr(p, ':');
if (p == NULL) {
return count;
}
p++;
while (*p != '\0' && count < MAX_BYTES) {
if (isspace((unsigned char)*p)) {
p++;
continue;
}
int high = hex_value(p[0]);
int low = high < 0 ? -1 : hex_value(p[1]);
if (low < 0) {
break;
}
bytes[count++] = (uint8_t)(high * 16 + low);
p += 2;
}
return count;
}
static const char *protocol_name(uint8_t protocol) {
switch (protocol) {
case 1:
return "ICMP";
case 6:
return "TCP";
case 17:
return "UDP";
default:
return "otro";
}
}
static uint16_t read_u16(const uint8_t *p) {
return (uint16_t)((p[0] << 8) | p[1]);
}
static void print_address(const char *label, const uint8_t *p) {
char text[INET_ADDRSTRLEN];
if (inet_ntop(AF_INET, p, text, sizeof text) == NULL) {
perror("inet_ntop");
return;
}
printf("%-12s %s\n", label, text);
}
int main(void) {
static uint8_t bytes[MAX_BYTES];
size_t count = 0;
bool started = false;
char line[256];
while (fgets(line, sizeof line, stdin) != NULL) {
size_t before = count;
count = read_hex_line(line, bytes, count);
if (count > before) {
started = true;
} else if (started) {
break; // terminó el primer paquete
}
}
if (ferror(stdin)) {
perror("stdin");
return 1;
}
if (count < HEADER_MIN) {
fprintf(stderr, "ipv4_header: se leyeron %zu bytes; faltan datos\n", count);
return 1;
}
unsigned version = bytes[0] >> 4;
unsigned header_length = (unsigned)(bytes[0] & 0x0f) * 4;
if (version != 4 || header_length < HEADER_MIN || header_length > count) {
fprintf(stderr, "ipv4_header: no es un encabezado IPv4 válido\n");
return 1;
}
uint16_t flags_offset = read_u16(&bytes[6]);
unsigned offset_units = flags_offset & 0x1fff;
printf("versión %u\n", version);
printf("%-12s %u bytes\n", "encabezado", header_length);
printf("%-12s DSCP %u, ECN %u\n", "ToS", (unsigned)(bytes[1] >> 2), (unsigned)(bytes[1] & 3));
printf("%-12s %u bytes\n", "largo total", read_u16(&bytes[2]));
printf("%-12s 0x%04x\n", "id", read_u16(&bytes[4]));
printf("%-12s DF=%u MF=%u\n", "flags", (flags_offset >> 14) & 1u, (flags_offset >> 13) & 1u);
printf("%-12s %u (%u bytes)\n", "offset", offset_units, offset_units * 8);
printf("%-12s %u\n", "TTL", bytes[8]);
printf("%-12s %u (%s)\n", "protocolo", bytes[9], protocol_name(bytes[9]));
printf("%-12s 0x%04x\n", "checksum", read_u16(&bytes[10]));
print_address("origen", &bytes[12]);
print_address("destino", &bytes[16]);
return 0;
}// lpm.c — longest prefix match con una tabla de forwarding leída de un archivo.
//
// ./lpm tabla.txt 10.1.2.3 10.2.0.1
//
// Cada línea de la tabla tiene "prefijo/largo siguiente-salto"; lo que sigue a
// un # es comentario. La búsqueda recorre toda la tabla: es lo más simple de
// leer, pero un router de verdad no puede darse ese lujo (ver el capítulo).
#define _POSIX_C_SOURCE 200809L
#include <arpa/inet.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_ROUTES 256
#define HOP_SIZE 32
struct route {
uint32_t network; // en orden de host
uint32_t mask;
int length;
char hop[HOP_SIZE];
};
static uint32_t mask_of(int length) {
return length == 0 ? 0 : UINT32_MAX << (32 - length);
}
static const char *ip_text(uint32_t addr, char *out) {
struct in_addr raw = {.s_addr = htonl(addr)};
if (inet_ntop(AF_INET, &raw, out, INET_ADDRSTRLEN) == NULL) {
perror("inet_ntop");
exit(1);
}
return out;
}
// Lee una línea de la tabla. Devuelve 1 si había una ruta, 0 si la línea estaba
// vacía y -1 si está mal escrita.
static int parse_line(char *line, struct route *route) {
char *comment = strchr(line, '#');
if (comment != NULL) {
*comment = '\0';
}
char prefix[32];
char hop[HOP_SIZE];
int fields = sscanf(line, "%31s %31s", prefix, hop);
if (fields <= 0) {
return 0;
}
if (fields != 2) {
return -1;
}
char *slash = strchr(prefix, '/');
if (slash == NULL) {
return -1;
}
*slash = '\0';
char *end;
long length = strtol(slash + 1, &end, 10);
if (end == slash + 1 || *end != '\0' || length < 0 || length > 32) {
return -1;
}
struct in_addr parsed;
if (inet_pton(AF_INET, prefix, &parsed) != 1) {
return -1;
}
route->length = (int)length;
route->mask = mask_of(route->length);
route->network = ntohl(parsed.s_addr);
if ((route->network & route->mask) != route->network) {
return -1; // el prefijo tiene bits de host encendidos
}
strcpy(route->hop, hop);
return 1;
}
static int load_table(const char *path, struct route *table) {
FILE *file = fopen(path, "r");
if (file == NULL) {
perror(path);
return -1;
}
char line[128];
int count = 0;
int line_number = 0;
while (fgets(line, sizeof line, file) != NULL) {
line_number++;
struct route route;
int result = parse_line(line, &route);
if (result < 0) {
fprintf(stderr, "%s:%d: línea inválida\n", path, line_number);
fclose(file);
return -1;
}
if (result == 1) {
if (count == MAX_ROUTES) {
fprintf(stderr, "%s: más de %d rutas\n", path, MAX_ROUTES);
fclose(file);
return -1;
}
table[count++] = route;
}
}
if (ferror(file)) {
perror(path);
fclose(file);
return -1;
}
if (fclose(file) != 0) {
perror(path);
return -1;
}
return count;
}
// Devuelve la ruta con el prefijo más largo que contiene a addr, o NULL. El
// orden de la tabla no importa: se revisan todas las entradas.
static const struct route *lookup(const struct route *table, int count, uint32_t addr) {
const struct route *best = NULL;
for (int i = 0; i < count; i++) {
const struct route *route = &table[i];
if ((addr & route->mask) == route->network
&& (best == NULL || route->length > best->length)) {
best = route;
}
}
return best;
}
int main(int argc, char *argv[]) {
if (argc < 3) {
fprintf(stderr, "uso: %s tabla dirección...\n", argv[0]);
return 2;
}
static struct route table[MAX_ROUTES];
int count = load_table(argv[1], table);
if (count < 0) {
return 2;
}
for (int a = 2; a < argc; a++) {
struct in_addr parsed;
if (inet_pton(AF_INET, argv[a], &parsed) != 1) {
fprintf(stderr, "lpm: dirección inválida: %s\n", argv[a]);
return 2;
}
uint32_t addr = ntohl(parsed.s_addr);
printf("%s\n", argv[a]);
char text[INET_ADDRSTRLEN];
for (int i = 0; i < count; i++) {
if ((addr & table[i].mask) == table[i].network) {
printf(" coincide %s/%d -> %s\n", ip_text(table[i].network, text),
table[i].length, table[i].hop);
}
}
const struct route *best = lookup(table, count, addr);
if (best == NULL) {
printf(" sin ruta: se descarta\n");
} else {
printf(" gana %s/%d -> %s\n", ip_text(best->network, text), best->length,
best->hop);
}
}
return 0;
}"""Ayudas de las pruebas de la semana 4: levantar topologías y correr comandos."""
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):
done = subprocess.run(
["containerlab", "deploy", "-t", self.topology, "--reconfigure"],
cwd=self.folder, capture_output=True, text=True, timeout=300)
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 load_nft(self, node, path):
with open(path, encoding="utf-8") as file:
done = run(["docker", "exec", "-i", self.prefix + node, "nft", "-f", "-"], stdin=file)
if done.returncode != 0:
raise RuntimeError(done.stderr)
def python(self, node, code):
"""Corre un programa corto de Python en un nodo."""
done = subprocess.run(["docker", "exec", "-i", self.prefix + node, "python3", "-"],
input=code, capture_output=True, text=True, timeout=60)
return done.stdout + done.stderr
def capture(self, node, interface, expression, count, seconds=5, flags=("-n", "-e", "-v")):
"""Empieza una captura; el resultado se pide con capture.result()."""
return Capture(self.prefix + node, interface, expression, count, seconds, flags)
class Capture:
def __init__(self, container, interface, expression, count, seconds, flags):
self.process = subprocess.Popen(
["docker", "exec", container, "timeout", str(seconds), "tcpdump", "-l", *flags,
"-i", interface, "-c", str(count), expression],
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)
time.sleep(0.7) # darle tiempo a tcpdump de abrir la interfaz
def result(self):
output, _ = self.process.communicate(timeout=30)
return output
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()# Tabla de ejemplo del capítulo: prefijo y siguiente salto.
10.200.0.0/16 A
10.200.4.0/22 B
10.200.5.0/24 C
0.0.0.0/0 D# Tres routers en triángulo, con un host en cada LAN. Las rutas estáticas están
# incompletas a propósito: completarlas es el paso 1 del laboratorio.
#
# h1 --- r1 ------- r2 --- h2
# \ /
# \ /
# r3 --- h3
name: w04-fwd
mgmt:
network: redes-w04-fwd
ipv4-subnet: 172.90.4.0/25
ipv6-subnet: 3fff:172:90:4::/80
topology:
defaults:
kind: linux
image: redes-router
nodes:
# 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.
r1:
binds: [r1/frr.conf:/etc/frr/frr.conf:ro]
exec: &router-exec
- ip route del default
- ip -6 route del default
- sysctl -w net.ipv6.conf.all.forwarding=1
r2:
binds: [r2/frr.conf:/etc/frr/frr.conf:ro]
exec: *router-exec
r3:
binds: [r3/frr.conf:/etc/frr/frr.conf:ro]
exec: *router-exec
h1:
exec:
- ip route del default
- ip -6 route del default
- ip addr add 10.200.1.10/24 dev eth1
- ip route add default via 10.200.1.1
h2:
exec:
- ip route del default
- ip -6 route del default
- ip addr add 10.200.2.10/24 dev eth1
- ip route add default via 10.200.2.1
h3:
exec:
- ip route del default
- ip -6 route del default
- ip addr add 10.200.3.10/24 dev eth1
- ip route add default via 10.200.3.1
# Containerlab usa MTU 9500 por defecto; aquí queremos el valor de Ethernet.
links:
- endpoints: ["h1:eth1", "r1:eth1"]
mtu: 1500
- endpoints: ["h2:eth1", "r2:eth1"]
mtu: 1500
- endpoints: ["h3:eth1", "r3:eth1"]
mtu: 1500
- endpoints: ["r1:eth2", "r2:eth2"]
mtu: 1500
- endpoints: ["r2:eth3", "r3:eth2"]
mtu: 1500
- endpoints: ["r1:eth3", "r3:eth3"]
mtu: 1500#!/bin/sh
# Direcciones IPv6 de ejemplo en el camino h1 - r1 - r3 (paso 6 del laboratorio).
# Se corre en el contenedor del curso, con fwd.clab.yml levantada.
set -e
P=clab-w04-fwd
docker exec $P-h1 ip addr replace 2001:db8:1::10/64 dev eth1
docker exec $P-h1 ip -6 route replace default via 2001:db8:1::1
docker exec $P-r1 ip addr replace 2001:db8:1::1/64 dev eth1
docker exec $P-r1 ip addr replace 2001:db8:13::1/64 dev eth3
docker exec $P-r3 ip addr replace 2001:db8:13::2/64 dev eth3
docker exec $P-r3 ip -6 route replace 2001:db8:1::/64 via 2001:db8:13::1
# Una dirección nueva pasa unos segundos en duplicate address detection; mientras
# tanto, el kernel no la usa como origen.
sleep 3#!/usr/bin/env python3
"""Servidor DHCP mínimo para ver discover, offer, request y ack (RFC 2131).
Es material de clase, no un servidor de verdad: una sola subred, sin renovar
concesiones y sin revisar conflictos. Responde siempre por broadcast.
Uso: dhcp_server.py <interfaz> <dirección del servidor> <primer host> <último host>
Ejemplo: dhcp_server.py eth1 10.200.50.1 10.200.50.100 10.200.50.110
"""
import ipaddress
import socket
import struct
import sys
BOOTREQUEST, BOOTREPLY = 1, 2
MAGIC_COOKIE = b"\x63\x82\x53\x63"
DISCOVER, OFFER, REQUEST, ACK = 1, 2, 3, 5
LEASE_SECONDS = 600
NETMASK = "255.255.255.0"
MESSAGE_NAMES = {1: "DISCOVER", 2: "OFFER", 3: "REQUEST", 5: "ACK"}
def parse_options(data):
"""Devuelve las opciones DHCP como un diccionario código -> bytes."""
options = {}
i = 0
while i < len(data) and data[i] != 255:
if data[i] == 0: # relleno
i += 1
continue
code, length = data[i], data[i + 1]
options[code] = data[i + 2 : i + 2 + length]
i += 2 + length
return options
def build_reply(request, message_type, offered, server):
"""Arma la respuesta copiando xid, flags y chaddr del pedido."""
header = struct.pack("!BBBB", BOOTREPLY, 1, 6, 0) + request[4:12]
header += b"\0" * 4 + socket.inet_aton(offered) # ciaddr, yiaddr
header += socket.inet_aton(server) + b"\0" * 4 # siaddr, giaddr
header += request[28:44] + b"\0" * 192 # chaddr, sname y file
options = bytes([53, 1, message_type])
options += bytes([54, 4]) + socket.inet_aton(server)
options += bytes([51, 4]) + struct.pack("!I", LEASE_SECONDS)
options += bytes([1, 4]) + socket.inet_aton(NETMASK)
options += bytes([3, 4]) + socket.inet_aton(server) # router por defecto
return header + MAGIC_COOKIE + options + b"\xff"
def main():
if len(sys.argv) != 5:
sys.exit(__doc__)
device, server, first, last = sys.argv[1:]
pool = [str(ipaddress.ip_address(n)) for n in range(
int(ipaddress.ip_address(first)), int(ipaddress.ip_address(last)) + 1)]
leases = {} # MAC -> dirección
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, device.encode())
sock.bind(("0.0.0.0", 67))
while True:
data, _ = sock.recvfrom(1500)
if len(data) < 240 or data[0] != BOOTREQUEST or data[236:240] != MAGIC_COOKIE:
continue
options = parse_options(data[240:])
kind = options.get(53, b"\0")[0]
mac = data[28:34]
if kind == DISCOVER:
free = [a for a in pool if a not in leases.values()]
if mac not in leases and not free:
print("sin direcciones libres", flush=True)
continue
leases.setdefault(mac, free[0] if free else None)
answer = OFFER
elif kind == REQUEST and mac in leases:
answer = ACK
else:
continue
print(f"{MESSAGE_NAMES[kind]} de {mac.hex(':')} -> "
f"{MESSAGE_NAMES[answer]} {leases[mac]}", flush=True)
reply = build_reply(data, answer, leases[mac], server)
sock.sendto(reply, ("255.255.255.255", 68))
if __name__ == "__main__":
main()# Redirección de puerto: lo que llega por eth2 al puerto 8080 de gw va a a1.
# Se agrega encima de masquerade.nft.
table ip nat {
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
iifname "eth2" tcp dport 8080 dnat to 10.200.50.10:8080
}
}# NAT de salida: lo que viene de la LAN y sale por eth2 toma la dirección de eth2.
flush ruleset
table ip nat {
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
ip saddr 10.200.50.0/24 oifname "eth2" masquerade
}
}# Una LAN privada detrás de un router de borde con NAT, un proveedor y un servidor
# externo. Las direcciones "públicas" salen de 100.64.0.0/10.
#
# a1 ---+
# a2 ---+-- sw --- gw ======= isp --- ext
# a3 ---+
# 10.200.50.0/24 100.64.0.0/30 100.64.9.0/24
#
# a3 empieza sin dirección: la pide por DHCP.
name: w04-nat
mgmt:
network: redes-w04-nat
ipv4-subnet: 172.90.4.128/25
ipv6-subnet: 3fff:172:90:4:1::/80
topology:
defaults:
kind: linux
image: redes-router
nodes:
# 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.
a1:
exec:
- ip route del default
- ip -6 route del default
- ip addr add 10.200.50.10/24 dev eth1
- ip route add default via 10.200.50.1
a2:
exec:
- ip route del default
- ip -6 route del default
- ip addr add 10.200.50.11/24 dev eth1
- ip route add default via 10.200.50.1
a3:
exec:
- ip route del default
- ip -6 route del default
sw:
exec:
- ip route del default
- ip -6 route del default
- ip link add br0 type bridge
- ip link set eth1 master br0
- ip link set eth2 master br0
- ip link set eth3 master br0
- ip link set eth4 master br0
- ip link set br0 up
gw:
binds:
- gw/frr.conf:/etc/frr/frr.conf:ro
- dhcp_server.py:/opt/dhcp_server.py
exec: ["ip route del default", "ip -6 route del default"]
isp:
binds: [isp/frr.conf:/etc/frr/frr.conf:ro]
exec: ["ip route del default", "ip -6 route del default"]
ext:
exec:
- ip route del default
- ip -6 route del default
- ip addr add 100.64.9.10/24 dev eth1
- ip route add default via 100.64.9.1
# Containerlab usa MTU 9500 por defecto; aquí queremos el valor de Ethernet.
links:
- endpoints: ["a1:eth1", "sw:eth1"]
mtu: 1500
- endpoints: ["a2:eth1", "sw:eth2"]
mtu: 1500
- endpoints: ["a3:eth1", "sw:eth3"]
mtu: 1500
- endpoints: ["gw:eth1", "sw:eth4"]
mtu: 1500
- endpoints: ["gw:eth2", "isp:eth1"]
mtu: 1500
- endpoints: ["isp:eth2", "ext:eth1"]
mtu: 1500frr defaults traditional
hostname r1
interface eth1
ip address 10.200.1.1/24
interface eth2
ip address 10.200.12.1/30
interface eth3
ip address 10.200.13.1/30
! Ruta inicial: la LAN de h3 se alcanza por el enlace directo a r3.
ip route 10.200.3.0/24 10.200.13.2frr defaults traditional
hostname r2
interface eth1
ip address 10.200.2.1/24
interface eth2
ip address 10.200.12.2/30
interface eth3
ip address 10.200.23.1/30frr defaults traditional
hostname r3
interface eth1
ip address 10.200.3.1/24
interface eth2
ip address 10.200.23.2/30
interface eth3
ip address 10.200.13.2/30
! Ruta inicial: la LAN de h1 se alcanza por el enlace directo a r1.
ip route 10.200.1.0/24 10.200.13.1frr defaults traditional
hostname gw
interface eth1
ip address 10.200.50.1/24
interface eth2
ip address 100.64.0.2/30
! Todo lo que no es de la LAN sale hacia el proveedor.
ip route 0.0.0.0/0 100.64.0.1frr defaults traditional
hostname isp
interface eth1
ip address 100.64.0.1/30
interface eth2
ip address 100.64.9.1/24
! Sin ruta hacia 10.200.50.0/24: el proveedor no conoce las direcciones privadas.Pruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
"""Pruebas de forwarding, ICMP, fragmentación, colas e IPv6 en fwd/fwd.clab.yml."""
import os
import re
import subprocess
import unittest
from netlab import HERE, Lab, answer_path, wait_until
LAB = Lab("fwd", "w04-fwd", "redes-w04-fwd")
NO_DF_SENDER = """
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.IPPROTO_IP, 10, 0) # IP_MTU_DISCOVER = IP_PMTUDISC_DONT: sin DF
s.sendto(b"x" * 1400, ("10.200.3.10", 9999))
"""
def setUpModule():
unittest.addModuleCleanup(LAB.destroy)
LAB.deploy()
ok = wait_until(lambda: "10.200.3.0/24" in LAB.out("r1", "ip", "route")
and "10.200.1.0/24" in LAB.out("r3", "ip", "route"))
if not ok:
raise RuntimeError("las rutas iniciales no aparecieron")
def average_rtt(output):
match = re.search(r"min/avg/max = [\d.]+/([\d.]+)/", output)
return float(match.group(1)) if match else None
def set_link_mtu(mtu):
LAB.sh("r1", "ip", "link", "set", "eth3", "mtu", str(mtu))
LAB.sh("r3", "ip", "link", "set", "eth3", "mtu", str(mtu))
class A_StartingState(unittest.TestCase):
def test_h1_reaches_h3(self):
self.assertEqual(LAB.sh("h1", "ping", "-c", "2", "-W", "2", "10.200.3.10").returncode, 0)
def test_no_default_route_through_management(self):
self.assertNotIn("default", LAB.out("r1", "ip", "route"))
self.assertNotIn("default", LAB.out("r2", "ip", "route"))
def test_links_use_ethernet_mtu(self):
self.assertIn("mtu 1500", LAB.out("h1", "ip", "link", "show", "eth1"))
def test_missing_route_gives_net_unreachable(self):
capture = LAB.capture("h1", "eth1", "icmp", 2)
LAB.sh("h1", "ping", "-c", "1", "-W", "1", "10.200.2.10")
self.assertIn("net 10.200.2.10 unreachable", capture.result())
self.assertIn("unreachable", LAB.out("r1", "ip", "route", "get", "10.200.2.10"))
class B_PerHop(unittest.TestCase):
"""Lo que cambia y lo que no en cada salto, en el camino h1 - r1 - r3 - h3."""
def captures(self):
points = [("h1", "eth1"), ("r1", "eth3"), ("r3", "eth1")]
running = [LAB.capture(n, i, "icmp[icmptype] == icmp-echo", 1) for n, i in points]
LAB.sh("h1", "ping", "-c", "1", "-W", "2", "10.200.3.10")
return [c.result() for c in running]
def test_ttl_macs_and_ips(self):
outputs = self.captures()
self.assertEqual([int(re.search(r"ttl (\d+)", o).group(1)) for o in outputs], [64, 63, 62])
for output in outputs:
self.assertIn("10.200.1.10 > 10.200.3.10", output)
macs = [re.match(r"\S+ (\S+) > (\S+),", o).groups() for o in outputs]
self.assertEqual(len(set(macs)), 3)
def test_checksum_is_redone_at_each_hop(self):
points = [("r1", "eth1"), ("r1", "eth3")]
running = [LAB.capture(n, i, "icmp[icmptype] == icmp-echo", 1, flags=("-n", "-x"))
for n, i in points]
LAB.sh("h1", "ping", "-c", "1", "-W", "2", "10.200.3.10")
words = [re.search(r"0x0000:\s+(.*)", c.result()).group(1).split() for c in running]
# Palabras 4 (TTL y protocolo) y 5 (checksum) del encabezado.
self.assertEqual(int(words[0][4], 16) - int(words[1][4], 16), 0x100)
self.assertEqual(int(words[1][5], 16) - int(words[0][5], 16), 0x100)
self.assertEqual(words[0][:4], words[1][:4])
class C_Icmp(unittest.TestCase):
def test_ttl_exceeded(self):
capture = LAB.capture("h1", "eth1", "icmp", 2)
LAB.sh("h1", "ping", "-c", "1", "-W", "1", "-t", "1", "10.200.3.10")
self.assertIn("10.200.1.1 > 10.200.1.10: ICMP time exceeded", capture.result())
def test_host_unreachable_comes_from_last_router(self):
capture = LAB.capture("h1", "eth1", "icmp", 2, seconds=9)
LAB.sh("h1", "ping", "-c", "1", "-W", "6", "10.200.3.99")
self.assertIn("10.200.13.2 > 10.200.1.10: ICMP host 10.200.3.99 unreachable",
capture.result())
def test_port_unreachable_comes_from_destination(self):
capture = LAB.capture("h1", "eth1", "icmp", 1)
LAB.sh("h1", "sh", "-c", "echo hola | nc -u -w1 10.200.3.10 9999")
self.assertIn("10.200.3.10 udp port 9999 unreachable", capture.result())
def test_traceroute_ends_at_destination(self):
lines = LAB.out("h1", "traceroute", "-n", "-w", "1", "10.200.3.10").splitlines()
self.assertIn("10.200.1.1", lines[1])
self.assertIn("10.200.13.2", lines[2])
self.assertIn("10.200.3.10", lines[3])
class D_Fragmentation(unittest.TestCase):
def setUp(self):
set_link_mtu(600)
LAB.sh("h1", "ip", "route", "flush", "cache")
self.addCleanup(LAB.sh, "h1", "ip", "route", "flush", "cache")
self.addCleanup(set_link_mtu, 1500)
def test_df_gets_need_to_frag_and_host_then_fragments(self):
capture = LAB.capture("h1", "eth1", "icmp", 2)
first = LAB.sh("h1", "ping", "-c", "1", "-W", "1", "-s", "1400", "10.200.3.10")
self.assertNotEqual(first.returncode, 0)
output = capture.result()
self.assertIn("flags [DF]", output)
self.assertIn("need to frag (mtu 600)", output)
self.assertIn("mtu 600", LAB.out("h1", "ip", "route", "get", "10.200.3.10"))
# h1 ya conoce el MTU del camino: fragmenta él, y el ping funciona.
at_source = LAB.capture("h1", "eth1", "ip[6:2] & 0x3fff != 0 and src 10.200.1.10", 3)
second = LAB.sh("h1", "ping", "-c", "1", "-W", "2", "-s", "1400", "10.200.3.10")
self.assertEqual(second.returncode, 0)
self.assertEqual(len(re.findall(r"offset \d+", at_source.result())), 3)
def test_without_df_the_router_fragments(self):
before = LAB.capture("h1", "eth1", "udp", 1)
after = LAB.capture("r1", "eth3", "src 10.200.1.10", 3)
self.assertEqual(LAB.python("h1", NO_DF_SENDER), "")
self.assertIn("offset 0, flags [none]", before.result())
offsets = re.findall(r"offset (\d+), flags \[(\+|none)\]", after.result())
self.assertEqual(offsets, [("0", "+"), ("576", "+"), ("1152", "none")])
class E_Queues(unittest.TestCase):
def setUp(self):
LAB.sh("r1", "tc", "qdisc", "add", "dev", "eth3", "root", "handle", "1:", "tbf",
"rate", "10mbit", "burst", "16kb", "limit", "256kb")
self.addCleanup(LAB.sh, "r1", "tc", "qdisc", "del", "dev", "eth3", "root")
LAB.daemon("h3", "iperf3 -s -1")
wait_until(lambda: "5201" in LAB.out("h3", "netstat", "-tln"), seconds=5)
def load(self):
return subprocess.Popen(
["docker", "exec", LAB.prefix + "h1", "iperf3", "-c", "10.200.3.10", "-t", "7"],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def test_queue_delay_and_strict_priority(self):
idle = average_rtt(LAB.out("h1", "ping", "-c", "5", "-i", "0.2", "10.200.3.10"))
traffic = self.load()
self.addCleanup(traffic.wait)
wait_until(lambda: "backlog 0b" not in LAB.out("r1", "tc", "-s", "qdisc", "show",
"dev", "eth3"), seconds=4, step=0.2)
loaded = average_rtt(LAB.out("h1", "ping", "-c", "8", "-i", "0.25", "10.200.3.10"))
# Las colas dependen de la máquina: solo se exige que el retardo se note.
self.assertGreater(loaded, 10)
self.assertGreater(loaded, 5 * idle)
traffic.wait()
# Con una banda de prioridad para ICMP, el ping se adelanta a la cola.
LAB.sh("r1", "tc", "qdisc", "add", "dev", "eth3", "parent", "1:1", "handle", "10:", "prio")
for rule in ("add table ip qos",
"add chain ip qos mark_icmp { type filter hook forward priority 0; }",
"add rule ip qos mark_icmp ip protocol icmp meta priority set 10:1"):
self.assertEqual(LAB.sh("r1", "nft", rule).returncode, 0, rule)
self.addCleanup(LAB.sh, "r1", "nft", "delete", "table", "ip", "qos")
LAB.daemon("h3", "iperf3 -s -1")
wait_until(lambda: "5201" in LAB.out("h3", "netstat", "-tln"), seconds=5)
traffic = self.load()
self.addCleanup(traffic.wait)
wait_until(lambda: "backlog 0b" not in LAB.out("r1", "tc", "-s", "qdisc", "show",
"dev", "eth3"), seconds=4, step=0.2)
priority = average_rtt(LAB.out("h1", "ping", "-c", "8", "-i", "0.25", "10.200.3.10"))
self.assertLess(priority, loaded / 2)
traffic.wait()
class F_Ipv6(unittest.TestCase):
@classmethod
def setUpClass(cls):
done = subprocess.run(["sh", os.path.join(HERE, "fwd", "ipv6.sh")],
capture_output=True, text=True)
if done.returncode != 0:
raise RuntimeError(done.stderr)
def test_hop_limit_drops_at_router(self):
LAB.sh("h1", "ip", "-6", "route", "flush", "cache")
before = LAB.capture("r1", "eth1", "icmp6 and ip6[40] == 128", 1, flags=("-n", "-v"))
after = LAB.capture("r1", "eth3", "icmp6 and ip6[40] == 128", 1, flags=("-n", "-v"))
self.assertEqual(LAB.sh("h1", "ping", "-c", "1", "-W", "2",
"2001:db8:13::2").returncode, 0)
self.assertIn("hlim 64", before.result())
self.assertIn("hlim 63", after.result())
def test_ping_and_neighbor_discovery(self):
LAB.sh("h1", "ip", "-6", "neigh", "flush", "dev", "eth1")
capture = LAB.capture("h1", "eth1", "icmp6", 4, flags=("-n", "-v"))
wait_until(lambda: LAB.sh("h1", "ping", "-c", "1", "-W", "2",
"2001:db8:13::2").returncode == 0, seconds=10)
output = capture.result()
self.assertIn("neighbor solicitation", output)
self.assertRegex(output, r"> ff02::1:ff00:1\b")
self.assertIn("echo request", output)
def test_router_sends_packet_too_big_and_source_fragments(self):
set_link_mtu(1280)
self.addCleanup(set_link_mtu, 1500)
LAB.sh("h1", "ip", "-6", "route", "flush", "cache")
capture = LAB.capture("h1", "eth1", "icmp6 and ip6[40] != 135 and ip6[40] != 136", 2,
flags=("-n", "-v"))
LAB.sh("h1", "ping", "-c", "1", "-W", "1", "-s", "1400", "2001:db8:13::2")
self.assertIn("packet too big, mtu 1280", capture.result())
# Ahora fragmenta el origen, con un encabezado de extensión.
fragments = LAB.capture("h1", "eth1", "ip6[6] == 44", 2, flags=("-n", "-v"))
done = LAB.sh("h1", "ping", "-c", "1", "-W", "2", "-s", "1400", "2001:db8:13::2")
self.assertEqual(done.returncode, 0)
self.assertIn("frag (", fragments.result())
class G_LabRoutes(unittest.TestCase):
"""Pasos 1, 2 y 6 del laboratorio. Se saltan hasta que completen lab/."""
@classmethod
def setUpClass(cls):
done = subprocess.run(["sh", answer_path("routes.sh")], capture_output=True, text=True)
if done.returncode != 0:
raise RuntimeError(done.stderr)
wait_until(lambda: "10.200.2.0/24" in LAB.out("r1", "ip", "route"))
def test_1_all_hosts_reach_each_other(self):
addresses = {"h1": "10.200.1.10", "h2": "10.200.2.10", "h3": "10.200.3.10"}
for source in addresses:
for target, address in addresses.items():
if source != target:
done = LAB.sh(source, "ping", "-c", "1", "-W", "2", address)
self.assertEqual(done.returncode, 0, f"{source} -> {target}")
def test_2_more_specific_route_wins_and_makes_path_asymmetric(self):
LAB.sh("r1", "vtysh", "-c", "conf t", "-c", "ip route 10.200.3.10/32 10.200.12.2")
self.addCleanup(LAB.sh, "r1", "vtysh", "-c", "conf t",
"-c", "no ip route 10.200.3.10/32 10.200.12.2")
self.assertTrue(wait_until(lambda: "10.200.3.10 nhid" in LAB.out("r1", "ip", "route")
or "10.200.3.10 via" in LAB.out("r1", "ip", "route")))
self.assertIn("via 10.200.12.2", LAB.out("r1", "ip", "route", "get", "10.200.3.10"))
self.assertIn("via 10.200.13.2", LAB.out("r1", "ip", "route", "get", "10.200.3.11"))
hops = LAB.out("h1", "traceroute", "-n", "-w", "1", "10.200.3.10").splitlines()[1:]
self.assertEqual(len(hops), 4)
self.assertIn("10.200.12.2", hops[1])
# r3 responde con la dirección de la interfaz por la que sale hacia h1.
self.assertIn("10.200.13.2", hops[2])
# A la vuelta, r3 no pasa por r2.
at_r2 = LAB.capture("r2", "eth3", "icmp", 1, seconds=3)
at_r3 = LAB.capture("r3", "eth3", "icmp and src 10.200.3.10", 1, seconds=3)
LAB.sh("h1", "ping", "-c", "1", "-W", "2", "10.200.3.10")
self.assertIn("echo request", at_r2.result())
self.assertIn("echo reply", at_r3.result())
def test_3_router_that_stays_silent_shows_asterisks(self):
LAB.sh("r1", "vtysh", "-c", "conf t", "-c", "ip route 10.200.3.10/32 10.200.12.2")
self.addCleanup(LAB.sh, "r1", "vtysh", "-c", "conf t",
"-c", "no ip route 10.200.3.10/32 10.200.12.2")
wait_until(lambda: "10.200.3.10 " in LAB.out("r1", "ip", "route"))
for rule in ("add table ip quiet",
"add chain ip quiet out { type filter hook output priority 0; }",
"add rule ip quiet out icmp type time-exceeded drop"):
self.assertEqual(LAB.sh("r3", "nft", rule).returncode, 0, rule)
self.addCleanup(LAB.sh, "r3", "nft", "delete", "table", "ip", "quiet")
hops = LAB.out("h1", "traceroute", "-n", "-w", "1", "10.200.3.10").splitlines()[1:]
self.assertIn("* * *", hops[2].replace(" ", " "))
self.assertIn("10.200.3.10", hops[3])
def test_4_ipv6_reaches_h3(self):
path = answer_path("ipv6_h3.sh")
done = subprocess.run(["sh", path], capture_output=True, text=True)
self.assertEqual(done.returncode, 0, done.stderr)
self.assertTrue(wait_until(lambda: LAB.sh("h1", "ping", "-c", "1", "-W", "2",
"2001:db8:3::10").returncode == 0, seconds=10))
match = re.search(r"ttl=(\d+)", LAB.out("h1", "ping", "-c", "1", "2001:db8:3::10"))
self.assertEqual(match.group(1), "62")
if __name__ == "__main__":
unittest.main()"""Pruebas de DHCP y NAT en nat/nat.clab.yml."""
import os
import re
import time
import unittest
from netlab import HERE, Lab, answer_path, wait_until
LAB = Lab("nat", "w04-nat", "redes-w04-nat")
GW_PUBLIC = "100.64.0.2"
EXT = "100.64.9.10"
def setUpModule():
unittest.addModuleCleanup(LAB.destroy)
LAB.deploy()
ok = wait_until(lambda: "default" in LAB.out("gw", "ip", "route")
and "100.64.9.0/24" in LAB.out("isp", "ip", "route")
and LAB.sh("isp", "ping", "-c", "1", "-W", "1", EXT).returncode == 0)
if not ok:
raise RuntimeError("la topología no quedó lista")
LAB.daemon("ext", "nc -lk -p 9000 -e /bin/cat")
def reset_nat():
LAB.sh("gw", "nft", "flush", "ruleset")
LAB.sh("gw", "conntrack", "-F")
class A_Dhcp(unittest.TestCase):
def test_lease_with_four_messages_by_broadcast(self):
self.assertEqual(LAB.out("a3", "ip", "-4", "addr", "show", "dev", "eth1").count("inet "), 0)
LAB.daemon("gw", "python3 /opt/dhcp_server.py eth1 10.200.50.1 "
"10.200.50.100 10.200.50.110 > /tmp/dhcp.log 2>&1")
time.sleep(1)
at_client = LAB.capture("sw", "eth3", "udp port 67 or udp port 68", 4, seconds=8,
flags=("-n", "-e", "-v"))
at_other = LAB.capture("a2", "eth1", "udp port 67 or udp port 68", 4, seconds=8,
flags=("-n", "-e", "-v"))
done = LAB.sh("a3", "udhcpc", "-i", "eth1", "-n", "-q", "-f", timeout=30)
self.assertEqual(done.returncode, 0, done.stderr)
output = at_client.result()
for name in ("Discover", "Offer", "Request", "ACK"):
self.assertIn(f"DHCP-Message (53), length 1: {name}", output)
self.assertEqual(output.count("> ff:ff:ff:ff:ff:ff"), 4)
self.assertIn("0.0.0.0.68 > 255.255.255.255.67", output)
# Otro host de la LAN también ve los mensajes: es broadcast.
self.assertIn("Discover", at_other.result())
self.assertIn("10.200.50.100/24", LAB.out("a3", "ip", "-4", "addr", "show", "dev", "eth1"))
self.assertIn("default via 10.200.50.1", LAB.out("a3", "ip", "route"))
self.assertEqual(LAB.sh("a3", "ping", "-c", "1", "-W", "2", "10.200.50.1").returncode, 0)
class B_Nat(unittest.TestCase):
def setUp(self):
reset_nat()
self.addCleanup(reset_nat)
def load_masquerade(self):
LAB.load_nft("gw", os.path.join(HERE, "nat", "masquerade.nft"))
def test_without_nat_private_source_does_not_come_back(self):
capture = LAB.capture("gw", "eth2", "icmp", 2, seconds=3, flags=("-n",))
done = LAB.sh("a1", "ping", "-c", "1", "-W", "1", EXT)
self.assertNotEqual(done.returncode, 0)
self.assertIn(f"10.200.50.10 > {EXT}", capture.result())
def test_masquerade_rewrites_source_on_the_way_out_and_back(self):
self.load_masquerade()
inside = LAB.capture("gw", "eth1", "icmp", 2, flags=("-n",))
outside = LAB.capture("gw", "eth2", "icmp", 2, flags=("-n",))
at_ext = LAB.capture("ext", "eth1", "icmp", 2, flags=("-n",))
self.assertEqual(LAB.sh("a1", "ping", "-c", "1", "-W", "2", EXT).returncode, 0)
self.assertIn(f"10.200.50.10 > {EXT}", inside.result())
self.assertIn(f"{GW_PUBLIC} > {EXT}", outside.result())
at_ext = at_ext.result()
self.assertIn(f"{GW_PUBLIC} > {EXT}", at_ext)
self.assertNotIn("10.200.50", at_ext)
def test_same_source_port_from_two_hosts_gets_two_external_ports(self):
self.load_masquerade()
syns = LAB.capture("ext", "eth1", "tcp[tcpflags] & tcp-syn != 0 and dst port 9000", 2,
seconds=6, flags=("-n",))
LAB.daemon("a1", f"sleep 8 | nc -p 40000 {EXT} 9000")
LAB.daemon("a2", f"sleep 8 | nc -p 40000 {EXT} 9000")
ports = re.findall(rf"{GW_PUBLIC}\.(\d+) > ", syns.result())
self.assertEqual(len(set(ports)), 2)
table = LAB.out("gw", "conntrack", "-L", "-p", "tcp")
self.assertRegex(table, r"src=10\.200\.50\.10 .*sport=40000")
self.assertRegex(table, r"src=10\.200\.50\.11 .*sport=40000")
def test_private_address_is_unreachable_from_outside(self):
self.load_masquerade()
capture = LAB.capture("ext", "eth1", "icmp", 1, flags=("-n",))
done = LAB.sh("ext", "curl", "-sS", "--connect-timeout", "2",
"http://10.200.50.10:8080/")
self.assertNotEqual(done.returncode, 0)
self.assertIn("10.200.50.10 unreachable", capture.result())
def test_public_address_refuses_until_port_is_redirected(self):
self.load_masquerade()
LAB.daemon("a1", "nc -lk -p 8080 -e /bin/echo hola-desde-a1")
time.sleep(0.5)
capture = LAB.capture("gw", "eth2", "tcp port 8080", 2, flags=("-n",))
before = LAB.sh("ext", "curl", "-sS", "--connect-timeout", "2",
f"http://{GW_PUBLIC}:8080/")
self.assertNotEqual(before.returncode, 0)
self.assertIn("Flags [R.]", capture.result())
LAB.load_nft("gw", os.path.join(HERE, "nat", "dnat_demo.nft"))
after = LAB.sh("ext", "sh", "-c", f"echo x | nc -w2 {GW_PUBLIC} 8080")
self.assertIn("hola-desde-a1", after.stdout)
table = LAB.out("gw", "conntrack", "-L", "-p", "tcp", "--dport", "8080")
self.assertRegex(table, rf"dst={GW_PUBLIC} sport=\d+ dport=8080 src=10\.200\.50\.10")
def test_lab_step_5_port_forward(self):
path = answer_path("portforward.nft")
self.load_masquerade()
LAB.load_nft("gw", path)
LAB.daemon("a2", "nc -lk -p 8000 -e /bin/echo hola-desde-a2")
time.sleep(0.5)
done = LAB.sh("ext", "sh", "-c", f"echo x | nc -w2 {GW_PUBLIC} 2222")
self.assertIn("hola-desde-a2", done.stdout)
# a1 no recibe nada por ese puerto, y sigue saliendo con NAT.
self.assertEqual(LAB.sh("a1", "ping", "-c", "1", "-W", "2", EXT).returncode, 0)
if __name__ == "__main__":
unittest.main()"""Pruebas de los programas en C: cidr, lpm e ipv4_header."""
import os
import subprocess
import tempfile
import unittest
from netlab import HERE
# Encabezado capturado en r1 (eth1): TTL 64, antes del forwarding.
CAPTURED = "\t0x0000: 4500 0054 73b5 4000 4001 ad50 0ac8 010a\n" \
"\t0x0010: 0ac8 030a 0800 5973 0099 0000 e30f bae3\n"
def tool(name, *args, stdin=None):
return subprocess.run([os.path.join(HERE, name), *args], capture_output=True, text=True,
input=stdin, timeout=10)
def internet_checksum(header):
"""Suma de complemento a uno de palabras de 16 bits (solo para verificar la captura)."""
total = sum(int.from_bytes(header[i:i + 2], "big") for i in range(0, len(header), 2))
while total >> 16:
total = (total & 0xffff) + (total >> 16)
return ~total & 0xffff
class CidrTest(unittest.TestCase):
def test_info_of_22(self):
out = tool("cidr", "10.20.0.0/22").stdout
self.assertIn("255.255.252.0", out)
self.assertIn("10.20.3.255", out)
self.assertIn("(1022)", out)
def test_split_guide_example(self):
out = tool("cidr", "10.20.0.0/22", "4").stdout
for sub in ("10.20.0.0/24", "10.20.1.0/24", "10.20.2.0/24", "10.20.3.0/24"):
self.assertIn(sub, out)
def test_host_bits_are_reported(self):
out = tool("cidr", "10.1.2.3/24").stdout
self.assertIn("bits de host", out)
self.assertIn("10.1.2.0/24", out)
def test_edges(self):
self.assertIn("255.255.255.254", tool("cidr", "10.0.0.0/31").stdout)
self.assertIn("(2)", tool("cidr", "10.0.0.0/31").stdout)
self.assertIn("(1)", tool("cidr", "10.0.0.1/32").stdout)
self.assertIn("0.0.0.0", tool("cidr", "0.0.0.0/0").stdout)
def test_invalid_input(self):
self.assertEqual(tool("cidr", "10.0.0.0/33").returncode, 2)
self.assertEqual(tool("cidr", "300.0.0.0/8").returncode, 2)
self.assertEqual(tool("cidr", "10.0.0.0").returncode, 2)
self.assertEqual(tool("cidr", "10.0.0.0/24", "3").returncode, 1)
self.assertEqual(tool("cidr", "10.0.0.0/31", "4").returncode, 1)
class LpmTest(unittest.TestCase):
TABLE = os.path.join(HERE, "table.txt")
def winner(self, address, table=None):
out = tool("lpm", table or self.TABLE, address).stdout
lines = [line for line in out.splitlines() if "gana" in line or "sin ruta" in line]
return lines[0].split()[-1]
def test_chapter_example(self):
self.assertEqual(self.winner("10.200.5.9"), "C")
self.assertEqual(self.winner("10.200.6.1"), "B")
self.assertEqual(self.winner("10.200.9.1"), "A")
def test_default_route(self):
self.assertEqual(self.winner("100.64.9.10"), "D")
def test_order_does_not_matter(self):
with open(self.TABLE, encoding="utf-8") as file:
lines = file.read().splitlines()
with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False) as other:
other.write("\n".join(reversed(lines)) + "\n")
try:
self.assertEqual(self.winner("10.200.5.9", other.name), "C")
finally:
os.unlink(other.name)
def test_no_route_without_default(self):
with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False) as other:
other.write("10.0.0.0/8 A\n")
try:
self.assertIn("sin ruta", tool("lpm", other.name, "11.0.0.1").stdout)
finally:
os.unlink(other.name)
def test_rejects_prefix_with_host_bits(self):
with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False) as other:
other.write("10.1.2.3/24 A\n")
try:
self.assertEqual(tool("lpm", other.name, "10.1.2.3").returncode, 2)
finally:
os.unlink(other.name)
class HeaderTest(unittest.TestCase):
def test_fields_of_captured_packet(self):
out = tool("ipv4_header", stdin=CAPTURED).stdout
self.assertRegex(out, r"TTL\s+64")
self.assertRegex(out, r"DF=1 MF=0")
self.assertRegex(out, r"largo total\s+84 bytes")
self.assertRegex(out, r"protocolo\s+1 \(ICMP\)")
self.assertRegex(out, r"origen\s+10\.200\.1\.10")
self.assertRegex(out, r"destino\s+10\.200\.3\.10")
def test_rejects_short_and_non_ipv4(self):
self.assertEqual(tool("ipv4_header", stdin="\t0x0000: 4500 0054\n").returncode, 1)
ipv6 = "\t0x0000: 6000 0000 0040 3a40 2001 0db8 0001 0000\n" \
"\t0x0010: 0000 0000 0000 0010 2001 0db8 0001 0000\n"
self.assertEqual(tool("ipv4_header", stdin=ipv6).returncode, 1)
def test_checksum_changes_by_one_hundred_hex_when_ttl_drops(self):
before = bytes.fromhex("4500005473b540004001" "ad50" "0ac8010a0ac8030a")
after = bytes.fromhex("4500005473b540003f01" "ae50" "0ac8010a0ac8030a")
# Un encabezado correcto, con su checksum incluido, suma 0xffff: el
# complemento da 0.
self.assertEqual(internet_checksum(before), 0)
self.assertEqual(internet_checksum(after), 0)
self.assertEqual(0xae50 - 0xad50, 0x100)
if __name__ == "__main__":
unittest.main()Laboratorio
Archivos de partida del laboratorio de la semana.
#!/bin/sh
# Paso 6 del laboratorio: fwd/ipv6.sh deja IPv6 hasta r3. Completen lo que falta
# para que h1 haga ping a h3 en 2001:db8:3::10. Se corre después de fwd/ipv6.sh.
# Cuando terminen, borren la línea TODO: make test usa este archivo.
set -e
P=clab-w04-fwd
# TODO: direcciones y rutas que faltan# Paso 5 del laboratorio: quien se conecte desde afuera al puerto 2222 de gw
# (100.64.0.2) debe llegar al puerto 8000 de a2. Se carga encima de
# nat/masquerade.nft con: docker exec -i clab-w04-nat-gw nft -f - < lab/portforward.nft
# Cuando terminen, borren la línea TODO: make test usa este archivo.
# TODO: una tabla con la cadena y la regla#!/bin/sh
# Paso 1 del laboratorio: agreguen las rutas que faltan para que h1, h2 y h3 se
# alcancen entre sí. Se corre en el contenedor del curso, con fwd.clab.yml levantada.
# Cuando terminen, borren la línea TODO: make test usa este archivo.
set -e
P=clab-w04-fwd
# Ejemplo de la forma que tiene cada ruta:
# docker exec $P-r1 vtysh -c 'conf t' -c 'ip route PREFIJO SIGUIENTE-SALTO'
# TODO: rutas de r1, r2 y r3Compilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 4: IP, forwarding y NAT. Se usa dentro del contenedor del curso.
#
# make compila cidr, lpm e ipv4_header
# make test compila, levanta las dos 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-fwd levanta la topología de tres routers (fwd/fwd.clab.yml)
# make up-nat levanta la red con NAT (nat/nat.clab.yml)
# make down destruye las dos 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 = cidr lpm ipv4_header
.PHONY: all test up-fwd up-nat 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-fwd:
cd fwd && containerlab deploy -t fwd.clab.yml --reconfigure
up-nat:
cd nat && containerlab deploy -t nat.clab.yml --reconfigure
down:
@cd fwd && containerlab destroy -t fwd.clab.yml --cleanup >/dev/null 2>&1 || true
@cd nat && containerlab destroy -t nat.clab.yml --cleanup >/dev/null 2>&1 || true
@docker network rm redes-w04-fwd >/dev/null 2>&1 || true
@docker network rm redes-w04-nat >/dev/null 2>&1 || true
clean: down
rm -f $(PROGRAMS)
rm -rf __pycache__ fwd/clab-w04-fwd nat/clab-w04-nat