Document cover
Repaso y hardenizado/blindaje de mi router Xiaomi con OpenWRT desde cero. Checklist personal
Como configurar tu router OpenWRT para que sea lo más seguro y útil en una red domestica pequeña.

Antes de empezar

Este checklist asume que ya tengo el router, Xiaomi AX3000T, recién flasheado con la última versión estable de OpenWrt, sin ninguna configuración previa aplicada todavía. La idea es documentar, en orden lógico, todo lo que hay que revisar y configurar para pasar de "router de fábrica" a "red doméstica razonablemente blindada".

Si quieres flashear el tuyo puedes ir al foro de OpenWrt donde dice como.

Mi topología de red va a ser esta, y la doy por hecha en el resto del documento:

    LAN principal (cableada + WiFi): mis dispositivos de confianza; portátiles, móviles, NAS, servidores propios. Rango: 192.168.0.0/24, router en 192.168.0.1.

    WLAN secundaria para IoT: una red WiFi separada (no una interfaz física distinta, sino un SSID propio) para todo lo que sea "cacharrería inteligente" como enchufes, bombillas, cámaras, sensores. Rango: 192.168.10.0/24, router en 192.168.10.1.

    VPN WireGuard: mi única puerta de entrada desde internet. Rango del túnel: 10.10.10.0/24, router en 10.10.10.1.

    IPv6: lo voy a desactivar en la LAN (RA/DHCPv6/NDP-Proxy apagados) porque no lo necesito de puertas para adentro y simplifica lo que tengo que asegurar. Pero lo voy a dejar activo en la WAN (mi ISP ya me delega un prefijo por DHCPv6-PD), por si algún día lo necesito, así no tengo que renegociar nada con el ISP cuando llegue ese momento, simplemente activarlo en la LAN cuando haga falta.

Una nota sobre el orden de las fases

La primera vez que hice esto, configuré el firewall primero y monté la VPN después y acabé teniendo que volver a tocar zonas y reglas para encajar WireGuard. Error clásico. En esta versión del documento, WireGuard va antes del firewall (Fase 5), para que cuando llegue a definir zonas y reglas ya tenga todas las redes creadas y pueda hacerlo del tirón, una sola vez.

Una nota sobre los comandos

Casi todo se puede hacer por LuCI (la interfaz web), y así lo explico. Pero pongo también los comandos de terminal equivalentes porque:

    Copiar/pegar un comando es más rápido y más fácil de documentar que "haz clic aquí, luego aquí".

    uci show <sección> te vuelca la configuración completa de un módulo de un plumazo - ideal para revisar, para pedir ayuda en un foro, o para guardarla en un documento como este.

    El día que algo no funcione, el log (logread) te dirá más que cualquier pantalla.

Ojo al compartir salidas de uci show: contienen claves privadas de WireGuard, contraseñas WiFi (option key) y tokens de DDNS en texto plano. Antes de pegar nada en un foro o un blog, censura esos campos.

Fase 0 - El backup de "por si todo sale mal"

Antes de tocar absolutamente nada, hago un backup de configuración, aunque el router esté prácticamente virgen. Es mi punto de retorno a cero, y cuesta diez segundos.

Por LuCI: System → Backup / Flash Firmware → Generate archive

Por terminal:

# Genera el backup en /tmp (que es RAM, se pierde al reiniciar) sysupgrade -b /tmp/backup-$(date +%Y%m%d).tar.gz # Y desde MI ORDENADOR, me lo traigo: scp root@192.168.0.1:/tmp/backup-*.tar.gz ~/backups-router/

El backup no vale de nada si vive solo dentro del router. Descárgalo siempre a tu PC.

También anoto en algún sitio (este mismo documento, por ejemplo):

    El modelo exacto del router y su variante de hardware. Esto es crítico: hay modelos que comparten nombre comercial pero llevan chips distintos e incompatibles (por ejemplo, el Xiaomi AX3000T existe en versión MediaTek MT7981B y en versión Qualcomm IPQ5018, flashear la imagen equivocada puede dejar el router inservible). Se comprueba así:

# Modelo y plataforma exacta cat /tmp/sysinfo/model cat /etc/openwrt_release ubus call system board

    Si el bootloader es el de fábrica o algo como breed, esto determina si tendré manga ancha para hacer rollback de firmware el día de mañana sin restricciones del fabricante. Con breed no hay anti-rollback y siempre puedes recuperar por TFTP; con el bootloader original de algunos fabricantes puedes encontrarte con que no te deja volver a una versión anterior.

Fase 1 - Blindar el acceso de administración (siempre lo primero)

Esto va antes que nada más porque, si alguien compromete el acceso al propio router, da igual lo bien que tenga montado el resto, tiene las llaves de todo.

1.1 Contraseña de root

Algo robusto y único, generado con gestor de contraseñas. Por terminal: passwd.

1.2 SSH por clave pública (y solo por clave)

Esto solo lo recomiendo si sabes que estas haciendo. Si decides que el tema de las keys no son lo tuyo puede estar bien cambiar el puerto de ssh del 22 a otro para dificultar el acceso.

Paso 1 — Genero el par de claves en mi ordenador (si no lo tengo ya):

ssh-keygen -t ed25519 -C "portatil" cat ~/.ssh/id_ed25519.pub

Paso 2 — Pego la clave pública en System → Administration → SSH-Keys. O por terminal:

echo "ssh-ed25519 AAAAC3Nza... portatil" >> /etc/dropbear/authorized_keys chmod 600 /etc/dropbear/authorized_keys

Paso 3 — VERIFICO que entra con la clave antes de tocar nada más. Dejo la sesión de LuCI abierta, abro otra terminal, y pruebo:

ssh root@192.168.0.1

Si entra sin pedirme contraseña, sigo. Si falla, no toco nada más hasta resolverlo, este es el paso donde la gente se queda fuera de su propio router.

Paso 4 — Ahora sí, cierro el acceso por contraseña:

uci set dropbear.@dropbear[0].PasswordAuth='0' uci set dropbear.@dropbear[0].RootPasswordAuth='0' uci set dropbear.@dropbear[0].Interface='lan' uci commit dropbear /etc/init.d/dropbear restart

