Debian 13 Ruteo con Doble NIC: Evitar Rutas Asimétricas y Caídas Aleatorias (Caso #53)

¿Te fue útil?

Las máquinas Debian con dos NIC fallan de una manera muy específica y muy irritante: todo “funciona” hasta que deja de hacerlo. SSH se queda colgado 20 segundos,
las comprobaciones de salud fluctúan, la replicación de almacenamiento se detiene y los flujos TCP se reinician sin un patrón salvo “empeora en horas punta”.

El culpable suele ser el ruteo asimétrico: los paquetes entran por la NIC A y salen por la NIC B, así que dispositivos stateful (o tu propio kernel) deciden que estás mintiendo.
Este es el caso #53: dos interfaces, dos gateways, un servidor y un montón de caídas aleatorias que no puedes reproducir a demanda.

La forma de la falla: cómo se ve la asimetría en producción

Las NIC dobles deberían ser anodinas. Una NIC para “frontend”, otra para “backend”, o quizá una para gestión y otra para almacenamiento.
En el mundo real, el límite se difumina. Alguien añade una segunda ruta por defecto “por si acaso”. O un daemon de ruteo aprende algo
que no querías anunciar. O conectas ambas NIC a redes que tienen sus propias opiniones sobre tus paquetes.

El ruteo asimétrico no es “malo” en abstracto. Internet funciona con asimetría todo el día. El problema es la asimetría a través de
puntos de estrangulamiento stateful: firewalls, NAT, balanceadores, conntrack, dispositivos DSR y a veces el propio kernel Linux
cuando el filtrado por ruta inversa está activado de forma agresiva.

Síntomas típicos:

  • Reinicios TCP intermitentes, sobre todo en flujos de larga duración (replicación, bases de datos, iSCSI/NFS, streaming de API).
  • SSH a veces se queda bloqueado justo después de iniciar sesión o durante scp; reintentos “lo arreglan”.
  • El monitor muestra pérdida de paquetes, pero solo desde ciertas subredes de origen.
  • El tráfico entrante llega por una interfaz, pero las respuestas salen por la otra (visto con tcpdump).
  • Registros del kernel como martian source o respuestas descartadas silenciosamente cuando rp_filter está estricto.

Si tienes dos gateways por defecto en la misma máquina y no tienes policy routing, no tienes redundancia.
Tienes una moneda al aire con consecuencias.

Datos y contexto interesantes (sí, las redes tienen tradición)

  1. El policy routing en Linux es anterior a muchos patrones “modernos” de la nube. El framework ip rule llegó a finales de los 90 con Linux 2.2 y maduró en 2.4/2.6.
  2. El filtrado por ruta inversa (rp_filter) se popularizó para combatir el spoofing. Es una característica de seguridad que se convierte en una trampa en hosts multi-homed.
  3. El ruteo asimétrico no es inherentemente incorrecto. Se vuelve problemático cuando un middlebox espera ver ambos sentidos de un flujo por la misma ruta.
  4. Linux tiene múltiples mecanismos de selección de ruta. La coincidencia de prefijo más largo ocurre dentro de una tabla; luego las reglas deciden qué tabla consultar y cuándo.
  5. El ARP flux es real. Si Linux responde ARP por la “interfaz equivocada”, los pares envían tráfico al MAC incorrecto y persigues fantasmas.
  6. ECMP (equal-cost multipath) puede parecer “caídas aleatorias”. Es determinista por hash de flujo, pero la aplicación lo ve como caos cuando los middleboxes no están de acuerdo.
  7. Conntrack no es solo un detalle de firewall. Incluso reglas “permitir todo” stateful dependen de conntrack; el tráfico de retorno fuera de ruta se etiqueta como INVALID y se descarta.
  8. Systemd-networkd cambió la experiencia por defecto para muchos admins. Debian 13 facilita producir múltiples rutas por defecto a menos que actúes deliberadamente.

Guía rápida de diagnóstico

Cuando los paquetes “caen aleatoriamente”, no tienes tiempo para debates filosóficos de redes. Quieres un embudo rápido:
confirmar asimetría, identificar quién descarta y luego hacer el ruteo determinista.

1) Probar (o descartar) ruteo asimétrico en 5 minutos

  • Revisa tablas y reglas de ruteo: ip route, ip rule.
  • Captura ingreso/egreso en ambas NIC durante un flujo que falla: tcpdump en ambas interfaces.
  • Confirma la selección de IP de origen: ip route get <dest> from <src>.

2) Encuentra quién descarta: kernel, firewall o red

  • Busca rp_filter/martians: journalctl -k, sysctl net.ipv4.conf.*.rp_filter.
  • Busca descartes INVALID de conntrack: nft list ruleset y contadores, o iptables -L -v si aún vives peligrosamente.
  • Revisa errores/descartes de NIC: ip -s link, ethtool -S.

