Mikrotik Firewall: El sútil arte del jump-zone
Llevo un par de años operando routers MikroTik en mi homelab y, si hay algo que aprendí en el proceso, es que el firewall de RouterOS es una espada de doble filo: increíblemente potente, pero fácil de convertir en un ovillo de reglas inmanejable si no tienes un patrón claro.
El problema no es que MikroTik sea malo gestionando reglas de firewall. El problema es que, como cualquier firewall basado en reglas lineales, escala mal. A las 50 reglas todo va bien. A las 200, empiezas a preguntarte si esa regla que añadiste el mes pasado no debería estar más arriba. A las 500, cualquier cambio es un acto de fe.
Aquí quiero contar la solución que encontré: firewall zone-based con jumps a chains custom. No es una característica nativa de RouterOS — es un patrón que se construye sobre interface lists y la capacidad de hacer jump a otras chains. Y una vez lo entiendes, vuelves a tus reglas viejas y te dan un poco de vergüenza ajena.
El problema de las reglas lineales
RouterOS filtra paquetes con una cadena de reglas evaluadas de arriba abajo, primera coincidencia gana. No hay goto, no hay switch, no hay funciones. Es como un if/elif/elif/.../else:
regla 1 → ¿match? → sí → ejecutar acción
regla 2 → ¿match? → no → siguiente
regla 3 → ¿match? → sí → ejecutar acción
...
regla N → ¿match? → no → default deny
Esto funciona perfectamente para redes pequeñas. Pero cuando empiezas a segmentar — VLAN de invitados, red de IoT, DMZ, red de trabajo — las reglas se multiplican. Porque para cada par de zonas (¿puede la zona A hablar con la zona B?) necesitas reglas específicas. Y si tienes N zonas, tienes potencialmente N×N pares.
El resultado típico es una chain forward con 100+ reglas donde nadie recuerda qué hace cada una, y tener miedo de tocar nada porque “si muevo esta regla, se rompe todo”.
La idea: zone-based firewalling
El zone-based firewalling es un patrón conocido en el mundo de los firewalls. La idea central es simple: agrupar interfaces en zonas con un nivel de seguridad similar, y definir reglas para el tráfico entre zonas, no para cada interfaz individual.
Piensa en las zonas como “vecindarios de confianza”:
- LAN: todo lo que confías ciegamente (tu red local, tu VPN)
- WAN: Internet, el vecino del barrio que no conoces
- DMZ: servidores que necesitan ser accesibles desde fuera, pero que no deberían poder entrar a tu casa
- IoT: dispositivos que no confías ni a ti mismo (luces inteligentes, cámaras baratas)
- Guests: invitados en tu WiFi, que solo deberían ver Internet
El firewall ahora tiene que responder a preguntas simples: “¿Puede IoT hablar con LAN?” “¿Puede Guests hablar con IoT?” “¿Puede DMZ publicar algo hacia LAN?” Mucho más razonable que “¿Puede el puerto 5 del switch hablar con la VLAN 28?”
RouterOS no tiene zonas. Pero las podemos simular.
RouterOS no tiene un concepto nativo de “zonas” como pfSense o los firewalls empresariales. Pero tiene dos cosas que, combinadas, hacen lo mismo:
1. Interface lists
Una interface list es un grupo de interfaces con un nombre. No es más que eso: un nombre y una lista de interfaces. Lo importante es que las reglas de firewall matchean por interface list, no por interfaz individual.
/interface list add name=LAN
/interface list add name=DMZ
/interface list add name=IoT
/interface list add name=Guest
/interface list add name=WAN
/interface list member add interface=bridge list=LAN
/interface list member interface=ether2 list=DMZ
/interface list member interface=vlan-iot list=IoT
/interface list member interface=vlan-guest list=Guest
/interface list member interface=pppoe-internet list=WAN
Si mañana cambias la VLAN de un dispositivo o añades una nueva interfaz, solo actualizas la interface list. Las reglas de firewall no se tocan.
2. Jump a chains custom
RouterOS permite que una regla de firewall haga jump (saltar) a otra chain. Es como una llamada a función: ejecutas la regla, el firewall salta a la chain destino, evalúa las reglas ahí, y cuando termina (o matchea algo), vuelve.
Las chains principales de firewall son dos:
input: tráfico dirigido al propio router (SSH, DNS, API…)forward: tráfico que cruza el router (de una interfaz a otra)
La chain forward es donde ocurre la magia del firewall zone-based.
Anatomía de la chain forward
Vamos a ver cómo se estructura una chain forward con el patrón zone-based. Esto es conceptual — los nombres de interfaces y reglas que pongo aquí son ilustrativos.
Fase 1: filtrado rápido
Antes de saltar a ninguna zona, la chain principal hace un filtrado barato:
# 1. Conexiones ya conocidas — fasttrack para rendimiento
/ip firewall filter add chain=forward action=fasttrack-connection \
connection-state=established,related comment="fasttrack established"
# 2. Backup: conexiones conocidas que no fasttrackearon
/ip firewall filter add chain=forward action=accept \
connection-state=established,related,untracked comment="accept established"
# 3. Basura — descartar
/ip firewall filter add chain=forward action=drop \
connection-state=invalid comment="drop invalid"
El fasttrack es importante: es un bypass del stack de firewall para conexiones ya establecidas. Puede duplicar el throughput del router. Pero cuidado: si usas fasttrack, el tráfico fasttrack no pasa por la chain mangle. Si necesitas marcar tráfico específico (como hacer split routing para un servicio concreto), necesitas excluirlo del fasttrack.
Fase 2: bloquear conexiones nuevas desde WAN sin NAT
# Bloquear conexiones entrantes nuevas desde WAN que no tengan
# un dstnat (port-forward) asociado. Sin esto, una IP mal
# configurada podría exponer servicios internos.
/ip firewall filter add chain=forward action=drop \
connection-state=new connection-nat-state=!dstnat \
in-interface-list=WAN comment="drop new from WAN without dstnat"
Esta regla es de las más importantes y menos obvias. Impide que alguien desde Internet pueda conectarse a cualquier servicio interno que no hayas publicado explícitamente con un dstnat (port-forwarding).
Fase 3: jumps a chains de zona
Aquí viene la parte bonita. Por cada par origen→destino que quieras permitir, añades un jump a una chain zone-<origen>-to-<destino>:
/ip firewall filter add chain=forward action=jump \
in-interface-list=LAN out-interface-list=LAN \
jump-target=zone-LAN-to-LAN comment="LAN→LAN"
/ip firewall filter add chain=forward action=jump \
in-interface-list=LAN out-interface-list=WAN \
jump-target=zone-LAN-to-WAN comment="LAN→WAN"
/ip firewall filter add chain=forward action=jump \
in-interface-list=LAN out-interface-list=DMZ \
jump-target=zone-LAN-to-DMZ comment="LAN→DMZ"
/ip firewall filter add chain=forward action=jump \
in-interface-list=DMZ out-interface-list=WAN \
jump-target=zone-DMZ-to-WAN comment="DMZ→WAN"
/ip firewall filter add chain=forward action=jump \
in-interface-list=IoT out-interface-list=WAN \
jump-target=zone-IoT-to-WAN comment="IoT→WAN"
/ip firewall filter add chain=forward action=jump \
in-interface-list=Guest out-interface-list=WAN \
jump-target=zone-Guest-to-WAN comment="Guest→WAN"
# ... más jumps para los pares que necesites
Fíjate en el patrón: cada regla de jump tiene dos condiciones: in-interface-list (de dónde viene) y out-interface-list (hacia dónde va). Esto define exactamente qué par de zonas representa.
Y lo más importante: si un par de zonas no tiene un jump, el tráfico entre esas zonas se cae en el default deny final. No necesitas una regla explícita de “deny” para cada par prohibido. La ausencia de jump ya es el deny.
Fase 4: default deny
/ip firewall filter add chain=forward action=drop \
comment="default deny"
Esta regla al final captura todo lo que no matcheó antes. Es la regla más importante del firewall. Sin ella, todo lo no matcheado pasaría. Con ella, solo pasa lo que explícitamente permitiste.
Las chains zone--to-
Cada chain zone-X-to-Y contiene las reglas específicas para ese par de zonas. Vamos a ver ejemplos conceptuales:
zone-LAN-to-LAN (todo permitido)
/ip firewall filter add chain=zone-LAN-to-LAN action=accept \
comment="LAN→LAN todo permitido"
Dentro de LAN, confiamos. No necesitamos reglas. Un accept y punto.
zone-LAN-to-WAN (todo permitido)
/ip firewall filter add chain=zone-LAN-to-WAN action=accept \
comment="LAN→WAN todo permitido"
LAN puede salir a Internet sin restricciones.
zone-IoT-to-WAN (solo Internet, nada de LAN)
# IoT puede salir a Internet
/ip firewall filter add chain=zone-IoT-to-WAN action=accept \
comment="IoT→WAN permitido"
# IoT NO puede ir a LAN — simplemente no hay regla,
# y el default deny se encarga del resto
Fíjate en lo elegante: para prohibir IoT→LAN, no hacemos nada. La ausencia de un jump IoT→LAN en la chain forward ya hace el trabajo. El default deny al final se encarga del resto.
zone-DMZ-to-WAN (servidores con salida a Internet)
/ip firewall filter add chain=zone-DMZ-to-WAN action=accept \
comment="DMZ→WAN permitido"
zone-Guest-to-WAN (solo Internet, sin acceso a nada interno)
/ip firewall filter add chain=zone-Guest-to-WAN action=accept \
comment="Guest→WAN permitido"
zone-DMZ-to-LAN (restringido — solo puertos específicos)
# La DMZ puede contactar a LAN solo por HTTP/HTTPS
/ip firewall filter add chain=zone-DMZ-to-LAN action=accept \
protocol=tcp dst-port=80,443 \
comment="DMZ→LAN solo HTTP/HTTPS"
Aquí sí necesitamos reglas explícitas porque queremos un subset, no todo ni nada.
zone-LAN-to-DMZ (todo permitido)
/ip firewall filter add chain=zone-LAN-to-DMZ action=accept \
comment="LAN→DMZ permitido"
zone-IoT-to-LAN (prohibido — no hay regla)
# No hay jump para IoT→LAN → default deny
zone-Guest-to-LAN (prohibido — no hay regla)
# No hay jump para Guest→LAN → default deny
Anatomía de la chain input
La chain input es similar en concepto pero más simple, porque solo hay un destino: el propio router.
# 1. Conexiones establecidas
/ip firewall filter add chain=input action=accept \
connection-state=established,related,untracked \
comment="input established"
# 2. Basura
/ip firewall filter add chain=input action=drop \
connection-state=invalid comment="input invalid"
# 3. ICMP desde interfaces locales (ping interno OK)
/ip firewall filter add chain=input action=accept \
protocol=icmp in-interface-list=!WAN comment="input ICMP local"
# 4. Jumps por zona
/ip firewall filter add chain=input action=jump \
in-interface-list=LAN jump-target=zone-LAN-to-router \
comment="input LAN→router"
/ip firewall filter add chain=input action=jump \
in-interface-list=DMZ jump-target=zone-DMZ-to-router \
comment="input DMZ→router"
/ip firewall filter add chain=input action=jump \
in-interface-list=WAN jump-target=zone-WAN-to-router \
comment="input WAN→router"
# 5. Default deny
/ip firewall filter add chain=input action=drop \
comment="input default deny"
Cada chain zone-<zona>-to-router define qué servicios del router son accesibles desde esa zona:
# LAN → router: acceso completo
/ip firewall filter add chain=zone-LAN-to-router action=accept \
comment="LAN puede todo al router"
# DMZ → router: solo DNS
/ip firewall filter add chain=zone-DMZ-to-router action=accept \
protocol=udp dst-port=53 comment="DMZ solo DNS al router"
/ip firewall filter add chain=zone-DMZ-to-router action=accept \
protocol=tcp dst-port=53 comment="DMZ solo DNS al router"
# WAN → router: solo puertos de VPN (WireGuard, etc.)
/ip firewall filter add chain=zone-WAN-to-router action=accept \
protocol=udp dst-port=13231 comment="WAN puertos WG al router"
Address-lists: reglas parametrizadas
Una de las cosas más útiles de RouterOS son las address-lists: listas de IPs que puedes referenciar en las reglas de firewall. En vez de hardcodear una IP en cada regla, la pones en una address-list y las reglas la referencian.
# Definir la address-list
/ip firewall address-list add address=10.0.1.50 list=workstation
/ip firewall address-list add address=10.0.1.51 list=workstation
# Usarla en una regla
/ip firewall filter add chain=zone-WAN-to-LAN action=accept \
protocol=tcp dst-port=22 dst-address-list=workstation \
comment="SSH solo a workstations"
Esto es especialmente útil para reglas que se repiten. Si una dirección IP cambia, actualizas la address-list y todas las reglas que la referencian se actualizan automáticamente.
Pitfalls (o los errores que yo cometí)
El orden de las reglas importa
RouterOS evalúa de arriba abajo, primera coincidencia gana. Si tienes una regla accept genérica arriba y una regla drop específica abajo, la drop nunca se ejecuta. Siempre añade reglas nuevas antes del default deny:
/ip firewall filter add chain=forward action=accept \
protocol=tcp dst-port=443 \
place-before=[find comment="fwd-default-deny"] \
comment="nueva-regla-HTTPS"
El default deny es la regla más importante
Sin la regla drop final al final de cada chain principal, todo lo que no matchee pasa por defecto. Es decir: tu firewall permitiría todo. La regla más importante es la que no deja pasar nada.
fasttrack salta el mangle
Si una conexión es fasttrackeada, no pasa por la chain mangle. Esto afecta a cosas como el MSS clamping o el split routing. Si necesitas que cierto tráfico pase por mangle, exclúyelo del fasttrack:
/ip firewall filter add chain=forward action=fasttrack-connection \
connection-state=established,related \
src-address-list=!svc-users \
dst-address-list=!svc-users \
comment="fasttrack excepto svc"
IPv6 es independiente
El firewall IPv6 es completamente independiente del IPv4. Si activas IPv6, necesitas configurar /ipv6 firewall filter por separado. Y no olvides que IPv6 necesita ICMPv6 para funcionar (Neighbor Discovery, Router Discovery, MTU exceeded…).
Interface lists incluyen interfaces por composición
Puedes definir una interface list que incluya otra interface list con include=. Así, si una interfaz entra en VLAN, y LAN incluye VLAN, entonces esa interfaz también está en LAN. Útil para tener una zona “confiable” compuesta por varias sublistas.
Escalabilidad: de 2 zonas a 8
La belleza de este patrón es que escala. Con dos zonas (LAN y WAN) tienes 4 pares posibles (LAN→LAN, LAN→WAN, WAN→LAN, WAN→WAN), pero solo 2-3 que necesiten reglas explícitas. Con 8 zonas, son 64 pares potenciales, pero probablemente solo 10-15 necesiten reglas.
Un ejemplo completo de flujo
Para cerrar, vamos a seguir un paquete desde que entra hasta que sale (o se cae):
Paquete: un dispositivo en IoT quiere visitar google.com
- Entra por
vlan-iot→ matcheain-interface-list=IoT - Chain
forward:- ¿Fasttrack? No (primera petición, no hay conexión establecida)
- ¿Established? No
- ¿Invalid? No
- ¿New from WAN? No (viene de IoT, no de WAN)
- ¿Jump
zone-IoT-to-WAN? Sí → saltar a esa chain
- Chain
zone-IoT-to-WAN:- ¿Hay regla accept? Sí → paquete aceptado
- Vuelve a chain
forward - ¿Hay más reglas? No importa, ya matcheó
- Paquete enviado a Internet
Paquete: el mismo dispositivo intenta acceder a 10.0.1.50 (una workstation en LAN)
- Entra por
vlan-iot→ matcheain-interface-list=IoT - Chain
forward:- ¿Fasttrack? No
- ¿Established? No
- ¿Invalid? No
- ¿New from WAN? No
- ¿Jump
zone-IoT-to-LAN? No existe → saltar a la siguiente - ¿Jump
zone-IoT-to-WAN?out-interface-list=WANno matchea (va a LAN) → no - ¿Alguno más? No
- Default deny → paquete descartado
Sin escribir ni una regla explícita de “deny IoT→LAN”, el paquete se cayó. La arquitectura lo hizo posible por omisión.
Conclusión
El firewall zone-based con jumps no es una característica mágica de RouterOS. Es un patrón de diseño que se construye sobre funcionalidades básicas: interface lists y jumps a chains. Pero el efecto es dramático:
- Legibilidad: cada chain
zone-*es un fragmento autocontenido que puedes leer y entender sin ver el resto del firewall. - Mantenibilidad: añadir una nueva VLAN es actualizar una interface list y, si hace falta, un par de reglas en las chains correspondientes.
- Seguridad por omisión: si no hay un jump para un par de zonas, el tráfico no pasa. No necesitas reglas de deny para cada combinación prohibida.
- Razonamiento: puedes responder a preguntas como “¿puede IoT hablar con LAN?” mirando si existe un jump IoT→LAN. No necesitas buscar entre 200 reglas lineales.
Si tienes un firewall de RouterOS con muchas reglas y sientes que es un ovillo, este patrón puede ayudarte. No es difícil de implementar — la parte difícil es decidir qué zonas necesitas y qué reglas tiene cada par. Pero eso, al menos, es un problema de diseño, no de sintaxis de firewall.
Y si estás empezando ahora con MikroTik, te lo digo por experiencia: hazlo con zone-based desde el principio. No te vas a arrepentir cuando tu firewall crezca.