Por LuCI es System → Administration → SSH Access: desmarcar Password authentication y Allow root logins with password, y poner Interface en lan.

Trampa que me pilló: en versiones recientes de LuCI apareció un checkbox nuevo, Bind to Interface, separado del desplegable "Interface". Puedes tener Interface: lan seleccionado y aun así dropbear seguir escuchando en todas las interfaces si ese checkbox no está marcado. Márcalo. Para verificarlo de verdad:

# ¿En qué IPs está escuchando el 22? netstat -tlnp | grep :22 # Debería mostrar solo 192.168.0.1:22, NO 0.0.0.0:22

1.3 LuCI por HTTPS

System → Administration → HTTP(S) Access → marcar Redirect to HTTPS. O:

uci set uhttpd.main.redirect_https='1' uci commit uhttpd /etc/init.d/uhttpd restart

El certificado autofirmado dará un aviso en el navegador la primera vez , es normal para uso doméstico, solo hay que aceptar la excepción una vez por dispositivo. También puedes usar Nginx Proxy Manager para dar una dirección con certificado, es lo que utilizo yo.

Con esto, el objetivo de fondo (que ni SSH ni LuCI sean alcanzables desde wan) queda apalabrado aquí, aunque la confirmación definitiva llega en la Fase 6, cuando monte el firewall.

Fase 2 - Definir la topología de red (interfaces)

Aquí es donde le doy forma a la separación LAN / IoT que comentaba al principio.

Para ver de un vistazo cómo está todo en cualquier momento:

uci show network

WAN

Configuro el protocolo según lo que pida mi ISP. En mi caso PPPoE sobre una VLAN etiquetada (algunos operadores de fibra en España piden la VLAN 20 o similar):

uci set network.wan.proto='pppoe' uci set network.wan.device='wan.20' uci set network.wan.username='miusuario@miisp' uci set network.wan.password='micontraseña' uci set network.wan.ipv6='auto' uci commit network

Dejo el IPv6 en modo automático (auto), para que si el ISP delega un prefijo por DHCPv6-PD, el router lo reciba y lo tenga disponible.

LAN principal

IP estática, protocolo static:

uci set network.lan.proto='static' uci set network.lan.ipaddr='192.168.0.1' uci set network.lan.netmask='255.255.255.0' uci commit network

Red IoT

Aquí la clave es que no es una interfaz física nueva, sino una red lógica propia a la que luego ataré un SSID de WiFi dedicado (Fase 4). Lo importante es que quede como una interfaz static independiente de la LAN, con su propio rango de IPs, para que el firewall pueda tratarla como una zona separada:

uci set network.iot=interface uci set network.iot.proto='static' uci set network.iot.ipaddr='192.168.10.1' uci set network.iot.netmask='255.255.255.0' uci commit network /etc/init.d/network reload

La decisión de IPv6: activo en WAN, apagado en LAN

Esta es la parte que quiero documentar bien para mi yo del futuro, porque tiene su matiz:

    En la interfaz LAN, voy a DHCP Server → IPv6 Settings y pongo RA-Service, DHCPv6-Service y NDP-Proxy en disabled. Esto hace que ningún dispositivo de mi LAN reciba una dirección IPv6 pública — se quedan solo con IPv4 (y, como mucho, una dirección fe80:: link-local que no sale de la red y no cuenta).

    No toco nada en la WAN : dejo que wan6 (el cliente DHCPv6) siga pidiendo y manteniendo su prefijo delegado con normalidad.

Por terminal:

uci set dhcp.lan.ra='disabled' uci set dhcp.lan.dhcpv6='disabled' uci set dhcp.lan.ndp='disabled' uci commit dhcp /etc/init.d/odhcpd restart

¿Por qué esta combinación y no simplemente apagar IPv6 del todo? Porque así el router mantiene la conectividad IPv6 "viva" de cara a mi ISP, sin exponer nada hacia mis dispositivos internos. Si en el futuro quiero activar IPv6 en la LAN, solo tengo que volver a esta pantalla y reactivar esos tres servicios. Pero aun así, con el tamaño de una red domestica, creo que IPv4 simplifica la gestión.

Para verificar que ha funcionado, desde un dispositivo de la LAN:

# Linux / Mac ip -6 addr # Windows ipconfig /all

Solo debería quedar, como mucho, alguna fe80::... (link-local, normal, no cuenta). Ninguna dirección global que empiece por 2xxx:.

Aviso importante que aprendí a las malas: hay una diferencia entre "el router tiene una IPv6 en su interfaz LAN" y "el router reparte IPv6 a los clientes". Lo primero es normal y no importa; lo segundo es lo que estamos apagando. Ver una IPv6 en br-lan en la pantalla de interfaces no significa que tus dispositivos la tengan.

Y otro: no desactives las reglas de firewall de ICMPv6 (Allow-ICMPv6-Input, Allow-ICMPv6-Forward) si vas a dejar IPv6 activo en algún sitio. IPv6 depende de ICMPv6 para funcionar (Neighbor Discovery, Path MTU Discovery). Desactivarlas mientras IPv6 sigue vivo da síntomas raros e intermitentes, webs que a veces cargan y a veces no, en vez de un apagado limpio.

Fase 3 - DHCP y DNS (dnsmasq / odhcpd)

Esto viene incluido de serie en OpenWrt, no hay que instalar nada extra. Para volcar toda la config de golpe:

uci show dhcp