3) Haz el ruteo determinista (no “ajustes” primero)

  • Elige un egress por cada subred de origen usando policy routing.
  • Configura rp_filter en loose (2) en hosts multi-homed a menos que sepas exactamente qué estás aplicando.
  • Deja de anunciar/usar dos rutas por defecto sin métricas y reglas.

Broma #1: Si tu ruteo depende de “el gateway que se sienta afortunado hoy”, enhorabuena: has inventado balanceo de carga, pero sin sus beneficios.

Modelo mental: ruteo Linux, reglas y por qué Debian 13 te sorprende

Depurar ruteo con doble NIC implica entender tres capas de toma de decisiones:
selección de dirección, consulta de tabla de ruteo y reglas de policy routing.
La mayoría de las outages vienen de suponer que Linux “simplemente responderá por la misma interfaz por la que llegó”.
Esa suposición es adorable. También falsa.

Selección de ruta: tablas

Linux mantiene tablas de ruteo. La mayoría de sistemas usan la tabla main por defecto. Cuando ejecutas ip route,
normalmente estás viendo la tabla main. Gana la coincidencia de prefijo más largo, y si hay múltiples rutas iguales, aplican métricas y reglas ECMP.

Los problemas de doble NIC empiezan cuando ambas NIC añaden una ruta por defecto a main. Linux entonces elige un egress
basado en métricas (si difieren) o ECMP (si son iguales). Eso puede ser estable por flujo, pero no se alineará con las expectativas de tu red.

Policy routing: reglas

ip rule te permite elegir qué tabla consultar según propiedades del paquete: dirección de origen, fwmark, interfaz de entrada,
TOS, rangos de UID y más. En la práctica, para servidores con doble NIC, el ruteo basado en origen es la herramienta:
“Tráfico originado desde 192.0.2.10 usa la tabla 100, que tiene default via gateway A.”

Filtrado por ruta inversa: rp_filter

rp_filter verifica si la dirección de origen de un paquete entrante es alcanzable a través de la interfaz por la que llegó.
Con modo estricto (1), la multi-conectividad puede romperse porque la “mejor ruta de regreso” puede ser por la otra NIC.
El modo loose (2) es típicamente lo que quieres en hosts multi-homed: verifica alcanzabilidad, pero no necesariamente por la misma interfaz.

Conntrack y filtrado stateful

Si usas nftables/iptables con reglas stateful (la mayoría lo hace, a veces sin darse cuenta), la asimetría puede hacer que los paquetes de retorno sean vistos como
INVALID porque conntrack no vio la dirección original por ese camino. Entonces un paquete perfectamente válido se descarta.
El paquete no está “malo”. Tu topología lo está.

ARP y selección de vecinos

Otro villano silencioso es el comportamiento ARP en hosts con múltiples interfaces en el mismo dominio L2 o prefijos superpuestos.
Linux podría responder ARP en una interfaz con el MAC de otra, o elegir una dirección de origen que confunda a los peers.
El resultado: el tráfico llega donde no esperabas, y tu “arreglo de ruteo” se convierte en una sesión de whack-a-mole.

Tareas prácticas (comandos, salidas, decisiones)

Las siguientes tareas están escritas como si estuvieras de guardia y el pager aún estuviera caliente. Cada una incluye:
un comando, un fragmento realista de salida, lo que significa y la decisión que tomas a partir de ello.

Task 1 — Inventario de interfaces y direcciones

cr0x@server:~$ ip -br addr
lo               UNKNOWN        127.0.0.1/8 ::1/128
enp1s0           UP             192.0.2.10/24 fe80::a00:27ff:fe12:3456/64
enp2s0           UP             198.51.100.10/24 fe80::a00:27ff:fe65:4321/64

Significado: Dos NIC, dos subredes IPv4. Esto está bien. Deja de estar bien cuando ambas se proclaman “internet” (dos defaults).

Decisión: Confirma para qué sirve cada subred (frontend/backend/gestión). Escríbelo. Si nadie lo sabe, ya estás en problemas.

Task 2 — Revisar la tabla main buscando múltiples defaults

cr0x@server:~$ ip route show table main
default via 192.0.2.1 dev enp1s0 proto dhcp metric 100
default via 198.51.100.1 dev enp2s0 proto dhcp metric 100
192.0.2.0/24 dev enp1s0 proto kernel scope link src 192.0.2.10 metric 100
198.51.100.0/24 dev enp2s0 proto kernel scope link src 198.51.100.10 metric 100

Significado: Dos defaults con la misma métrica. Linux puede ECMP esto. Tu firewall probablemente no pueda.

Decisión: Necesitas policy routing o un default único. No “solo cambies métricas” a menos que estés de acuerdo con el comportamiento de failover y lo pruebes.