Ajustes de dnsmasq

    Authoritative activado (mi router es la única fuente de verdad DHCP en la red).

    Rango DHCP coherente con el tamaño real de la subred. Ojo con este error tonto: si mi red es un /24 y pongo Start: 160 con Limit: 240, estoy pidiendo repartir de la .160 a la .399 , direcciones que no existen. dnsmasq simplemente se para en la .254, pero el número no refleja la realidad. Un Start: 100 + Limit: 150 (rango .100–.249) deja limpio el tramo .250–.254 para reservas estáticas. Si no planeas tener tantos dispositivos, corta el límite y déjale algo de holgura para posibles.

    Lease time razonable, 12h me vale.

    DHCP-Option 6,192.168.0.1 : esto fuerza a todos los clientes de la LAN a usar mi propio router como servidor DNS, en vez del que les proponga el ISP. Es la base para que cualquier filtrado de anuncios/tracking que monte más adelante aplique a toda la casa:

    uci add_list dhcp.lan.dhcp_option='6,192.168.0.1' # Y lo mismo para la red IoT, apuntando a SU gateway: uci add_list dhcp.iot.dhcp_option='6,192.168.10.1' uci commit dhcp

    Nota: El "6" no tiene nada que ver con IPv6, es el número de opción DHCP definido en el RFC 2132 que significa "Domain Name Server". Me confundió la primera vez que lo vi.

    TFTP server: desactivado (pestaña PXE/TFTP). Es un protocolo antiguo, sin autenticación ni cifrado; si no arranco máquinas por red en casa, fuera. Una menos de superficie expuesta.

    Non-wildcard activado (pestaña Devices & Ports): dnsmasq se ata solo a las IPs configuradas de sus interfaces en vez de escuchar en todas partes por defecto.

Ajustes de DNS

    DNSSEC activado (+ DNSSEC check unsigned): valida criptográficamente que las respuestas DNS no han sido manipuladas por el camino. Es gratis, y muy poca gente lo activa.

    Rebind protection activado: bloquea un ataque real (DNS rebinding) donde una web maliciosa intenta resolver a una IP privada mía para saltarse la seguridad del navegador y atacar mis paneles internos.

    Whitelist de rebind: si auto-hospedo servicios con un dominio público que resuelve a una IP privada cuando estoy en casa (split-horizon DNS), tengo que añadir ese dominio a la whitelist o la propia protección bloqueará mis propias respuestas.

    Local service only + Filter private activados: mi DNS no puede usarse como resolver abierto desde fuera, ni filtrar mi esquema de IPs internas mediante lookups inversos.

    Excluir wan explícitamente en Devices & Ports → Exclude interfaces: cinturón y tirantes, aunque el firewall ya debería bloquear esto por su cuenta.

    DNS upstream propio (1.1.1.1, 8.8.8.8) en vez de depender ciegamente del DNS que me imponga el ISP.

    Tamaño de caché DNS: el valor por defecto es 150 entradas, que se queda corto si tienes varios servicios propios y bastantes dispositivos. Subirlo a 1000–4000 mejora la velocidad de respuesta. Es rendimiento, no seguridad.

    Max EDNS0 packet size: 1232: el valor recomendado desde el "DNS Flag Day 2020" para evitar problemas de fragmentación con DNSSEC.

    Ejemplo por terminal de los principales:

    uci set dhcp.@dnsmasq[0].dnssec='1' uci set dhcp.@dnsmasq[0].dnsseccheckunsigned='1' uci set dhcp.@dnsmasq[0].rebind_protection='1' uci set dhcp.@dnsmasq[0].localservice='1' uci set dhcp.@dnsmasq[0].boguspriv='1' uci set dhcp.@dnsmasq[0].cachesize='2000' uci add_list dhcp.@dnsmasq[0].rebind_domain='mdominio.duckdns.org' uci add_list dhcp.@dnsmasq[0].server='1.1.1.1' uci add_list dhcp.@dnsmasq[0].server='8.8.8.8' uci commit dhcp /etc/init.d/dnsmasq restart

Registros locales (split-horizon)

Si auto-hospedo servicios, creo los registros en DNS → DNS Records → Hostnames para que, estando en casa, el dominio público resuelva directamente a la IP interna en vez de salir a internet y volver a entrar:

nextcloud.dominio.duckdns.org → 192.168.0.166 #ip de NPM vault.dominio.duckdns.org → 192.168.0.166 #ip de NPM router.dominio.duckdns.org → 192.168.0.166 #ip de NPM

Para verificar que todo el bloque DNS funciona:

# ¿Resuelve mi dominio interno a la IP privada? nslookup nextcloud.dominio.duckdns.org 192.168.0.1 # ¿Y qué tiene publicado el mundo exterior? (saltándome mi propio resolver) nslookup dominio.duckdns.org 1.1.1.1

Esa segunda consulta, forzando un DNS externo, es la única forma fiable de saber qué hay publicado de verdad. Si preguntas a tu propio router, te contestará tu anulación local y te engañarás a ti mismo. Este detalle me costó un buen rato de depuración con el DDNS (ver Fase 8).

Fase 4 - WiFi: dos redes, dos SSIDs

Aquí es donde la separación LAN/IoT se hace física de verdad, a nivel de radio. Volcado completo:

uci show wireless

Ajustes de radio

    Country code correcto (afecta a los canales y potencias permitidos legalmente). ES en mi caso.

    Analizar el espectro con Network → Wireless → Channel Analysis (viene integrado en LuCI moderno, no hace falta instalar nada). En 2.4GHz ceñirme siempre a los canales 1, 6 u 11 — son los únicos que no se solapan entre sí. En mi caso pasé del canal 9 (compartido con 5 redes de vecinos) al 11 (solo una), y se nota.

    En 2.4GHz, ancho de canal 20MHz. Usar 40MHz en una banda tan concurrida solo empeora las cosas: invades canales vecinos y provocas más colisiones.

    En 5GHz, si eliges un canal de la banda DFS (100 y superiores), tu router cambiará de canal automáticamente si detecta un radar cerca, y tras cada reinicio tardará 60-90 segundos extra en levantar el WiFi mientras hace el "CAC" (comprobación de disponibilidad de canal). Si alguna vez notas que el 5GHz "tarda en aparecer" tras reiniciar, es esto, no es un fallo. Los canales 36–48 no tienen DFS pero permiten menos potencia.

SSID principal (LAN)

    Cifrado WPA2/WPA3 mixto (sae-mixed en la configuración): el estándar recomendado actual, compatible con dispositivos algo más antiguos que aún no soportan WPA3 puro.

    Campo Network: solo lan.

SSID de IoT

    Un SSID completamente distinto, atado únicamente a la red iot.

    Cifrado: si tengo dispositivos IoT antiguos que no soportan WPA3, uso WPA2 puro (psk2) en este SSID en concreto. Prefiero tener compatibilidad garantizada en la red menos sensible antes que forzar WPA3 y que algún cacharro no conecte.

    Ejemplo completo de creación del SSID de IoT por terminal:

    uci add wireless wifi-iface uci set wireless.@wifi-iface[-1].device='radio0' uci set wireless.@wifi-iface[-1].mode='ap' uci set wireless.@wifi-iface[-1].ssid='MiCasa-IoT' uci set wireless.@wifi-iface[-1].encryption='psk2' uci set wireless.@wifi-iface[-1].key='UnaContraseñaLargaYUnica' uci set wireless.@wifi-iface[-1].network='iot' uci set wireless.@wifi-iface[-1].isolate='1' uci commit wireless wifi reload

Los dos detalles que más se olvidan

1. Comprobar dos veces el campo "Network" de cada SSID.

En General Setup → Network, un SSID puede tener varias redes marcadas a la vez. Si tu SSID principal tiene marcadas lan y iot, estás puenteando ambas redes a nivel de enlace, y toda la segmentación de firewall que montes después no sirve absolutamente de nada, el tráfico se mezcla antes de llegar al firewall. Verificación rápida:

uci get wireless.@wifi-iface[0].network # debe devolver solo: lan uci get wireless.@wifi-iface[2].network # debe devolver solo: iot

2. "Isolate Clients" en el SSID de IoT (Advanced Settings → Isolate Clients).

Esto impide que dos dispositivos conectados al mismo WiFi se hablen directamente a nivel de enlace, sin pasar por el router. Es imprescindible como complemento a las reglas de firewall: el Intra zone forward: reject que pondremos en la Fase 6 actúa una capa por encima y no ve ese tráfico si esto no está activado. Yo lo activo también en el SSID principal.

El filtrado por MAC lo dejo como algo opcional y de bajo valor real, se salta con facilidad falsificando la dirección, así que no me la juego solo a esto. Y ojo: dejar macfilter='deny' con la lista vacía no filtra nada, solo da falsa sensación de seguridad.

Fase 5 - VPN de acceso remoto (WireGuard)

Esta fase va antes del firewall a propósito. WireGuard crea una interfaz de red más (algineerWG, 10.10.10.0/24), y quiero tenerla ya existiendo cuando defina zonas y reglas — así el firewall se configura una sola vez y de forma coherente, en vez de tener que volver atrás a retocarlo.

WireGuard viene incluido en el kernel de Linux y el soporte básico suele estar en las imágenes de OpenWrt, aunque el paquete de LuCI (luci-proto-wireguard) puede necesitar instalarse según la imagen concreta que uses:

apk add wireguard-tools luci-proto-wireguard # OpenWrt 25.x en adelante (apk) opkg install wireguard-tools luci-proto-wireguard # versiones anteriores (opkg)

5.1 Generar las claves del servidor

wg genkey | tee /tmp/wg_server.key | wg pubkey > /tmp/wg_server.pub cat /tmp/wg_server.key # clave PRIVADA del router — nunca se comparte cat /tmp/wg_server.pub # clave PÚBLICA — esta va en cada cliente

5.2 Crear la interfaz

uci set network.algineerWG=interface uci set network.algineerWG.proto='wireguard' uci set network.algineerWG.private_key='<clave privada del router>' uci set network.algineerWG.listen_port='51820' uci add_list network.algineerWG.addresses='10.10.10.1/24' # Empujo mi DNS local a los clientes VPN, así también les aplica el filtrado uci add_list network.algineerWG.dns='192.168.0.1' uci commit network

El dns='192.168.0.1' es un detalle que agradezco: hace que mis dispositivos, cuando están conectados por VPN desde fuera, sigan usando mi resolver local y por tanto sigan teniendo bloqueo de anuncios y resolución de mis dominios internos.

5.3 Añadir peers (un peer por dispositivo o persona)

En cada dispositivo cliente genero su propio par de claves y le añado su entrada aquí. Nada de compartir la misma clave entre varios sitios — con un peer por dispositivo puedo revocar el acceso de uno solo sin afectar al resto.

uci add network wireguard_algineerWG uci set network.@wireguard_algineerWG[-1].description='MiMovil' uci set network.@wireguard_algineerWG[-1].public_key='<clave pública del móvil>' uci add_list network.@wireguard_algineerWG[-1].allowed_ips='10.10.10.2/32' uci set network.@wireguard_algineerWG[-1].route_allowed_ips='1' uci set network.@wireguard_algineerWG[-1].persistent_keepalive='25' uci commit network /etc/init.d/network reload

Ejemplo de asignación de IPs del túnel, para no perderme:


Peer

IP del túnel

Qué es

MiMovil

10.10.10.2/32

Mi teléfono

MiPortatil

10.10.10.3/32

Portátil de trabajo

TV-Salon

10.10.10.7/32

Stick de streaming


Un peer puede no ser un dispositivo, sino una red entera. Si algún día conectas otra vivienda (enlace site-to-site), su entrada tendrá un allowed_ips con dos valores: la IP del túnel y la subred remota completa (por ejemplo 10.10.10.8/32 y 192.168.8.0/24), más una ruta estática apuntando a ella. Eso significa que todos los dispositivos de esa red remota heredan el mismo nivel de acceso que cualquier otro peer de la VPN mucho más de lo que probablemente quieras. Lo trato en la Fase 7.

5.4 Acceso remoto por nombre: DDNS

Si mi IP pública es dinámica, los clientes necesitan un dominio en vez de una IP. Lo monto en la Fase 8 con ddns-scripts, pero lo apunto aquí porque conceptualmente pertenece a la VPN: el endpoint_host de todos mis clientes apunta a ese dominio, así que si el DDNS falla y la IP cambia, pierdo el acceso remoto de golpe. No es un accesorio, es parte crítica del acceso.

5.5 Verificación

# Estado del túnel: peers, último handshake, bytes transferidos wg show # ¿Está la interfaz arriba y con su IP? ip addr show algineerWG