Task 3 — Inspeccionar reglas de policy routing

cr0x@server:~$ ip rule show
0:      from all lookup local
32766:  from all lookup main
32767:  from all lookup default

Significado: No hay reglas de policy routing. Todo consulta main, así que la asimetría está en el menú.

Decisión: Añade reglas basadas en origen y tablas por interfaz (o elimina la ruta por defecto extra).

Task 4 — Pregunta a Linux “cómo rutarías esto” para cada origen

cr0x@server:~$ ip route get 203.0.113.50 from 192.0.2.10
203.0.113.50 via 198.51.100.1 dev enp2s0 src 192.0.2.10 uid 0
    cache

Significado: El tráfico con origen 192.0.2.10 saldría por enp2s0. Eso alimenta la asimetría clásica.

Decisión: Implementa “desde 192.0.2.10 usar gateway 192.0.2.1” vía policy routing.

Task 5 — Revisar rp_filter (modo estricto es un asesino silencioso frecuente)

cr0x@server:~$ sysctl net.ipv4.conf.all.rp_filter net.ipv4.conf.enp1s0.rp_filter net.ipv4.conf.enp2s0.rp_filter
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.enp1s0.rp_filter = 1
net.ipv4.conf.enp2s0.rp_filter = 1

Significado: El filtrado por ruta inversa estricto está habilitado. Con multi-homing, a menudo descarta paquetes legítimos.

Decisión: Poner en modo loose (2) o desactivar (0) según tu modelo de amenazas y topología. Loose es el valor sensato para ruteo con doble NIC.

Task 6 — Buscar pistas del kernel: martians, descartes rp_filter, rarezas de vecinos

cr0x@server:~$ journalctl -k --since "2 hours ago" | tail -n 8
Dec 30 09:11:04 server kernel: IPv4: martian source 203.0.113.50 from 203.0.113.50, on dev enp1s0
Dec 30 09:11:04 server kernel: ll header: 00000000: 00 1b 21 22 33 44 00 1b 21 aa bb cc 08 00
Dec 30 09:12:18 server kernel: nf_conntrack: table full, dropping packet

Significado: “Martian source” suele correlacionar con rp_filter o inconsistencia de ruteo. También: conntrack lleno, lo que causa descartes que parecen “aleatorios”.

Decisión: Arregla ruteo primero. Luego ajusta el dimensionamiento de conntrack si es necesario. Si conntrack está lleno, no estás depurando ruteo: estás depurando sobrecarga también.

Task 7 — Comprobar utilización de conntrack

cr0x@server:~$ sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max
net.netfilter.nf_conntrack_count = 262144
net.netfilter.nf_conntrack_max = 262144

Significado: Estás en el techo. Los flujos nuevos se descartan o pasan según las reglas. En cualquier caso: dolor.

Decisión: Si este host maneja muchas conexiones (proxies, NAT, API ocupada), aumenta el máximo y confirma memoria disponible. También reduce timeouts de idle donde corresponda.

Task 8 — Inspeccionar reglas nftables y contadores por descartes INVALID

cr0x@server:~$ nft list ruleset | sed -n '1,120p'
table inet filter {
  chain input {
    type filter hook input priority 0; policy drop;
    ct state established,related accept
    ct state invalid counter packets 1843 bytes 110580 drop
    iif "lo" accept
    tcp dport 22 accept
  }
}

Significado: INVALID se está descartando y el contador crece. La asimetría es una de las causas principales.

Decisión: No “aceptes INVALID” como parche. Arregla el ruteo para que los paquetes sean consistentemente rastreados.

Task 9 — Revisar estadísticas de interfaz para descartes reales vs descartes por ruteo

cr0x@server:~$ ip -s link show dev enp1s0
2: enp1s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 00:1b:21:aa:bb:cc brd ff:ff:ff:ff:ff:ff
    RX:  bytes  packets  errors  dropped  missed   mcast
     91433921  812334   0       0        0        1290
    TX:  bytes  packets  errors  dropped  carrier  collsns
     88122301  799221   0       0        0        0

Significado: No hay descartes a nivel de NIC. Así que las “caídas aleatorias” probablemente ocurren en política/firewall/conntrack, o en upstream.

Decisión: Mantente enfocado: reglas de ruteo, rp_filter, conntrack, estado del firewall.

Task 10 — Capturar tráfico en ambas NIC para probar asimetría

cr0x@server:~$ sudo tcpdump -ni enp1s0 host 203.0.113.50 and tcp port 443 -c 6
09:14:22.110001 IP 203.0.113.50.443 > 192.0.2.10.53122: Flags [S.], seq 1200, ack 900, win 65160, options [mss 1460], length 0
09:14:22.110050 IP 203.0.113.50.443 > 192.0.2.10.53122: Flags [.], ack 901, win 65160, length 0
cr0x@server:~$ sudo tcpdump -ni enp2s0 host 203.0.113.50 and tcp port 443 -c 6
09:14:22.110120 IP 192.0.2.10.53122 > 203.0.113.50.443: Flags [.], ack 1201, win 501, length 0
09:14:22.110200 IP 192.0.2.10.53122 > 203.0.113.50.443: Flags [P.], seq 901:1041, ack 1201, win 501, length 140