Un peer con latest handshake reciente = conectado y funcionando. Si nunca ha hecho handshake, el problema suele ser: puerto no abierto en el firewall (Fase 7), DDNS mal, o claves cruzadas.

Fase 6 - Firewall: Zonas

Ahora sí, con todas las redes ya creadas (lan, wan, iot, algineerWG), defino las zonas de una sola vez. Volcado completo en cualquier momento:

uci show firewall

Una zona por cada red:

Zona

Redes

Input

Output

Forward

Intra-zona

Masquerade

lan

lan

accept

accept

→ wan, vpn

accept

❌

wan

wan, wan6

reject

accept

reject

reject

✅

iot

iot

reject

accept

→ wan

reject

✅

vpn

algineerWG

accept

accept

→ wan, lan

accept

✅

La idea de fondo:

    wan no acepta nada entrante salvo lo que yo abra explícitamente en la Fase 7.

    iot no puede ni siquiera hablar con mi propio router (Input: reject) salvo para lo estrictamente necesario (DHCP y DNS, Fase 7), y sus dispositivos no se ven entre sí (Intra zone forward: reject) una cámara comprometida no puede tocar a otra.

    iot NO tiene lan en su lista de destinos permitidos. Esto es lo que realmente bloquea el tráfico IoT → LAN: el comportamiento por defecto de nftables/firewall4 para pares de zonas no declarados es rechazar. No hace falta ninguna regla explícita de bloqueo.

    vpn puede llegar a mi LAN e internet.

Ejemplo de creación de la zona IoT por terminal:

uci add firewall zone uci set firewall.@zone[-1].name='iot' uci add_list firewall.@zone[-1].network='iot' uci set firewall.@zone[-1].input='REJECT' uci set firewall.@zone[-1].output='ACCEPT' uci set firewall.@zone[-1].forward='REJECT' uci set firewall.@zone[-1].masq='1' uci set firewall.@zone[-1].mtu_fix='1' # Y el reenvío que SÍ quiero: iot → wan uci add firewall forwarding uci set firewall.@forwarding[-1].src='iot' uci set firewall.@forwarding[-1].dest='wan' uci commit firewall /etc/init.d/firewall restart

Ajustes globales del firewall

En Network → Firewall → General Settings, dos casillas de coste cero y sin efectos secundarios:

uci set firewall.@defaults[0].synflood_protect='1' uci set firewall.@defaults[0].drop_invalid='1' uci commit firewall /etc/init.d/firewall restart

    SYN-flood protection: mitiga ataques de inundación de conexiones TCP a medio abrir.

    Drop invalid packets: descarta paquetes que no encajan en ningún estado de conexión conocido, a veces usados para evadir firewalls o escanear de forma sigilosa.

Cuidado con las "zonas decorativas". En su día creé una zona llamada bloquearIOTaLAN para "documentar" la intención de bloquear ese tráfico. El problema: no tenía ninguna red asignada (covered networks: unspecified), así que era completamente inerte, no bloqueaba nada, solo daba una falsa sensación de seguridad. Lo que protegía de verdad era, simplemente, que lan no estuviera en la lista de destinos de iot. Si una zona no tiene redes, no hace nada: mejor borrarla que confiar en ella.

Fase 7 - Firewall: Reglas de tráfico

7.1 Repasar las reglas por defecto

OpenWrt trae de fábrica: Allow-DHCP-Renew, Allow-Ping, Allow-IGMP, Allow-DHCPv6, Allow-MLD, Allow-ICMPv6-Input, Allow-ICMPv6-Forward. Ninguna compromete la seguridad.

    Allow-Ping: puedo desactivarlo si prefiero que mi router no responda a pings desde internet. Es más "perfil bajo" que seguridad real: un ping no expone ningún servicio ni permite ejecutar nada, solo confirma "hay algo vivo aquí". Cuestión de gustos.

    Las de ICMPv6: si dejo IPv6 activo en la WAN (mi caso), las dejo tal cual. Ver el aviso de la Fase 2.

7.2 Borrar reglas heredadas que no uso

Aquí es donde encontré basura real: tenía activas Allow-IPSec-ESP y Allow-ISAKMP (reenvío WAN → LAN de protocolo ESP y UDP/500) de una configuración IPsec antigua que ya no usaba, porque me había pasado a WireGuard. Puertos abiertos hacia mi LAN sin ningún propósito = superficie de ataque gratis.

# Listar todas las reglas con su índice, para saber qué borrar uci show firewall | grep "=rule" # Ver una en concreto uci show firewall.@rule[5]

Regla general: cualquier regla de un protocolo VPN que no uses, fuera.

7.3 La regla de entrada de WireGuard

Es la única puerta que abro desde internet:

uci add firewall rule uci set firewall.@rule[-1].name='Allow-WireGuard' uci set firewall.@rule[-1].src='wan' uci set firewall.@rule[-1].dest_port='51820' uci set firewall.@rule[-1].proto='udp' uci set firewall.@rule[-1].target='ACCEPT' uci commit firewall /etc/init.d/firewall restart

    Solo UDP (WireGuard no usa TCP).

    Un puerto exacto, nunca un rango.

    Sin restringir la IP de origen — mis dispositivos conectan desde cualquier red (datos móviles, WiFi ajeno...).

💡 El puerto por defecto 51820 es el primero que prueban los escáneres automáticos. No es un agujero real, WireGuard es "sordo y mudo" ante quien no tenga la clave correcta, no responde nada a paquetes no autenticados pero cambiarlo a un puerto alto arbitrario reduce el ruido en los logs. Cosmética de seguridad, opcional.

7.4 Excepciones para IoT (DHCP y DNS)

Como puse Input: reject en la zona iot, los dispositivos ya no pueden ni pedir IP. Necesito dos excepciones quirúrgicas:

# DHCP — SIN restricción de IP de destino uci add firewall rule uci set firewall.@rule[-1].name='Permitir DHCP a IoT' uci set firewall.@rule[-1].src='iot' uci set firewall.@rule[-1].proto='udp' uci set firewall.@rule[-1].dest_port='67-68' uci set firewall.@rule[-1].target='ACCEPT' # DNS — aquí SÍ puedo fijar la IP de destino uci add firewall rule uci set firewall.@rule[-1].name='Permitir DNS a IoT' uci set firewall.@rule[-1].src='iot' uci set firewall.@rule[-1].proto='tcp udp' uci set firewall.@rule[-1].dest_ip='192.168.10.1' uci set firewall.@rule[-1].dest_port='53' uci set firewall.@rule[-1].target='ACCEPT' uci commit firewall /etc/init.d/firewall restart

La trampa del DHCP. En la regla de DNS puedo fijar dest_ip porque las consultas DNS siempre van dirigidas (unicast) a una IP que el cliente ya conoce. Pero en DHCP no debo fijarla: cuando un dispositivo nuevo se conecta por primera vez, todavía no tiene IP y no sabe cuál es el servidor, así que el DHCPDISCOVER inicial va por broadcast (255.255.255.255). Si fijas la IP de destino, los dispositivos que ya tenían lease seguirán renovando bien, pero ningún dispositivo nuevo conseguirá IP jamás, un fallo que tarda semanas en aparecer y es un infierno de diagnosticar.

7.5 Forzar el DNS local (bloquear el 53 saliente)

El DHCP-Option 6 de la Fase 3 es una sugerencia que cualquier dispositivo (o malware) puede ignorar usando 8.8.8.8 directamente. Esta regla lo convierte en obligación:

uci add firewall rule uci set firewall.@rule[-1].name='Bloquear DNS directo a WAN' uci set firewall.@rule[-1].src='iot' uci set firewall.@rule[-1].dest='wan' uci set firewall.@rule[-1].proto='tcp udp' uci set firewall.@rule[-1].dest_port='53' uci set firewall.@rule[-1].target='REJECT' uci commit firewall /etc/init.d/firewall restart

Nota importante: esta regla es de tipo forward (afecta al tráfico reenviado de mis clientes hacia fuera), no de tipo output. El propio dnsmasq del router sigue pudiendo consultar a sus servidores upstream con total normalidad.

Uso REJECT y no DROP a propósito: con reject, la app falla rápido y sé enseguida qué se ha roto, en vez de quedarme esperando un timeout eterno.

¿Aplicarlo también a la LAN? Es un trade-off consciente. Si mis dispositivos tienen 1.1.1.1 como DNS secundario para que internet siga funcionando si mi resolver local se cae, esta regla se lo rompería. La alternativa elegante es mover ese "plan B" al propio router (configurar 1.1.1.1 como upstream de dnsmasq, que ya está en la Fase 3) y entonces sí bloquear el 53 saliente también en LAN: mantienes la resiliencia y ganas que el filtrado de anuncios aplique siempre.

Límite conocido: esto no detiene DNS-over-HTTPS (DoH), que viaja camuflado por el puerto 443 como tráfico web normal. Bloquear DoH es harina de otro costal (listas de IPs de resolvers conocidos, y con riesgo de romper cosas). Lo dejo apuntado como tarea futura.

7.6 Excepciones entre segmentos: quirúrgicas, siempre

Cuando necesito que un dispositivo de la LAN llegue a algo concreto de la red IoT, no abro la zona entera. Ejemplo real: acceder al panel web de un cacharro que vive en la red IoT:

uci add firewall rule uci set firewall.@rule[-1].name='LAN al panel del cacharro' uci set firewall.@rule[-1].src='lan' uci set firewall.@rule[-1].dest='iot' uci set firewall.@rule[-1].dest_ip='192.168.10.145' uci set firewall.@rule[-1].proto='tcp' uci set firewall.@rule[-1].dest_port='443' uci set firewall.@rule[-1].target='ACCEPT' uci commit firewall

Host concreto + puertos concretos. Este es el patrón a repetir para cualquier excepción entre segmentos.

7.7 Acotar el alcance de la VPN

Si algún peer es en realidad un enlace a otra red completa (segunda vivienda, casa de un familiar), no lo trates como "un dispositivo más". Tu seguridad pasa a depender de lo bien protegida que esté esa otra red, sobre la que probablemente no tengas control diario.

Ejemplo de acotación puntual (bloquear una cámara concreta de la red remota, permitiendo el resto):

uci add firewall rule uci set firewall.@rule[-1].name='Bloquear camara remota hacia LAN' uci set firewall.@rule[-1].src='vpn' uci set firewall.@rule[-1].src_ip='192.168.8.50' uci set firewall.@rule[-1].dest='lan' uci set firewall.@rule[-1].target='REJECT' uci commit firewall

Al ser una coincidencia más específica que la política general de la zona, tiene prioridad sobre ella.

Fase 8 - Extras que sí requieren instalar algo

Todo lo que viene a continuación no forma parte de una instalación limpia de OpenWrt, hay que instalarlo explícitamente desde System → Software o por terminal.

# OpenWrt 25.12 en adelante usa apk: apk update apk add <paquete> # Versiones anteriores usan opkg: opkg update opkg install <paquete>

UPnP (miniupnpd + luci-app-upnp)

Mi recomendación por defecto: no instalarlo, o desinstalarlo si venía en la imagen de fábrica. Permite que cualquier dispositivo de la red se abra puertos hacia internet sin pedir permiso, saltándose todo el trabajo de segmentación del firewall porque inserta sus propias reglas dinámicas de NAT sobre la marcha. Es la contradicción de todo lo que hemos montado.

apk del luci-app-upnp miniupnpd # o miniupnpd-nftables

Si de verdad lo necesitas para un caso concreto (una consola con NAT estricto), al menos limítalo a la zona lan y activa el "secure mode".

Adblock (adblock + luci-app-adblock)

Bloqueo de anuncios y tracking a nivel de DNS para toda la red.

    Un par de feeds sólidos (adguard, stevenblack) es más que suficiente; no hace falta activarlos todos.

    Comprobar que detecta bien el DNS Backend (debería ser dnsmasq).

    Verificación real, que vale más que cualquier panel:

service adblock status nslookup doubleclick.net 192.168.0.1 # Debe devolver 0.0.0.0 o NXDOMAIN. Si devuelve una IP real de Google, no está bloqueando.