Significado: SYN/ACK llegó por enp1s0, pero ACK/datos salieron por enp2s0. Eso es asimetría, probado con recibos.

Decisión: Implementa policy routing para que las respuestas al tráfico con destino 192.0.2.10 salgan por enp1s0 (o asegúrate de que el camino upstream sea simétrico, lo cual es más difícil).

Task 11 — Revisar comportamiento de vecinos/ARP por flux

cr0x@server:~$ ip neigh show nud reachable,stale | head
192.0.2.1 dev enp1s0 lladdr 00:11:22:33:44:55 REACHABLE
198.51.100.1 dev enp2s0 lladdr 00:11:22:33:44:66 STALE

Significado: Nada obviamente roto aquí. Pero si ves la misma IP de vecino reachable vía dos interfaces, o muchas entradas FAILED, sospecha ARP flux o problemas de diseño L2.

Decisión: Si las redes se solapan o comparten VLAN, aplica controles ARP (sección posterior) y arregla el direccionamiento.

Task 12 — Verificar ruteo por origen una vez añadido el policy routing

cr0x@server:~$ ip route get 203.0.113.50 from 192.0.2.10
203.0.113.50 via 192.0.2.1 dev enp1s0 src 192.0.2.10 uid 0
    cache

Significado: Ahora el egress coincide con la subred de origen. Este es el comportamiento determinista que quieres.

Decisión: Vuelve a ejecutar la validación con tcpdump y observa los contadores de nft. Si los descartes INVALID dejan de subir, acabas de comprar estabilidad.

Task 13 — Confirmar que no quedan defaults ECMP accidentales

cr0x@server:~$ ip route show default
default via 192.0.2.1 dev enp1s0 proto static metric 100
default via 198.51.100.1 dev enp2s0 proto static metric 200

Significado: Aún puedes mantener dos defaults con métricas distintas para failover, pero tus reglas de policy deben ser consistentes.

Decisión: Prefiere un default único en main, y coloca el otro default solo en una tabla dedicada. Los modelos mixtos confunden al tú-del-futuro.

Task 14 — Revisar el estado de systemd-networkd (realidad Debian 13)

cr0x@server:~$ networkctl status enp1s0 | sed -n '1,40p'
● 2: enp1s0
                 Link File: /usr/lib/systemd/network/99-default.link
              Network File: /etc/systemd/network/10-enp1s0.network
                      State: routable (configured)
               Online state: online
                    Address: 192.0.2.10/24
                    Gateway: 192.0.2.1

Significado: networkd está gestionando tu ruteo. Eso es bueno si lo configuras intencionalmente, y caótico si dejas que DHCP disperse defaults.

Decisión: Pon el policy routing en la configuración de networkd para que sobreviva a un reinicio y no dependa de un pegado heroico del on-call.

Patrones de solución que realmente funcionan

Patrón A: Policy routing basado en origen (recomendado para la mayoría de servidores con doble NIC)

Objetivo: el tráfico originado desde la IP de la interfaz A usa el gateway de la interfaz A; el tráfico originado desde la IP de la interfaz B usa el gateway de la interfaz B.
Esto detiene las respuestas asimétricas sin necesitar cambios upstream.

Lo implementas con:

  • Dos tablas de ruteo (una por NIC)
  • Dos entradas ip rule que coincidan con subredes de origen
  • Rutas en cada tabla: subred conectada + default vía su gateway

Configuración inmediata (runtime) con iproute2

cr0x@server:~$ sudo ip route add 192.0.2.0/24 dev enp1s0 src 192.0.2.10 table 100
cr0x@server:~$ sudo ip route add default via 192.0.2.1 dev enp1s0 table 100
cr0x@server:~$ sudo ip route add 198.51.100.0/24 dev enp2s0 src 198.51.100.10 table 200
cr0x@server:~$ sudo ip route add default via 198.51.100.1 dev enp2s0 table 200
cr0x@server:~$ sudo ip rule add from 192.0.2.10/32 table 100 priority 1000
cr0x@server:~$ sudo ip rule add from 198.51.100.10/32 table 200 priority 1001

Esto funciona, pero desaparece al reiniciar a menos que lo persistas. No seas esa persona.

Configuración persistente con systemd-networkd (amigable con Debian 13)

Ejemplo /etc/systemd/network/10-enp1s0.network:

cr0x@server:~$ sudo sed -n '1,200p' /etc/systemd/network/10-enp1s0.network
[Match]
Name=enp1s0