La trampa de los triggers. Si tras configurarlo el panel aparece vacío y logread | grep adblock no devuelve nada, no está necesariamente roto: el script no se ejecuta al llamarlo con start, sino que registra un disparador que espera al evento "una interfaz se ha levantado". Si esas interfaces ya estaban arriba, el evento no volverá a ocurrir. Un reboot del router lo resuelve, porque provoca la secuencia de arranque completa que el script espera. Perdí un buen rato con esto pensando que era un fallo del paquete.

banIP (banip + luci-app-banip)

Bloqueo automático de IPs con mala reputación y de intentos de fuerza bruta.

    Inbound Block Policy: drop (no reject) — así los atacantes reciben silencio en vez de una confirmación clara de que hay algo ahí. Fíjate que es lo contrario de lo que recomendé para el bloqueo de DNS interno: ahí queríamos fallo rápido para depurar; aquí queremos que el atacante se quede a oscuras.

    Feeds: allowlist, blocklist, bruteforceblock, ipsum, ipthreat es una combinación sólida.

    Bloqueo por país: válido y efectivo, pero elige países con volumen real de escaneo automatizado, no al azar. Y asegúrate de tener Auto Allowlist activo, o el día que viajes a uno de esos países te bloquearás a ti mismo tu propia VPN.

    Log Count en 2 o 3, no en 1 — con umbral 1, un simple error de tecleo en tu contraseña de SSH te autobanea.

    No se autorefresca solo. Hay que añadir una tarea programada (System → Scheduled Tasks):

echo "0 4 * * * /etc/init.d/banip reload" >> /etc/crontabs/root /etc/init.d/cron restart

Para comprobar que está trabajando de verdad:

logread | grep -i banip # Y las capturas en tiempo real: logread -f | grep banIP

Ver líneas del tipo banIP/inbound/drop/ipsum.v4: ... SRC=x.x.x.x ... DROP significa que está parando escaneos reales contra tu IP pública.

QoS / Control de ancho de banda

El paquete concreto varía según la imagen (luci-app-sqm, o alguna variante de "QoS traffic shaping"). Sea cual sea:

    Capar el total un ~10% por debajo de tu velocidad contratada real para evitar el bufferbloat (esa sensación de "internet lento" cuando alguien está descargando a tope). Con 300Mbps contratados, poner 280.

    Aplicar un límite bajo a la red IoT (por ejemplo 5/10 Mbps) — no solo por rendimiento, también como capa extra de seguridad: si un cacharro se ve comprometido, su capacidad de participar en un DDoS o exfiltrar datos queda limitada de raíz.

    No olvidar la subred de la VPN. Es facilísimo dejarla fuera sin querer: las reglas suelen escribirse para 192.168.x.0/24 y el túnel vive en 10.10.10.0/24.

    Borra la regla example que viene de plantilla (suele apuntar a 192.168.1.0/24, que probablemente no es tu red).

    Detalle a saber: estos límites suelen ser techos independientes por subred, no una tarta repartida. Si LAN(280) + VPN(250) + IoT(10) suman mucho más que tu línea real, en el caso puntual de que coincidan todos a tope el bufferbloat puede reaparecer. Poco probable en uso doméstico, pero conviene saberlo.

Watchcat (watchcat + luci-app-watchcat)

Un vigilante de conectividad que actúa si detecta que se ha caído la conexión.

    Modo Restart Interface (apuntando a pppoe-wan o tu interfaz WAN) en vez de Ping Reboot. Reiniciar solo la WAN resuelve la mayoría de cuelgues típicos (sesión PPPoE atascada) sin tirar abajo tu WiFi, tu LAN y tu VPN. Y si el problema es una caída real del ISP, reiniciar el router entero no arregla nada, solo añade una interrupción extra.

    Al menos dos hosts a comprobar (1.1.1.1 y 8.8.8.8, de proveedores distintos). Con uno solo, un hipo puntual de ese proveedor te provoca una acción innecesaria.

    Periodo de 20 - 30 minutos y check interval de 30s es un buen equilibrio.

Dynamic DNS (ddns-scripts + luci-app-ddns)

Imprescindible si tu IP pública es dinámica y quieres que la VPN de la Fase 5 siga siendo alcanzable por nombre.

    IP address source: apunta a la interfaz WAN real (wan / pppoe-wan), o mejor aún, usa el método basado en URL web (que pregunta "¿qué IP ves tú de mí?"), más fiable con PPPoE.

    La trampa del auto-engaño. Si tienes DNS local con anulaciones para tus propios dominios (Fase 3), debes configurar un DNS-Server externo explícito (1.1.1.1) en Advanced Settings del servicio DDNS. Si lo dejas vacío, el script comprueba si su actualización funcionó preguntándole a su propio resolver local, que le devuelve la anulación interna (192.168.0.1) en vez de la respuesta pública real y entra en un bucle infinito de "actualización fallida" que en realidad no lo es. En mi caso llevaba 38 reintentos fallidos acumulados y todo estaba bien de puertas afuera.

Diagnóstico:

logread | grep -i ddns | tail -30 # Y la comprobación de la verdad, preguntando FUERA: nslookup mi-dominio.ddns.org 1.1.1.1

Si esa consulta devuelve tu IP pública real, funciona. Si devuelve 192.168.x.x, tienes el problema descrito arriba.

Fase 9 - Auditoría de paquetes instalados

Especialmente importante si el firmware no es el oficial de openwrt.org, sino una build de un fabricante o de la comunidad.

Sacar el listado completo

# OpenWrt 25.12 en adelante: apk list --installed > /tmp/paquetes.txt # Versiones anteriores: opkg list-installed > /tmp/paquetes.txt # Contar cuántos hay (útil para comparar antes/después de la limpieza) apk list --installed | wc -l # Y me lo llevo a mi PC para revisarlo con calma # (desde MI ordenador): scp root@192.168.0.1:/tmp/paquetes.txt .

Guarda ese archivo. Es lo que te permitirá comparar tras una actualización de firmware y detectar qué se quedó fuera.