[Network]
Address=192.0.2.10/24
Gateway=192.0.2.1
DNS=192.0.2.53

[RoutingPolicyRule]
From=192.0.2.10/32
Table=100
Priority=1000

[Route]
Destination=0.0.0.0/0
Gateway=192.0.2.1
Table=100

Y /etc/systemd/network/20-enp2s0.network:

cr0x@server:~$ sudo sed -n '1,200p' /etc/systemd/network/20-enp2s0.network
[Match]
Name=enp2s0

[Network]
Address=198.51.100.10/24
Gateway=198.51.100.1
DNS=198.51.100.53

[RoutingPolicyRule]
From=198.51.100.10/32
Table=200
Priority=1001

[Route]
Destination=0.0.0.0/0
Gateway=198.51.100.1
Table=200

Luego reinicia networkd:

cr0x@server:~$ sudo systemctl restart systemd-networkd
cr0x@server:~$ ip rule show | sed -n '1,10p'
0:      from all lookup local
1000:   from 192.0.2.10 lookup 100
1001:   from 198.51.100.10 lookup 200
32766:  from all lookup main
32767:  from all lookup default

Patrón B: Un default, una red “especial” (mejor cuando una NIC es realmente privada)

Si enp2s0 es estrictamente una red de almacenamiento y nunca debe usarse para internet o respuestas a clientes, no le des una ruta por defecto.
Dale solo la ruta conectada y quizá unas rutas explícitas a peers de almacenamiento.

Esto elimina una clase entera de outages. La interfaz se convierte en “tubería tonta hacia la subred X”.
La política de ruteo mejor es la que no necesitas.

Patrón C: Ruteo basado en fwmark (para apps complejas y VIPs)

Si tienes múltiples direcciones de origen en una interfaz (VIPs), contenedores o necesitas encaminar solo cierto tráfico,
puedes marcar paquetes en nftables y rutear basándote en fwmark.
Esto es más potente y propenso a errores. Úsalo cuando las reglas basadas en origen no sean suficientes.

rp_filter: configúralo deliberadamente, no por superstición

En hosts multi-homed, rp_filter estricto suele ser incompatible con policy routing y asimetría legítima.
El modo loose es el compromiso habitual: aún requiere una ruta de regreso a la fuente, pero no necesariamente por la misma interfaz.

cr0x@server:~$ sudo sysctl -w net.ipv4.conf.all.rp_filter=2
net.ipv4.conf.all.rp_filter = 2
cr0x@server:~$ sudo sysctl -w net.ipv4.conf.default.rp_filter=2
net.ipv4.conf.default.rp_filter = 2
cr0x@server:~$ sudo sysctl -w net.ipv4.conf.enp1s0.rp_filter=2
net.ipv4.conf.enp1s0.rp_filter = 2
cr0x@server:~$ sudo sysctl -w net.ipv4.conf.enp2s0.rp_filter=2
net.ipv4.conf.enp2s0.rp_filter = 2

Persiste vía /etc/sysctl.d/99-multihome.conf:

cr0x@server:~$ sudo tee /etc/sysctl.d/99-multihome.conf >/dev/null <<'EOF'
net.ipv4.conf.all.rp_filter=2
net.ipv4.conf.default.rp_filter=2
EOF
cr0x@server:~$ sudo sysctl --system | tail -n 4
* Applying /etc/sysctl.d/99-multihome.conf ...
net.ipv4.conf.all.rp_filter = 2
net.ipv4.conf.default.rp_filter = 2

Controles ARP: evitar “responder por la NIC equivocada”

Si ambas NIC están en el mismo L2 o tienes rutas superpuestas, ajusta el comportamiento ARP para reducir el flux:
arp_ignore y arp_announce.
No siempre es necesario, pero cuando lo es, marca la diferencia entre la cordura y un baile interpretativo.

cr0x@server:~$ sudo sysctl -w net.ipv4.conf.all.arp_ignore=1
net.ipv4.conf.all.arp_ignore = 1
cr0x@server:~$ sudo sysctl -w net.ipv4.conf.all.arp_announce=2
net.ipv4.conf.all.arp_announce = 2

Una cita sobre fiabilidad (idea parafraseada)

Idea parafraseada (atribuida a Richard Cook): “El éxito no es la ausencia de fallos; es la presencia de capacidad adaptativa.”

Tres mini-historias del mundo corporativo

Mini-historia 1: El incidente causado por una suposición errónea

Una compañía SaaS mediana ejecutaba servidores Debian con dos NIC: una VLAN pública para tráfico de clientes y una VLAN privada para base de datos y backups.
El equipo supuso que las respuestas saldrían por la misma NIC que recibió la petición. “Lo habían visto funcionar” en laboratorio.

Un lunes, los inicios de sesión de clientes comenzaron a fluctuar. No caía por completo; solo lento, espigado y extraño. El balanceador mostraba SYN/ACKs que volvían tarde
y la mitad de los handshakes TLS fallaban. Los ingenieros persiguieron CPU, luego certificados, luego el balanceador. Los gráficos estaban todos ligeramente equivocados de diferentes maneras.

El problema real fue sencillo: una renovación DHCP en la NIC privada reintrodujo una ruta por defecto con la misma métrica que la NIC pública.
De repente, algunas respuestas al tráfico de clientes salieron por el gateway privado. El firewall perimetral las descartó porque no tenía estado para esa dirección.
Desde la perspectiva del servidor, “envió” el paquete. Desde la del cliente, el servidor desapareció.

La solución no fue heroica: fijar un default en main, usar policy routing basado en origen para la interfaz secundaria y dejar de aceptar rutas por defecto vía DHCP en la VLAN backend.
El postmortem dejó una línea para enmarcar: “Asumimos simetría; configuramos aleatoriedad.”

Mini-historia 2: La optimización que salió mal

Otra organización intentó “optimizar latencia” activando rp_filter estricto en todas partes.
Su baseline de seguridad lo trató como protección gratuita contra spoofing. Se desplegó con automatización en una flota que incluía hosts multi-homed.

Unas semanas después, la replicación de almacenamiento comenzó a caer bajo carga, pero solo entre ciertos racks. TCP establecía, transfería algo de datos
y luego se quedaba. Los reintentos funcionaban. Todos culparon al proveedor de almacenamiento. Luego culparon al MTU. Luego alguien culpó al ASIC del switch, porque siempre es el ASIC del switch cuando estás cansado.

Resultó que ya existía policy routing, pero rp_filter estricto no gustó de la consulta de ruta de retorno en ciertos casos donde la “mejor ruta” difería de la de ingreso.
El kernel descartó silenciosamente paquetes legítimos como martians. La replicación cayó de rendimiento por reintentos y timeouts, convirtiendo una discrepancia de policy routing en colapso de throughput.

La “optimización” volvió el sistema frágil. El modo loose en rp_filter restauró la estabilidad, y dejaron la protección contra spoofing donde corresponde: en el borde de la red,
con ACL explícitas, no con sysctls esperanzadores.

Mini-historia 3: La práctica aburrida pero correcta que salvó el día

Un gran equipo de plataforma interna tenía una regla: cada host multi-homed debía tener una nota de “intento de ruteo” de una página en el repositorio.
Describía qué interfaz posee qué direcciones de origen, cuál es la ruta por defecto y qué reglas de policy existen. Sin poesía. Solo verdad.

Durante una migración de datacenter, se introdujo un nuevo clúster de firewall upstream. Un subconjunto pequeño de servicios empezó a ver 502 intermitentes.
Los equipos de aplicación escalaron; el equipo de red juró que “nada cambió para esas VLANs”. Olía a problema L7, pero la pérdida de paquetes olía a L3.

El SRE de guardia abrió la nota de intento de ruteo y notó inmediatamente que el host estaba diseñado para responder por una NIC específica usando la tabla 200.
Un rápido ip rule mostró que esas reglas faltaban en la variante de la nueva imagen. El sistema arrancó bien. También arrancó mal.

Restauraron la configuración networkd prevista, redeplegaron y el problema se evaporó. Sin drama. Sin war room. Sin culpar los sentimientos del firewall.
Documentación aburrida más configuración determinista no es glamorosa, pero es la diferencia entre un incidente y un mensaje en Slack.

Broma #2: Lo único más aleatorio que el ruteo asimétrico es la reunión donde todos insisten en que “definitivamente es DNS”.

Errores comunes: síntomas → causa raíz → solución

1) SSH se queda colgado o se pausa de forma intermitente

Síntomas: SSH conecta y luego se pausa; scp se queda; los reintentos ayudan.

Causa raíz: Las respuestas salen por la NIC equivocada; firewall stateful o ACL upstream descartan el tráfico de retorno.

Solución: Policy routing basado en origen por interfaz; asegurar un solo default en main; validar con tcpdump en ambas NIC.

2) nftables muestra contadores INVALID en aumento

Síntomas: El contador ct state invalid drop sube; los usuarios ven fallos aleatorios.

Causa raíz: Conntrack ve solo una dirección por la asimetría (o la tabla conntrack está llena).

Solución: Arregla la simetría de ruteo primero; luego dimensiona conntrack correctamente y asegura que no estés rastreando tráfico masivo innecesario.

3) Aparecen registros “Martian source”

Síntomas: Kernel registra martians; paquetes aparecen en la interfaz “equivocada”.

Causa raíz: rp_filter estricto en host multi-homed, o rutas/reglas incorrectas.