Puedes preguntar a tu LLM de confianza que te analice los paquetes y te sugiera si sobra alguno con el objetivo que buscas.

Qué buscar

Repásalo entero, línea a línea si hace falta. En builds de terceros (como la que instalé) puedes encontrar cosas que nunca pediste. En la mía apareció, entre otras:

    Un kit completo de evasión de censura (zapret, podkop, ruantiblock, amneziawg) herramientas legítimas para saltarse el bloqueo de internet en otros países, pero completamente inútiles y superfluas en mi contexto.

    Un logger de URLs (urllogger) que registraba tráfico de mi red sin que yo lo hubiera decidido.

    Protocolos VPN antiguos e inseguros "por si acaso": pptp, l2tp, openvpn que no usaba.

    Túneles que no necesito: 6in4, 6rd, 6to4, gre, batman-adv, relayd.

    Un puñado de paquetes de idioma de un idioma que no hablo (luci-i18n-*-ru).

    Terminales web redundantes (ttyd) teniendo ya SSH bien configurado.

Cómo saber si algo se usa antes de borrarlo

# ¿Se borraría solo, o arrastra dependencias? (NO borra nada, solo simula) apk del <paquete> --simulate # ¿Hay alguna interfaz usando este protocolo? (ejemplo con GRE) uci show network | grep -i gre # ¿Hay algún proceso corriendo? ps | grep -i <nombre> # ¿Escucha en algún puerto? netstat -tlnp

Cómo borrar

# Varios de golpe, nombres explícitos apk del openvpn-mbedtls luci-app-openvpn batctl-full kmod-batman-adv 6in4 6rd 6to4

Tres lecciones que aprendí borrando:

    Un paquete "motor" y su luci-app-* suelen ser dos paquetes separados. Si solo borras la app de LuCI, el demonio sigue corriendo. Bórralos juntos en el mismo comando: apk del relayd luci-proto-relay.

    Cuidado con las regex. Usé grep -oE 'luci-i18n-[a-zA-Z0-9]+-ru' para borrar paquetes de idioma en bloque, y falló de dos formas: se dejó fuera los que tenían guion interno (luci-i18n-package-manager-ru), y un grep -- '-ru' a secas también capturaba luci-lua-**ru**ntime — que es el runtime de Lua del que depende toda la interfaz web. Lista primero, revisa, borra después.

    Algunos paquetes "raros" tienen una razón de ser. kmod-mtd-rw y facinstall parecían basura, pero resultaron ser las herramientas que permiten volver al firmware de fábrica desde OpenWrt en routers Xiaomi. No son un riesgo (no escuchan en ningún puerto, no corren de fondo) y son tu botón de pánico. Investiga antes de borrar.

Fase 10 - Verificación final

Antes de dar el trabajo por terminado:

reboot

Y después, la ronda de comprobaciones:

Qué compruebo

Cómo

Resultado esperado

Un dispositivo LAN coge IP y resuelve DNS

conectar y navegar

✅ funciona

Un dispositivo IoT coge IP y sale a internet

conectar y navegar

✅ funciona

IoT no llega a la LAN

ping 192.168.0.166 desde IoT

❌ debe fallar

IoT no ve a otro IoT

ping 192.168.10.x entre dos IoT

❌ debe fallar

IoT no llega al panel del router

abrir http://192.168.10.1 desde IoT

❌ debe fallar

La VPN conecta desde fuera

wg show con handshake reciente

✅ funciona

SSH/LuCI no alcanzables desde WAN

escáner de puertos externo

❌ cerrado

Adblock bloquea

nslookup doubleclick.net 192.168.0.1

0.0.0.0 / NXDOMAIN

DDNS publica la IP correcta

nslookup midominio.duckdns.org 1.1.1.1

IP pública real

banIP está activo

logread | grep -i banip

líneas de drop

Comandos útiles para el repaso:

# Estado general ubus call system board uptime # ¿Qué está escuchando y en qué interfaz? netstat -tlnp netstat -ulnp # El ruleset de firewall realmente cargado (no lo que dice la config) nft list ruleset | less # Logs recientes de todo logread | tail -50

Y por último:

    Guardar un backup de configuración fuera del router (sysupgrade -b), ahora que todo está como quieres.

    Guardar la lista de paquetes (apk list --installed).

    Guardar una captura del System Overview con la versión de firmware y kernel, como referencia "antes" para la próxima actualización.

Bonus: cosas que solo se aprenden rompiéndolas

Un par de cosas que no encajan en ninguna fase pero que me costaron tiempo:

Actualizaciones de versión mayor. Al saltar de OpenWrt 24.x a 25.x, el gestor de paquetes cambia de opkg a apk. La configuración se migra sola sin drama, pero conviene guardar el listado de paquetes antes para comparar después. Y verifica siempre el checksum de la imagen antes de flashear:

# En Windows / PowerShell Get-FileHash .\openwrt-...-sysupgrade.bin -Algorithm SHA256 Get-Content .\sha256sums | Select-String "sysupgrade.bin"
# En Linux / Mac sha256sum openwrt-...-sysupgrade.bin

De los archivos de una release, el que necesitas para actualizar desde un OpenWrt que ya funciona es el -sysupgrade.bin. Los que llevan initramfs en el nombre son para recuperación o para la primera instalación desde el firmware de fábrica.

El fin... de momento.

Escribo todo esto sobre todo para mí mismo, para el día que toque migrar de router y no tener que reconstruir de memoria cada decisión que tomé y por qué. Si a alguien más le sirve de plantilla, mejor que mejor, pero el objetivo principal es no tener que empezar de cero la próxima vez.

Y si algo se queda sin hacer, mejor apuntarlo que fingir que no existe. Mi lista de pendientes actual:

    [ ] Bloqueo de DNS-over-HTTPS, que se salta la regla del puerto 53.

    [ ] Acotar el acceso de la VPN a la LAN por host/puerto, en vez de acceso total.

    [ ] Limitar iot → wan a los puertos que realmente necesitan los dispositivos (80/443/53/123), en vez de "todo permitido".

Do you like what you are reading? Subscribe to receive updates.

Unsubscribe anytime