Solución: Poner rp_filter en loose (2) e implementar policy routing para alinear la consulta de ruta con la realidad.

4) Funciona hasta que renueva DHCP, luego rompe

Síntomas: Cada pocas horas/días las cosas se ponen raras; reiniciar “arregla” temporalmente.

Causa raíz: DHCP instala o cambia rutas por defecto/métricas; el ruteo vuelve a ser no determinista.

Solución: Deshabilita la ruta por defecto vía DHCP en la NIC secundaria, o aísla DHCP a una interfaz; persiste rutas vía networkd.

5) Solo fallan algunas subredes remotas

Síntomas: La mayoría de clientes bien; una región/proveedor flaquea.

Causa raíz: La ruta de retorno de esos clientes golpea un upstream distinto (firewall/NAT diferente), haciendo visible la asimetría.

Solución: Forzar egress determinista por origen; asegurar que el upstream vea rutas consistentes.

6) “Hemos puesto métricas, así que está bien”

Síntomas: Mayormente estable, pero el failover o fallos parciales producen agujeros negros.

Causa raíz: Las métricas deciden preferencia, pero no garantizan que las respuestas usen la misma interfaz que la dirección de origen, especialmente con múltiples orígenes.

Solución: Usa policy routing; las métricas son para preferencia, no para corrección.

7) Tráfico de almacenamiento o replicación se desploma bajo carga

Síntomas: Throughput colapsa, retransmisiones aumentan, timeouts.

Causa raíz: Agotamiento de la tabla conntrack, o filtrado stateful más asimetría, o ambas.

Solución: Evita rastrear tráfico que no necesita tracking; dimensiona conntrack; arregla el ruteo para que los flujos sean consistentes.

8) Rarezas ARP: el tráfico llega a la NIC equivocada aun con ruteo correcto

Síntomas: Los peers envían paquetes al MAC “equivocado”; el failover se comporta raro.

Causa raíz: ARP flux: el host contesta ARP desde la interfaz equivocada; o dominios L2 superpuestos.

Solución: Separa dominios L2; ajusta arp_ignore/arp_announce; asegura subredes únicas y un diseño VLAN limpio.

Listas de verificación / plan paso a paso

Paso a paso: detener el ruteo asimétrico en Debian 13 (orden seguro para producción)

  1. Captura el estado actual (antes de tocar nada):

    • ip -br addr
    • ip route show table main
    • ip rule show
    • sysctl net.ipv4.conf.all.rp_filter

    Decisión: confirma si realmente estás multi-homed (no solo aliases) y si existen dos defaults.

  2. Prueba la asimetría con tcpdump en ambas NIC durante un flujo que falle.

    Decisión: si ingreso y egreso difieren para el mismo flujo, procede con policy routing. Si no, puede ser MTU, congestión o filtrado upstream.

  3. Decide tu intención:

    • Una NIC es el default para todo, la otra es solo privada (mejor).
    • Ambas NIC sirven clientes/peers reales y deben rutear correctamente (se requiere policy routing).

    Decisión: escribe la intención en un comentario en la config. “Lo recordaremos” no es un plan.

  4. Implementa policy routing (tablas + reglas) vía networkd o ifupdown.

    Decisión: prefiere persistencia en networkd en Debian 13 si ya está gestionando interfaces.

  5. Configura rp_filter en modo loose (2) para hosts multi-homed.

    Decisión: si seguridad exige estricto, haz que firmen el informe de incidente cuando falle. Loose es el compromiso realista.

  6. Valida con ip route get para destinos representativos desde cada dirección de origen.

    Decisión: si algún origen elige el gateway “incorrecto”, las tablas/reglas están incompletas.

  7. Vuelve a comprobar contadores del firewall (nft INVALID drops, etc.).

    Decisión: si los INVALID siguen subiendo, verifica que conntrack no esté lleno y que el tráfico no esté evitando la ruta esperada.

  8. Prueba con carga o reproduce tráfico parecido a producción.

    Decisión: si los fallos solo aparecen bajo carga, revisa dimensionamiento de conntrack, saturación de IRQ, qdisc y policing upstream—no solo ruteo.

  9. Hazlo duradero:

    • Sube los archivos de networkd y sysctl a tu gestión de configuración.
    • Documenta la intención de ruteo y una salida “conocida buena” de ip rule y ip route show table 100/200.

    Decisión: si la solución puede ser revertida por una renovación DHCP, no lo arreglaste.

Checklist operativo: verificación tras el cambio (10 minutos)

  • Confirmar reglas: ip rule show
  • Confirmar tablas: ip route show table 100, ip route show table 200
  • Confirmar rp_filter: sysctl net.ipv4.conf.all.rp_filter
  • Confirmar contadores nft estabilizados: nft list chain inet filter input (o equivalente)
  • Confirmar simetría de tráfico con tcpdump durante una transacción de prueba
  • Confirmar que no hay defaults sorpresa vía DHCP después de la renovación (o esperar la ventana de renovación)

Preguntas frecuentes

1) ¿Puedo simplemente ajustar métricas y dar por cerrado el caso?

Las métricas deciden preferencia, no corrección. No garantizan que las respuestas usen la misma interfaz que la dirección de origen,
y no solucionan las expectativas de conntrack/dispositivos stateful. Usa policy routing para corrección; usa métricas para preferencia/failover.

2) ¿Realmente necesito policy routing si las subredes son distintas?

Si solo hay una ruta por defecto y la otra interfaz no tiene default, a menudo puedes evitar policy routing.
Si ambas interfaces tienen gateways o generas tráfico desde ambas direcciones, el policy routing es el diseño seguro.

3) ¿Qué valor de rp_filter debo usar en hosts dual-NIC?

Típicamente 2 (loose). El estricto (1) con frecuencia descarta tráfico legítimo en setups multi-homed.
Desactivar (0) solo si entiendes las implicaciones de spoofing y tienes controles compensatorios.

4) ¿Por qué las caídas parecen aleatorias?

Porque las decisiones de ruteo pueden variar por flujo (ECMP hash), por entrada de caché o tras renovaciones DHCP.
Añade estado de conntrack y expectativas de firewall upstream, y obtienes fallos que dependen del timing y la forma del tráfico.

5) ¿Esto aplica también a IPv6?

Sí, pero los mecanismos difieren (rp_filter en IPv6 no es lo mismo, y las reglas de selección de origen son más ricas).
El policy routing existe para IPv6 con ip -6 rule y tablas por separado; prueba cuidadosamente porque IPv6 admite múltiples direcciones de origen por diseño.

6) Veo “nf_conntrack: table full.” ¿Eso es ruteo?

No directamente, pero crea pérdida de paquetes que se parece. Arregla la asimetría de ruteo primero, luego dimensiona conntrack.
Si rastreas millones de flujos cortos, necesitarás más capacidad y mejor estrategia de filtrado.

7) ¿Debería aceptar paquetes INVALID para evitar descartes?

No. Eso es como desactivar una alarma de humo porque es ruidosa. INVALID suele significar asimetría, desajuste de timeouts o tráfico de ataque.
Arregla el camino para que los paquetes vuelvan a ser válidos.

8) ¿Cómo manejo el failover entre gateways?

Si realmente necesitas failover, mantiene ruteo determinista por origen y usa mecanismos controlados:
ruteo dinámico (BFD/FRR), rutas trackeadas o cambios explícitos de métricas con automatización. Evita “dos defaults iguales” a menos que quieras ECMP y entiendas el camino extremo a extremo.

9) ¿Y el bonding (LACP)? ¿No lo solucionaría?

El bonding resuelve un problema distinto: redundancia/aggregate a nivel L2. Puede ayudar si ambos enlaces están en el mismo dominio L2 y quieres una interfaz lógica.
No reemplaza el ruteo L3 correcto cuando las redes/gateways son diferentes.

10) ¿Por qué solo falla cuando añadimos un firewall?

Porque los dispositivos stateful aplican simetría a los flujos que rastrean. Sin ellos, internet puede tolerar la asimetría.
Una vez introduces estado, la ruta forma parte del contrato.

Siguientes pasos que deberías tomar esta semana

Arreglar el caso #53 no es solo hacer que las caídas paren hoy. Es asegurarte de que el próximo cambio no las resucite.
Aquí tienes la lista práctica de tareas que sobrevive la rotación de personal y los experimentos de red “temporales”.

  1. Decide y documenta la intención de ruteo para cada host dual-NIC: qué NIC posee qué tráfico y por qué.
  2. Elimina defaults dobles accidentales: un default en main, o policy routing explícito con tablas separadas.
  3. Configura rp_filter conscientemente: modo loose para multi-homing, persiste vía sysctl.d.
  4. Valida con tres herramientas: ip route get, tcpdump en ambas NIC y contadores del firewall (nft/conntrack).
  5. Haz la config persistente en systemd-networkd (o tu gestor de red elegido) y súbela a la automatización.
  6. Vigila conntrack si eres stateful: capacidad, timeouts y si estás rastreando tráfico que no necesitas.
  7. Haz un ensayo post-cambio: simula un flap de gateway si afirmas tener failover, y verifica qué sucede realmente.

El ruteo con doble NIC no es difícil. Lo difícil es fingir que no existe hasta que elige un día ocupado para recordártelo.
Hazlo determinista y vuelve a resolver problemas que al menos sean interesantes.

← Anterior
Búsquedas DNS lentas en Linux: solucione systemd-resolved correctamente
Siguiente →
Pantalla negra al pasar GPU en Proxmox: 10 causas y soluciones

Deja un comentario